diff --git a/app/src/main/java/com/idormy/sms/forwarder/adapter/FrpcPagingAdapter.kt b/app/src/main/java/com/idormy/sms/forwarder/adapter/FrpcPagingAdapter.kt index 499c6af4..d328e439 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/adapter/FrpcPagingAdapter.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/adapter/FrpcPagingAdapter.kt @@ -1,77 +1,83 @@ -package com.idormy.sms.forwarder.adapter - -import android.annotation.SuppressLint -import android.os.Build -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.paging.PagingDataAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.idormy.sms.forwarder.R -import com.idormy.sms.forwarder.adapter.FrpcPagingAdapter.MyViewHolder -import com.idormy.sms.forwarder.database.entity.Frpc -import com.idormy.sms.forwarder.databinding.AdapterFrpcsCardViewListItemBinding -import com.xuexiang.xutil.resource.ResUtils.getColors -import frpclib.Frpclib - -class FrpcPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter(diffCallback) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { - val binding = AdapterFrpcsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return MyViewHolder(binding) - } - - override fun onBindViewHolder(holder: MyViewHolder, position: Int) { - val item = getItem(position) - if (item != null) { - holder.binding.ivImage.setImageResource(R.drawable.ic_menu_frpc) - holder.binding.ivAutorun.setImageResource(item.autorunImageId) - holder.binding.tvName.text = item.name - - if (item.connecting || Frpclib.isRunning(item.uid)) { - holder.binding.ivPlay.setImageResource(R.drawable.ic_stop) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - holder.binding.ivPlay.imageTintList = getColors(R.color.colorStop) - } - } else { - holder.binding.ivPlay.setImageResource(R.drawable.ic_start) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - holder.binding.ivPlay.imageTintList = getColors(R.color.colorStart) - } - } - - holder.binding.ivEdit.setImageResource(R.drawable.ic_edit) - holder.binding.ivDelete.setImageResource(R.drawable.ic_delete) - - holder.binding.ivPlay.setOnClickListener { view: View? -> - itemClickListener.onItemClicked(view, item) - } - holder.binding.ivEdit.setOnClickListener { view: View? -> - itemClickListener.onItemClicked(view, item) - } - holder.binding.ivDelete.setOnClickListener { view: View? -> - itemClickListener.onItemClicked(view, item) - } - } - } - - class MyViewHolder(val binding: AdapterFrpcsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root) - interface OnItemClickListener { - fun onItemClicked(view: View?, item: Frpc) - fun onItemRemove(view: View?, id: Int) - } - - companion object { - var diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Frpc, newItem: Frpc): Boolean { - return oldItem.uid == newItem.uid - } - - @SuppressLint("DiffUtilEquals") - override fun areContentsTheSame(oldItem: Frpc, newItem: Frpc): Boolean { - return oldItem === newItem - } - } - } +package com.idormy.sms.forwarder.adapter + +import android.annotation.SuppressLint +import android.os.Build +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.adapter.FrpcPagingAdapter.MyViewHolder +import com.idormy.sms.forwarder.database.entity.Frpc +import com.idormy.sms.forwarder.databinding.AdapterFrpcsCardViewListItemBinding +import com.xuexiang.xutil.resource.ResUtils.getColors +import frpclib.Frpclib + +class FrpcPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter(diffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { + val binding = AdapterFrpcsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return MyViewHolder(binding) + } + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: MyViewHolder, position: Int) { + val item = getItem(position) + if (item != null) { + holder.binding.ivImage.setImageResource(R.drawable.ic_menu_frpc) + holder.binding.ivAutorun.setImageResource(item.autorunImageId) + holder.binding.tvUid.text = "UID:${item.uid}" + holder.binding.tvName.text = item.name + + if (item.connecting || Frpclib.isRunning(item.uid)) { + holder.binding.ivPlay.setImageResource(R.drawable.ic_stop) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + holder.binding.ivPlay.imageTintList = getColors(R.color.colorStop) + } + } else { + holder.binding.ivPlay.setImageResource(R.drawable.ic_start) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + holder.binding.ivPlay.imageTintList = getColors(R.color.colorStart) + } + } + + holder.binding.ivCopy.setImageResource(R.drawable.ic_copy) + holder.binding.ivEdit.setImageResource(R.drawable.ic_edit) + holder.binding.ivDelete.setImageResource(R.drawable.ic_delete) + + holder.binding.ivCopy.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + } + holder.binding.ivPlay.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + } + holder.binding.ivEdit.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + } + holder.binding.ivDelete.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + } + } + } + + class MyViewHolder(val binding: AdapterFrpcsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root) + interface OnItemClickListener { + fun onItemClicked(view: View?, item: Frpc) + fun onItemRemove(view: View?, id: Int) + } + + companion object { + var diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Frpc, newItem: Frpc): Boolean { + return oldItem.uid == newItem.uid + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: Frpc, newItem: Frpc): Boolean { + return oldItem === newItem + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/database/dao/FrpcDao.kt b/app/src/main/java/com/idormy/sms/forwarder/database/dao/FrpcDao.kt index fced7f92..e4a0f84d 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/database/dao/FrpcDao.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/database/dao/FrpcDao.kt @@ -1,39 +1,42 @@ -package com.idormy.sms.forwarder.database.dao - -import androidx.paging.PagingSource -import androidx.room.* -import com.idormy.sms.forwarder.database.entity.Frpc -import io.reactivex.Single - -@Dao -interface FrpcDao { - - @Insert - fun insert(frpc: Frpc) - - @Delete - fun delete(frpc: Frpc) - - @Query("DELETE FROM Frpc where uid=:uid") - fun delete(uid: String) - - @Update - fun update(frpc: Frpc) - - @Query("SELECT * FROM Frpc where uid=:uid") - fun get(uid: String): Single - - //TODO:允许主线程访问,后面再优化 - @Query("SELECT * FROM Frpc where autorun=1") - fun getAutorun(): List - - @Query("SELECT * FROM Frpc ORDER BY time DESC") - fun pagingSource(): PagingSource - - //TODO:允许主线程访问,后面再优化 - @Query("SELECT * FROM Frpc ORDER BY time ASC") - fun getAll(): List - - @Query("DELETE FROM Frpc") - fun deleteAll() +package com.idormy.sms.forwarder.database.dao + +import androidx.paging.PagingSource +import androidx.room.* +import com.idormy.sms.forwarder.database.entity.Frpc +import io.reactivex.Single + +@Dao +interface FrpcDao { + + @Insert + fun insert(frpc: Frpc) + + @Delete + fun delete(frpc: Frpc) + + @Query("DELETE FROM Frpc where uid=:uid") + fun delete(uid: String) + + @Update + fun update(frpc: Frpc) + + @Query("SELECT * FROM Frpc where uid=:uid") + fun get(uid: String): Single + + //TODO:允许主线程访问,后面再优化 + @Query("SELECT * FROM Frpc where uid=:uid") + fun getOne(uid: String): Frpc + + @Query("SELECT * FROM Frpc where autorun=1") + fun getAutorun(): List + + @Query("SELECT * FROM Frpc ORDER BY time DESC") + fun pagingSource(): PagingSource + + //TODO:允许主线程访问,后面再优化 + @Query("SELECT * FROM Frpc ORDER BY time ASC") + fun getAll(): List + + @Query("DELETE FROM Frpc") + fun deleteAll() } \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt index 39cc854f..399dec5a 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt @@ -9,84 +9,130 @@ import java.io.Serializable data class CloneInfo( @SerializedName("version_code") var versionCode: Int = 0, + @SerializedName("version_name") var versionName: String? = null, + @SerializedName("enable_sms") var enableSms: Boolean = false, + @SerializedName("enable_phone") var enablePhone: Boolean = false, + @SerializedName("call_type1") var callType1: Boolean = false, + @SerializedName("call_type2") var callType2: Boolean = false, + @SerializedName("call_type3") var callType3: Boolean = false, + @SerializedName("call_type4") var callType4: Boolean = false, + @SerializedName("call_type5") var callType5: Boolean = false, + @SerializedName("call_type6") var callType6: Boolean = false, + @SerializedName("enable_app_notify") var enableAppNotify: Boolean = false, + @SerializedName("cancel_app_notify") var cancelAppNotify: Boolean = false, + @SerializedName("cancel_extra_app_notify") var cancelExtraAppNotify: String? = null, + @SerializedName("enable_not_user_present") var enableNotUserPresent: Boolean = false, + @SerializedName("enable_load_app_list") var enableLoadAppList: Boolean = false, + @SerializedName("enable_load_user_app_list") var enableLoadUserAppList: Boolean = false, + @SerializedName("enable_load_system_app_list") var enableLoadSystemAppList: Boolean = false, + @SerializedName("duplicate_messages_limits") var duplicateMessagesLimits: Int = 0, + @SerializedName("enable_network_state_receiver") var enableNetworkStateReceiver: Boolean = false, + @SerializedName("enable_battery_receiver") var enableBatteryReceiver: Boolean = false, + @SerializedName("battery_level_min") var batteryLevelMin: Int = 0, + @SerializedName("battery_level_max") var batteryLevelMax: Int = 0, + @SerializedName("battery_level_once") var batteryLevelOnce: Boolean = false, + @SerializedName("enable_battery_cron") var enableBatteryCron: Boolean = false, + @SerializedName("battery_cron_start_time") var batteryCronStartTime: String? = null, + @SerializedName("battery_cron_interval") var batteryCronInterval: Int = 0, + @SerializedName("enable_exclude_from_recents") var enableExcludeFromRecents: Boolean = false, + @SerializedName("enable_cactus") var enableCactus: Boolean = false, + @SerializedName("enable_play_silence_music") var enablePlaySilenceMusic: Boolean = false, + @SerializedName("enable_one_pixel_activity") var enableOnePixelActivity: Boolean = false, + @SerializedName("request_retry_times") var requestRetryTimes: Int = 0, + @SerializedName("request_delay_time") var requestDelayTime: Int = 0, + @SerializedName("request_timeout") var requestTimeout: Int = 0, + @SerializedName("notify_content") var notifyContent: String? = null, + @SerializedName("enable_sms_template") var enableSmsTemplate: Boolean = false, + @SerializedName("sms_template") var smsTemplate: String? = null, + @SerializedName("enable_help_tip") var enableHelpTip: Boolean = false, + @SerializedName("enable_pure_client_mode") var enablePureClientMode: Boolean = false, + + @SerializedName("enable_sms_command") + var enableSmsCommand: Boolean = false, + + @SerializedName("sms_command_safe_phone") + var smsCommandSafePhone: String? = null, + @SerializedName("sender_list") var senderList: List? = null, + @SerializedName("rule_list") var ruleList: List? = null, + @SerializedName("frpc_list") var frpcList: List? = null, ) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt index bd0f9b67..21819f50 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt @@ -1,202 +1,203 @@ -package com.idormy.sms.forwarder.fragment - -import android.content.Intent -import android.os.Build -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView.RecycledViewPool -import com.alibaba.android.vlayout.VirtualLayoutManager -import com.idormy.sms.forwarder.R -import com.idormy.sms.forwarder.adapter.FrpcPagingAdapter -import com.idormy.sms.forwarder.core.BaseFragment -import com.idormy.sms.forwarder.database.entity.Frpc -import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory -import com.idormy.sms.forwarder.database.viewmodel.FrpcViewModel -import com.idormy.sms.forwarder.databinding.FragmentFrpcsBinding -import com.idormy.sms.forwarder.service.ForegroundService -import com.idormy.sms.forwarder.utils.* -import com.jeremyliao.liveeventbus.LiveEventBus -import com.scwang.smartrefresh.layout.api.RefreshLayout -import com.xuexiang.xaop.annotation.SingleClick -import com.xuexiang.xpage.annotation.Page -import com.xuexiang.xpage.base.XPageActivity -import com.xuexiang.xpage.core.PageOption -import com.xuexiang.xui.utils.ThemeUtils -import com.xuexiang.xui.utils.WidgetUtils -import com.xuexiang.xui.widget.actionbar.TitleBar -import com.xuexiang.xui.widget.dialog.LoadingDialog -import frpclib.Frpclib -import io.reactivex.CompletableObserver -import io.reactivex.Observer -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - - -@Suppress("DEPRECATION") -@Page(name = "Frp内网穿透") -class FrpcFragment : BaseFragment(), FrpcPagingAdapter.OnItemClickListener { - - var titleBar: TitleBar? = null - private var adapter = FrpcPagingAdapter(this) - private val viewModel by viewModels { BaseViewModelFactory(context) } - - override fun viewBindingInflate( - inflater: LayoutInflater, - container: ViewGroup, - ): FragmentFrpcsBinding { - return FragmentFrpcsBinding.inflate(inflater, container, false) - } - - override fun initTitle(): TitleBar? { - titleBar = super.initTitle()!!.setImmersive(false) - titleBar!!.setTitle(R.string.menu_frpc) - titleBar!!.setActionTextColor(ThemeUtils.resolveColor(context, R.attr.colorAccent)) - titleBar!!.addAction(object : TitleBar.ImageAction(R.drawable.ic_add) { - @SingleClick - override fun performAction(view: View) { - FrpcUtils.getStringFromRaw(context!!, R.raw.frpc) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) {} - override fun onNext(content: String) { - LiveEventBus.get(INTENT_FRPC_EDIT_FILE).post(Frpc(content)) - PageOption.to(FrpcEditFragment::class.java).setNewActivity(true).open((context as XPageActivity?)!!) - } - - override fun onError(e: Throwable) { - e.message?.let { XToastUtils.error(it) } - } - - override fun onComplete() {} - }) - } - }) - return titleBar - } - - /** - * 初始化控件 - */ - override fun initViews() { - val virtualLayoutManager = VirtualLayoutManager(requireContext()) - binding!!.recyclerView.layoutManager = virtualLayoutManager - val viewPool = RecycledViewPool() - binding!!.recyclerView.setRecycledViewPool(viewPool) - viewPool.setMaxRecycledViews(0, 10) - - binding!!.recyclerView.adapter = adapter - } - - override fun initListeners() { - //下拉刷新 - binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout -> - //adapter.refresh() - lifecycleScope.launch { - viewModel.allFrpc.collectLatest { adapter.submitData(it) } - } - refreshLayout.finishRefresh() - } - binding!!.refreshLayout.autoRefresh() - - //更新时间 - LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG, Frpc::class.java).observe(this) { - adapter.refresh() - } - - //删除事件 - LiveEventBus.get(EVENT_FRPC_DELETE_CONFIG, Frpc::class.java).observe(this) { - adapter.refresh() - } - - //运行出错时间 - LiveEventBus.get(EVENT_FRPC_RUNNING_ERROR, String::class.java).observe(this) { - XToastUtils.error(getString(R.string.frpc_failed_to_run)) - //FrpcUtils.checkAndStopService(requireContext()) - adapter.refresh() - } - - //运行成功 - LiveEventBus.get(EVENT_FRPC_RUNNING_SUCCESS, String::class.java).observe(this) { - adapter.refresh() - } - } - - override fun onItemClicked(view: View?, item: Frpc) { - val id = view?.id - if (id == R.id.iv_play) { - //if (!FrpcUtils.isServiceRunning(ForegroundService::class.java.name, requireContext())) { - if (!ForegroundService.isRunning) { - val intent = Intent(requireContext(), ForegroundService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - requireContext().startForegroundService(intent) - } else { - requireContext().startService(intent) - } - } - - if (Frpclib.isRunning(item.uid)) { - Frpclib.close(item.uid) - item.setConnecting(false) - LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG).post(item) - //FrpcUtils.checkAndStopService(requireContext()) - return - } - - FrpcUtils.waitService(ForegroundService::class.java.name, requireContext()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : CompletableObserver { - var mLoadingDialog: LoadingDialog = WidgetUtils.getLoadingDialog(context!!).setIconScale(0.4f).setLoadingSpeed(8) - - override fun onSubscribe(d: Disposable) { - mLoadingDialog.setLoadingIcon(R.drawable.ic_menu_frpc) - mLoadingDialog.updateMessage(R.string.tipWaitService) - mLoadingDialog.show() - } - - override fun onComplete() { - mLoadingDialog.dismiss() - mLoadingDialog.recycle() - LiveEventBus.get(INTENT_FRPC_APPLY_FILE).postAcrossProcess(item.uid) - item.setConnecting(true) - LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG).post(item) - } - - override fun onError(e: Throwable) { - mLoadingDialog.dismiss() - mLoadingDialog.recycle() - e.message?.let { XToastUtils.error(it) } - item.setConnecting(false) - LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG).post(item) - } - }) - } else { - //编辑或删除需要先停止客户端 - if (Frpclib.isRunning(item.uid)) { - XToastUtils.warning(R.string.tipServiceRunning) - return - } - if (id == R.id.iv_edit) { - LiveEventBus.get(INTENT_FRPC_EDIT_FILE).post(item) - openNewPage(FrpcEditFragment::class.java) - } else if (id == R.id.iv_delete) { - try { - viewModel.delete(item) - LiveEventBus.get(EVENT_FRPC_DELETE_CONFIG).post(item) - XToastUtils.success(getString(R.string.successfully_deleted)) - } catch (e: Exception) { - e.message?.let { XToastUtils.error(it) } - } - } - } - } - - override fun onItemRemove(view: View?, id: Int) {} +package com.idormy.sms.forwarder.fragment + +import android.content.Intent +import android.os.Build +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView.RecycledViewPool +import com.alibaba.android.vlayout.VirtualLayoutManager +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.adapter.FrpcPagingAdapter +import com.idormy.sms.forwarder.core.BaseFragment +import com.idormy.sms.forwarder.database.entity.Frpc +import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory +import com.idormy.sms.forwarder.database.viewmodel.FrpcViewModel +import com.idormy.sms.forwarder.databinding.FragmentFrpcsBinding +import com.idormy.sms.forwarder.service.ForegroundService +import com.idormy.sms.forwarder.utils.* +import com.jeremyliao.liveeventbus.LiveEventBus +import com.scwang.smartrefresh.layout.api.RefreshLayout +import com.xuexiang.xaop.annotation.SingleClick +import com.xuexiang.xpage.annotation.Page +import com.xuexiang.xpage.base.XPageActivity +import com.xuexiang.xpage.core.PageOption +import com.xuexiang.xui.utils.ThemeUtils +import com.xuexiang.xui.utils.WidgetUtils +import com.xuexiang.xui.widget.actionbar.TitleBar +import com.xuexiang.xui.widget.dialog.LoadingDialog +import com.xuexiang.xutil.system.ClipboardUtils +import frpclib.Frpclib +import io.reactivex.CompletableObserver +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + + +@Suppress("DEPRECATION") +@Page(name = "Frp内网穿透") +class FrpcFragment : BaseFragment(), FrpcPagingAdapter.OnItemClickListener { + + var titleBar: TitleBar? = null + private var adapter = FrpcPagingAdapter(this) + private val viewModel by viewModels { BaseViewModelFactory(context) } + + override fun viewBindingInflate( + inflater: LayoutInflater, + container: ViewGroup, + ): FragmentFrpcsBinding { + return FragmentFrpcsBinding.inflate(inflater, container, false) + } + + override fun initTitle(): TitleBar? { + titleBar = super.initTitle()!!.setImmersive(false) + titleBar!!.setTitle(R.string.menu_frpc) + titleBar!!.setActionTextColor(ThemeUtils.resolveColor(context, R.attr.colorAccent)) + titleBar!!.addAction(object : TitleBar.ImageAction(R.drawable.ic_add) { + @SingleClick + override fun performAction(view: View) { + FrpcUtils.getStringFromRaw(context!!, R.raw.frpc).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(content: String) { + LiveEventBus.get(INTENT_FRPC_EDIT_FILE).post(Frpc(content)) + PageOption.to(FrpcEditFragment::class.java).setNewActivity(true).open((context as XPageActivity?)!!) + } + + override fun onError(e: Throwable) { + e.message?.let { XToastUtils.error(it) } + } + + override fun onComplete() {} + }) + } + }) + return titleBar + } + + /** + * 初始化控件 + */ + override fun initViews() { + val virtualLayoutManager = VirtualLayoutManager(requireContext()) + binding!!.recyclerView.layoutManager = virtualLayoutManager + val viewPool = RecycledViewPool() + binding!!.recyclerView.setRecycledViewPool(viewPool) + viewPool.setMaxRecycledViews(0, 10) + + binding!!.recyclerView.adapter = adapter + } + + override fun initListeners() { + //下拉刷新 + binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout -> + //adapter.refresh() + lifecycleScope.launch { + viewModel.allFrpc.collectLatest { adapter.submitData(it) } + } + refreshLayout.finishRefresh() + } + binding!!.refreshLayout.autoRefresh() + + //更新时间 + LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG, Frpc::class.java).observe(this) { + adapter.refresh() + } + + //删除事件 + LiveEventBus.get(EVENT_FRPC_DELETE_CONFIG, Frpc::class.java).observe(this) { + adapter.refresh() + } + + //运行出错时间 + LiveEventBus.get(EVENT_FRPC_RUNNING_ERROR, String::class.java).observe(this) { + XToastUtils.error(getString(R.string.frpc_failed_to_run)) + adapter.refresh() + } + + //运行成功 + LiveEventBus.get(EVENT_FRPC_RUNNING_SUCCESS, String::class.java).observe(this) { + adapter.refresh() + } + } + + override fun onItemClicked(view: View?, item: Frpc) { + when (val id = view?.id) { + R.id.iv_copy -> { + ClipboardUtils.copyText(item.uid) + XToastUtils.info(String.format(getString(R.string.copied_to_clipboard), item.uid)) + } + R.id.iv_play -> { + if (!ForegroundService.isRunning) { + val intent = Intent(requireContext(), ForegroundService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requireContext().startForegroundService(intent) + } else { + requireContext().startService(intent) + } + } + + if (Frpclib.isRunning(item.uid)) { + Frpclib.close(item.uid) + item.setConnecting(false) + LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG).post(item) + return + } + + FrpcUtils.waitService(ForegroundService::class.java.name, requireContext()).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : CompletableObserver { + var mLoadingDialog: LoadingDialog = WidgetUtils.getLoadingDialog(context!!).setIconScale(0.4f).setLoadingSpeed(8) + + override fun onSubscribe(d: Disposable) { + mLoadingDialog.setLoadingIcon(R.drawable.ic_menu_frpc) + mLoadingDialog.updateMessage(R.string.tipWaitService) + mLoadingDialog.show() + } + + override fun onComplete() { + mLoadingDialog.dismiss() + mLoadingDialog.recycle() + LiveEventBus.get(INTENT_FRPC_APPLY_FILE).postAcrossProcess(item.uid) + item.setConnecting(true) + LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG).post(item) + } + + override fun onError(e: Throwable) { + mLoadingDialog.dismiss() + mLoadingDialog.recycle() + e.message?.let { XToastUtils.error(it) } + item.setConnecting(false) + LiveEventBus.get(EVENT_FRPC_UPDATE_CONFIG).post(item) + } + }) + } + else -> { + //编辑或删除需要先停止客户端 + if (Frpclib.isRunning(item.uid)) { + XToastUtils.warning(R.string.tipServiceRunning) + return + } + when (id) { + R.id.iv_edit -> { + LiveEventBus.get(INTENT_FRPC_EDIT_FILE).post(item) + openNewPage(FrpcEditFragment::class.java) + } + R.id.iv_delete -> { + try { + viewModel.delete(item) + LiveEventBus.get(EVENT_FRPC_DELETE_CONFIG).post(item) + XToastUtils.success(getString(R.string.successfully_deleted)) + } catch (e: Exception) { + e.message?.let { XToastUtils.error(it) } + } + } + } + } + } + } + + override fun onItemRemove(view: View?, id: Int) {} } \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt index 9b451b64..7565a4b7 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt @@ -97,6 +97,10 @@ class SettingsFragment : BaseFragment(), View.OnClickL switchEnableAppNotify( binding!!.sbEnableAppNotify, binding!!.scbCancelAppNotify, binding!!.scbNotUserPresent ) + + //短信指令 + switchEnableSmsCommand(binding!!.sbEnableSmsCommand, binding!!.etSafePhone) + //设置自动消除额外APP通知 editExtraAppList(binding!!.etAppList) //启动时异步获取已安装App信息 @@ -440,12 +444,12 @@ class SettingsFragment : BaseFragment(), View.OnClickL val layoutOptionalAction: LinearLayout = binding!!.layoutOptionalAction layoutOptionalAction.visibility = if (isEnable) View.VISIBLE else View.GONE - val layoutAppList: LinearLayout = binding!!.layoutAppList - layoutAppList.visibility = if (isEnable) View.VISIBLE else View.GONE + //val layoutAppList: LinearLayout = binding!!.layoutAppList + //layoutAppList.visibility = if (isEnable) View.VISIBLE else View.GONE sbEnableAppNotify.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> layoutOptionalAction.visibility = if (isChecked) View.VISIBLE else View.GONE - layoutAppList.visibility = if (isChecked) View.VISIBLE else View.GONE + //layoutAppList.visibility = if (isChecked) View.VISIBLE else View.GONE SettingUtils.enableAppNotify = isChecked if (isChecked) { //检查权限是否获取 @@ -473,6 +477,57 @@ class SettingsFragment : BaseFragment(), View.OnClickL } } + //接受短信指令 + @SuppressLint("UseSwitchCompatOrMaterialCode") + fun switchEnableSmsCommand(sbEnableSmsCommand: SwitchButton, etSafePhone: EditText) { + sbEnableSmsCommand.isChecked = SettingUtils.enableSmsCommand + etSafePhone.visibility = if (SettingUtils.enableSmsCommand) View.VISIBLE else View.GONE + + sbEnableSmsCommand.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + SettingUtils.enableSmsCommand = isChecked + etSafePhone.visibility = if (isChecked) View.VISIBLE else View.GONE + if (isChecked) { + //检查权限是否获取 + XXPermissions.with(this) + // 接收短信 + .permission(Permission.RECEIVE_SMS) + // 发送短信 + //.permission(Permission.SEND_SMS) + // 读取短信 + .permission(Permission.READ_SMS).request(object : OnPermissionCallback { + override fun onGranted(permissions: List, all: Boolean) { + if (all) { + XToastUtils.info(R.string.toast_granted_all) + } else { + XToastUtils.info(R.string.toast_granted_part) + } + } + + override fun onDenied(permissions: List, never: Boolean) { + if (never) { + XToastUtils.info(R.string.toast_denied_never) + // 如果是被永久拒绝就跳转到应用权限系统设置页面 + XXPermissions.startPermissionActivity(requireContext(), permissions) + } else { + XToastUtils.info(R.string.toast_denied) + } + SettingUtils.enableSmsCommand = false + sbEnableSmsCommand.isChecked = false + } + }) + } + } + + etSafePhone.setText(SettingUtils.smsCommandSafePhone) + etSafePhone.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + SettingUtils.smsCommandSafePhone = etSafePhone.text.toString().trim().removeSuffix("\n") + } + }) + } + //设置自动消除额外APP通知 private fun editExtraAppList(textAppList: EditText) { textAppList.setText(SettingUtils.cancelExtraAppNotify) diff --git a/app/src/main/java/com/idormy/sms/forwarder/receiver/SmsReceiver.kt b/app/src/main/java/com/idormy/sms/forwarder/receiver/SmsReceiver.kt index 7bb80b73..2f30521f 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/receiver/SmsReceiver.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/receiver/SmsReceiver.kt @@ -10,15 +10,25 @@ import androidx.work.WorkManager import androidx.work.workDataOf import com.google.gson.Gson import com.idormy.sms.forwarder.App +import com.idormy.sms.forwarder.database.AppDatabase import com.idormy.sms.forwarder.entity.MsgInfo +import com.idormy.sms.forwarder.service.HttpService import com.idormy.sms.forwarder.utils.PhoneUtils import com.idormy.sms.forwarder.utils.SettingUtils import com.idormy.sms.forwarder.utils.Worker import com.idormy.sms.forwarder.workers.SendWorker +import com.xuexiang.xrouter.utils.TextUtils +import com.xuexiang.xutil.file.FileUtils +import frpclib.Frpclib +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async import java.util.* //短信广播 -@Suppress("PrivatePropertyName", "DEPRECATION") +@OptIn(DelicateCoroutinesApi::class) +@Suppress("PrivatePropertyName", "DEPRECATION", "DeferredResultUnused", "SENSELESS_COMPARISON") class SmsReceiver : BroadcastReceiver() { private var TAG = "SmsReceiver" @@ -28,20 +38,25 @@ class SmsReceiver : BroadcastReceiver() { //纯客户端模式 if (SettingUtils.enablePureClientMode) return - //总开关 - if (!SettingUtils.enableSms) return - //过滤广播 if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION && intent.action != Telephony.Sms.Intents.SMS_DELIVER_ACTION) return var from = "" - var content = "" + var message = "" for (smsMessage in Telephony.Sms.Intents.getMessagesFromIntent(intent)) { from = smsMessage.displayOriginatingAddress - content += smsMessage.messageBody + message += smsMessage.messageBody } - Log.d(TAG, "from = $from") - Log.d(TAG, "content = $content") + Log.d(TAG, "from = $from, message = $message") + + //短信指令 + if (SettingUtils.enableSmsCommand && message.startsWith("smsf#")) { + doSmsCommand(context, from, message) + return + } + + //总开关 + if (!SettingUtils.enableSms) return //TODO:准确获取卡槽信息,目前测试结果只有 subscription 相对靠谱 val slot = intent.extras?.getInt("slot") ?: -1 @@ -78,7 +93,7 @@ class SmsReceiver : BroadcastReceiver() { else -> "" } - val msgInfo = MsgInfo("sms", from, content, Date(), simInfo, simSlot, subscription) + val msgInfo = MsgInfo("sms", from, message, Date(), simInfo, simSlot, subscription) Log.d(TAG, "msgInfo = $msgInfo") val request = OneTimeWorkRequestBuilder().setInputData( @@ -93,4 +108,97 @@ class SmsReceiver : BroadcastReceiver() { } } + //处理短信指令 + private fun doSmsCommand(context: Context, from: String, message: String) { + var safePhone = SettingUtils.smsCommandSafePhone + Log.d(TAG, "safePhone = $safePhone") + + if (!TextUtils.isEmpty(safePhone)) { + var isSafePhone = false + safePhone = safePhone.replace(";", ",").replace(";", ",").replace(",", ",").trim() + for (phone in safePhone.split(",")) { + if (!TextUtils.isEmpty(phone.trim()) && from.endsWith(phone.trim())) { + isSafePhone = true + break + } + } + if (!isSafePhone) { + Log.d(TAG, "from = $from is not safePhone = $safePhone") + return + } + } + + val smsCommand = message.substring(5) + val cmdList = smsCommand.split("#") + Log.d(TAG, "smsCommand = $smsCommand, cmdList = $cmdList") + if (cmdList.count() < 2) return + + val function = cmdList[0] + val action = cmdList[1] + val param = if (cmdList.count() > 2) cmdList[2] else "" + when (function) { + "frpc" -> { + if (!FileUtils.isFileExists(context.filesDir?.absolutePath + "/libs/libgojni.so")) { + Log.d(TAG, "还未下载Frpc库") + return + } + + if (TextUtils.isEmpty(param)) { + GlobalScope.async(Dispatchers.IO) { + val frpcList = AppDatabase.getInstance(App.context).frpcDao().getAutorun() + + if (frpcList.isEmpty()) { + Log.d(TAG, "没有自启动的Frpc") + return@async + } + + for (frpc in frpcList) { + if (action == "start") { + if (!Frpclib.isRunning(frpc.uid)) { + val error = Frpclib.runContent(frpc.uid, frpc.config) + if (!TextUtils.isEmpty(error)) { + Log.e(TAG, error) + } + } + } else if (action == "stop") { + if (Frpclib.isRunning(frpc.uid)) { + Frpclib.close(frpc.uid) + } + } + } + } + } else { + GlobalScope.async(Dispatchers.IO) { + val frpc = AppDatabase.getInstance(App.context).frpcDao().getOne(param) + + if (frpc == null) { + Log.d(TAG, "没有找到指定的Frpc") + return@async + } + + if (action == "start") { + if (!Frpclib.isRunning(frpc.uid)) { + val error = Frpclib.runContent(frpc.uid, frpc.config) + if (!TextUtils.isEmpty(error)) { + Log.e(TAG, error) + } + } + } else if (action == "stop") { + if (Frpclib.isRunning(frpc.uid)) { + Frpclib.close(frpc.uid) + } + } + } + } + } + "httpserver" -> { + if (action == "start") { + context.startService(Intent(context, HttpService::class.java)) + } else if (action == "stop") { + context.stopService(Intent(context, HttpService::class.java)) + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/service/NotifyService.kt b/app/src/main/java/com/idormy/sms/forwarder/service/NotifyService.kt index b28eb50a..d4dfbf1f 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/service/NotifyService.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/service/NotifyService.kt @@ -48,9 +48,6 @@ class NotifyService : NotificationListenerService() { //纯客户端模式 if (SettingUtils.enablePureClientMode) return - //总开关 - if (!SettingUtils.enableAppNotify) return - //异常通知跳过 if (sbn!!.notification == null) return if (sbn.notification.extras == null) return @@ -70,6 +67,9 @@ class NotifyService : NotificationListenerService() { } } + //总开关 + if (!SettingUtils.enableAppNotify) return + //仅锁屏状态转发APP通知 if (SettingUtils.enableNotUserPresent && !ScreenUtils.isScreenLock()) return diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt index c9238c1c..f0ad5978 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt @@ -41,6 +41,9 @@ const val SP_ENABLE_CANCEL_APP_NOTIFY = "enable_cancel_app_notify" const val SP_CANCEL_EXTRA_APP_NOTIFY = "cancel_extra_app_notify_list" const val SP_ENABLE_NOT_USER_PRESENT = "enable_not_user_present" +const val SP_ENABLE_SMS_COMMAND = "enable_sms_command" +const val SP_SMS_COMMAND_SAFE_PHONE = "sms_command_safe_phone" + const val ENABLE_LOAD_APP_LIST = "enable_load_app_list" const val ENABLE_LOAD_USER_APP_LIST = "enable_load_user_app_list" const val ENABLE_LOAD_SYSTEM_APP_LIST = "enable_load_system_app_list" diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt index 70faa39d..e11cd706 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt @@ -180,6 +180,8 @@ class HttpServerUtils private constructor() { cloneInfo.smsTemplate = SettingUtils.smsTemplate cloneInfo.enableHelpTip = SettingUtils.enableHelpTip cloneInfo.enablePureClientMode = SettingUtils.enablePureClientMode + cloneInfo.enableSmsCommand = SettingUtils.enableSmsCommand + cloneInfo.smsCommandSafePhone = SettingUtils.smsCommandSafePhone cloneInfo.senderList = Core.sender.all cloneInfo.ruleList = Core.rule.all cloneInfo.frpcList = Core.frpc.all @@ -227,6 +229,8 @@ class HttpServerUtils private constructor() { SettingUtils.smsTemplate = cloneInfo.smsTemplate.toString() SettingUtils.enableHelpTip = cloneInfo.enableHelpTip SettingUtils.enablePureClientMode = cloneInfo.enablePureClientMode + SettingUtils.enableSmsCommand = cloneInfo.enableSmsCommand + SettingUtils.smsCommandSafePhone = cloneInfo.smsCommandSafePhone.toString() //删除发送通道、转发规则、转发日志 Core.sender.deleteAll() //发送通道 diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt index 05c930e3..d9d4f441 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt @@ -39,6 +39,10 @@ class SettingUtils private constructor() { //是否转发应用通知 var enableAppNotify: Boolean by SharedPreference(SP_ENABLE_APP_NOTIFY, false) + //是否接受短信指令 + var enableSmsCommand: Boolean by SharedPreference(SP_ENABLE_SMS_COMMAND, false) + var smsCommandSafePhone: String by SharedPreference(SP_SMS_COMMAND_SAFE_PHONE, "") + //是否转发应用通知——自动消除通知 var enableCancelAppNotify: Boolean by SharedPreference(SP_ENABLE_CANCEL_APP_NOTIFY, false) diff --git a/app/src/main/res/layout/adapter_frpcs_card_view_list_item.xml b/app/src/main/res/layout/adapter_frpcs_card_view_list_item.xml index d6e486ea..6624477d 100644 --- a/app/src/main/res/layout/adapter_frpcs_card_view_list_item.xml +++ b/app/src/main/res/layout/adapter_frpcs_card_view_list_item.xml @@ -1,80 +1,124 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index dc0b5b46..da96f8d7 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -8,6 +8,7 @@ android:orientation="vertical" tools:ignore="TooManyViews"> + + android:orientation="horizontal"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:orientation="vertical"> + android:text="@string/forward_app_notify" + android:textStyle="bold" + tools:ignore="RelativeOverlap" /> - + android:text="@string/forward_app_notify_tips" + android:textSize="9sp" + tools:ignore="SmallSp" /> @@ -361,23 +276,115 @@ + android:text="@string/optional_action" + android:textSize="10sp" + android:textStyle="bold" + tools:ignore="SmallSp" /> - + + + + + app:scb_color_checked="@color/colorPrimary" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0=disabled, judge duplicate: type+source+content Forward Sms Main switch, requires permissions to read and sned SMS messages, especially verification SMS texts. + Sms Command + Open the HttpServer or FRPC by the SMS command + Safe Phone + Only handle requests from specified phones Forward Calls Log Main switch, requires permissions to read call log and contacts. Forward App Notify @@ -524,6 +528,8 @@ TODO Forwarding Function Main switch: Enable the forwarding function as required + Extra Function + Enable the extra function as required Call date: Call duration: Ring duration: diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f49ca825..5e6aac61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -354,6 +354,10 @@ 0=禁用,判断重复:类型+来源+内容 转发短信广播 请授予读取短信、通知类短信、发送短信等权限,关闭验证码保护 + 短信指令 + 根据短信指令开关对应功能,指令格式:smsf#功能名#动作名 + 安全手机 + 仅处理指定手机请求,多个手机以逗号分隔 转发通话记录 请授予读取通话记录、联系人等权限,并选择转发类型,再开启 转发应用通知 @@ -525,6 +529,8 @@ TODO 转发功能 总开关,请根据实际需要,启用对应的转发功能 + 增强功能 + 请根据实际需要,启用对应的增强设置 通话时间: 通话时长: 响铃时长: @@ -544,11 +550,11 @@ 被动接收本地 HttpServer WiFi网络下可用,启动后局域网内其他机器可直接调用本机接口 网络状态监控 - 【注意】需要手动创建APP转发规则,包名:77777777 + 需要手动创建APP转发规则,包名:77777777 网络状态改变提醒 网络状态改变(连接方式/IP变化)时发出通知 电池监控 - 【注意】需要手动创建APP转发规则,包名:88888888 + 需要手动创建APP转发规则,包名:88888888 保活措施 建议开启前三项授权或设置,不要禁用通知栏,避免APP被杀 个性设置