diff --git a/.github/workflows/tencent_official.yml b/.github/workflows/tencent_official.yml index 69e7104b0..2047460da 100644 --- a/.github/workflows/tencent_official.yml +++ b/.github/workflows/tencent_official.yml @@ -50,6 +50,7 @@ jobs: sed -i 's#def versionAppName.*#def versionAppName = \"'$rt'\"#g' config.gradle sed -i 's#def sdkVersion.*#def sdkVersion = \"'$rt'\"#g' config.gradle sed -i "s#xxx#$rb+git.$rc#g" config.gradle + sed -i '/snapshots/, +d' build.gradle - name: Common-->Update Values of Signing run: | diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f38287d3a..37c6dd446 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,6 +78,7 @@ + @@ -163,6 +164,7 @@ + diff --git a/app/src/main/java/com/tencent/iot/explorer/link/AppData.kt b/app/src/main/java/com/tencent/iot/explorer/link/AppData.kt index fd22692f6..61a210815 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/AppData.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/AppData.kt @@ -39,7 +39,7 @@ class AppData private constructor() { var userInfo = UserInfo() var userSetting = UserSetting() var regionId = "1" - var conutryCode = "1" // 当前的登录的国家码 + var conutryCode = "86" // 当前的登录的国家码 var bleDevice: BleDevice? = null // 用于标记将要被索引的蓝牙设备 var region = "ap-guangzhou" var appLifeCircleId = "0" diff --git a/app/src/main/java/com/tencent/iot/explorer/link/customview/dialog/PermissionDialog.java b/app/src/main/java/com/tencent/iot/explorer/link/customview/dialog/PermissionDialog.java new file mode 100644 index 000000000..36dd98f02 --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/customview/dialog/PermissionDialog.java @@ -0,0 +1,74 @@ +package com.tencent.iot.explorer.link.customview.dialog; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.tencent.iot.explorer.link.R; +import com.tencent.iot.explorer.link.customview.dialog.entity.UpgradeInfo; + +public class PermissionDialog extends IosCenterStyleDialog implements View.OnClickListener { + + private TextView detail; + private TextView detailLips; + private TextView btnRefuse; + private TextView btnOK; + private ConstraintLayout outLayout; + private String detailString; + private String detailLipsString; + + public PermissionDialog(Context context, String detail, String detailLips) { + super(context, R.layout.popup_permission_layout); + this.detailString = detail; + this.detailLipsString = detailLips; + } + + @Override + public void initView() { + outLayout = view.findViewById(R.id.permission_dialog_layout); + detail = view.findViewById(R.id.tv_detail); + detailLips = view.findViewById(R.id.tv_detail_lips); + btnRefuse = view.findViewById(R.id.tv_refuse); + btnOK = view.findViewById(R.id.tv_ok); + + btnRefuse.setOnClickListener(this); + btnOK.setOnClickListener(this); + + detail.setText(detailString); + detailLips.setText(detailLipsString); + outLayout.setBackgroundColor(getContext().getResources().getColor(R.color.dialog_background)); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.tv_refuse: + dismiss(); + if (onDismisListener != null) { + onDismisListener.OnClickRefuse(); + } + break; + case R.id.tv_ok: + dismiss(); + if (onDismisListener != null) { + onDismisListener.OnClickOK(); + } + break; + default: + break; + } + } + + private volatile OnDismisListener onDismisListener; + + public interface OnDismisListener { + void OnClickRefuse(); + void OnClickOK(); + } + + public void setOnDismisListener(OnDismisListener onDismisListener) { + this.onDismisListener = onDismisListener; + } +} diff --git a/app/src/main/java/com/tencent/iot/explorer/link/customview/dialog/UserAgreeDialog.kt b/app/src/main/java/com/tencent/iot/explorer/link/customview/dialog/UserAgreeDialog.kt new file mode 100644 index 000000000..dea54306d --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/customview/dialog/UserAgreeDialog.kt @@ -0,0 +1,152 @@ +package com.tencent.iot.explorer.link.customview.dialog + +import android.content.Context +import android.content.DialogInterface +import android.os.Build +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.annotation.RequiresApi +import com.tencent.iot.explorer.link.R + +class UserAgreeDialog(context: Context) : IosCenterStyleDialog(context, R.layout.popup_user_agree_layout), View.OnClickListener, DialogInterface.OnKeyListener { + private var tip_title: TextView? = null + private var tv_tip_content: TextView? = null + private var tv_register_tip: TextView? = null + private var tv_confirm: TextView? = null + private var tv_cancel: TextView? = null + private var select_tag_layout: RelativeLayout? = null + private var iv_agreement: ImageView? = null + private var iv_agreement_status: ImageView? = null + @Volatile + private var readed = false + + private fun freshReadState() { + if (readed) { + iv_agreement?.setImageResource(R.mipmap.dev_mode_sel) + iv_agreement_status?.visibility = View.VISIBLE + } else { + iv_agreement?.setImageResource(R.mipmap.dev_mode_unsel) + iv_agreement_status?.visibility = View.GONE + } + } + + override fun initView() { + tip_title = view.findViewById(R.id.tip_title) + tv_tip_content = view.findViewById(R.id.tv_tip_content) + tv_register_tip = view.findViewById(R.id.tv_register_tip) + tv_confirm = view.findViewById(R.id.tv_confirm) + tv_cancel = view.findViewById(R.id.tv_cancel) + select_tag_layout = view.findViewById(R.id.select_tag_layout) + + tip_title?.text = context.getString(R.string.register_agree_2) + context.getString(R.string.rule_title_and) + context.getString(R.string.rule_title_private_protect) + + val agreeContentStrPrefix = context.getString(R.string.rule_content_prefix) + val partStr1 = "《${context.getString(R.string.register_agree_2)}》" + val partStr2 = context.getString(R.string.register_agree_3) + val partStr3 = "《${context.getString(R.string.register_agree_4)}》" + val agreeContentStrMiddle = context.getString(R.string.rule_content_middle) + val partStr4 = "《${context.getString(R.string.rule_content_list)}》" + val agreeContentStrSuffix = context.getString(R.string.rule_content_suffix) + var agreeContentStr = agreeContentStrPrefix + partStr1 + partStr2 + partStr3 + agreeContentStrMiddle + partStr4 + agreeContentStrSuffix + var agreeContentSpannable = SpannableStringBuilder(agreeContentStr) + agreeContentSpannable.setSpan(IndexClickableSpan(context, 1), + agreeContentStrPrefix.length, agreeContentStrPrefix.length + partStr1.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val start1 = agreeContentStrPrefix.length + partStr1.length + partStr2.length + agreeContentSpannable.setSpan(IndexClickableSpan(context, 2), + start1, start1 + partStr3.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val start2 = agreeContentStrPrefix.length + partStr1.length + partStr2.length + partStr3.length + agreeContentStrMiddle.length + agreeContentSpannable.setSpan(IndexClickableSpan(context, 3), + start2, start2 + partStr4.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + tv_tip_content?.movementMethod = LinkMovementMethod.getInstance() + tv_tip_content?.text = agreeContentSpannable + + select_tag_layout?.setOnClickListener(this) + tv_cancel?.setOnClickListener(this) + tv_confirm?.setOnClickListener(this) + this.setOnKeyListener(this) + freshReadState() + } + + override fun onClick(v: View?) { + when(v) { + select_tag_layout -> { + } + + tv_cancel -> { + onDismisListener?.onDismised() + } + + tv_confirm -> { + readed = true + freshReadState() + onDismisListener?.onOkClicked() + dismiss() + } + } + } + + @Volatile + private var onDismisListener: OnDismisListener? = null + + interface OnDismisListener { + fun onDismised() + fun onOkClicked() + fun onOkClickedUserAgreement() + fun onOkClickedPrivacyPolicy() + fun onOkClickedThirdSDKList() + } + + fun setOnDismisListener(onDismisListener: OnDismisListener?) { + this.onDismisListener = onDismisListener + } + + inner class IndexClickableSpan(context: Context, index: Int): ClickableSpan() { + private var index = 0 + private var context: Context? = null + + init { + this.index = index + this.context = context + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun onClick(widget: View) { + context?.let { + tv_tip_content?.highlightColor = it.getColor(android.R.color.transparent) + tv_register_tip?.highlightColor = it.getColor(android.R.color.transparent) + } + if (index == 1) { + onDismisListener?.onOkClickedUserAgreement() + } else if (index == 2) { + onDismisListener?.onOkClickedPrivacyPolicy() + } else if (index == 3) { + onDismisListener?.onOkClickedThirdSDKList() + } + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + context?.let { + ds.color = it.getColor(R.color.blue_0066FF) + } + ds.isUnderlineText = false + } + } + + override fun onKey(dialog: DialogInterface?, keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + onDismisListener?.onDismised() + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/AccountAndSafetyActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/AccountAndSafetyActivity.kt index 581f8c093..7db03a01a 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/AccountAndSafetyActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/AccountAndSafetyActivity.kt @@ -15,6 +15,7 @@ import com.tencent.iot.explorer.link.T import com.tencent.iot.explorer.link.core.auth.callback.MyCallback import com.tencent.iot.explorer.link.core.auth.response.BaseResponse import com.tencent.iot.explorer.link.core.utils.Utils +import com.tencent.iot.explorer.link.customview.dialog.PermissionDialog import kotlinx.android.synthetic.main.activity_account_and_safety.* import kotlinx.android.synthetic.main.menu_back_layout.* @@ -49,6 +50,7 @@ class AccountAndSafetyActivity : PActivity(), AccountAndSafetyView, View.OnClick tv_wechat.setOnClickListener(this) tv_modify_passwd.setOnClickListener(this) tv_account_logout.setOnClickListener(this) + tv_controller_of_permission.setOnClickListener(this) } override fun onClick(v: View?) { @@ -71,7 +73,18 @@ class AccountAndSafetyActivity : PActivity(), AccountAndSafetyView, View.OnClick if (App.data.userInfo.HasWxOpenID == "1") { T.show(getString(R.string.wechat_bind_already)) //微信已经绑定过了, 请勿重复绑定 } else { - WeChatLogin.getInstance().login(this, this) + var dlg = PermissionDialog(this@AccountAndSafetyActivity, getString(R.string.permission_of_wechat), getString(R.string.permission_of_wechat_lips)) + dlg.show() + dlg.setOnDismisListener(object : PermissionDialog.OnDismisListener { + override fun OnClickRefuse() { + + } + + override fun OnClickOK() { + WeChatLogin.getInstance().login(this@AccountAndSafetyActivity, this@AccountAndSafetyActivity) + } + + }) } } tv_modify_passwd -> {// 修改密码 @@ -80,6 +93,9 @@ class AccountAndSafetyActivity : PActivity(), AccountAndSafetyView, View.OnClick tv_account_logout -> {// 注销账号 jumpActivity(LogoutActivity::class.java) } + tv_controller_of_permission -> { + jumpActivity(ControlPermissionActivity::class.java) + } } } diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ChooseCountryActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ChooseCountryActivity.kt new file mode 100644 index 000000000..d1ad5e5a7 --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ChooseCountryActivity.kt @@ -0,0 +1,148 @@ +package com.tencent.iot.explorer.link.kitlink.activity + +import android.content.Intent +import android.text.TextUtils +import android.view.View +import com.alibaba.fastjson.JSONObject +import com.tencent.iot.explorer.link.R +import com.tencent.iot.explorer.link.T +import com.tencent.iot.explorer.link.core.utils.Utils +import com.tencent.iot.explorer.link.customview.dialog.InputBirthdayDialog +import com.tencent.iot.explorer.link.kitlink.consts.CommonField +import com.tencent.iot.explorer.link.mvp.IPresenter +import com.tencent.iot.explorer.link.mvp.presenter.ChooseCountryPresenter +import com.tencent.iot.explorer.link.mvp.view.ChooseCountryView +import kotlinx.android.synthetic.main.activity_choose_country.* +import kotlinx.android.synthetic.main.activity_register.* +import kotlinx.android.synthetic.main.layout_email_register.view.* +import kotlinx.android.synthetic.main.layout_phone_register.view.* +import kotlinx.android.synthetic.main.menu_back_layout.* +import java.util.* + +class ChooseCountryActivity : PActivity(), ChooseCountryView, View.OnClickListener { + + private lateinit var presenter: ChooseCountryPresenter + + override fun getPresenter(): IPresenter? { + return presenter + } + + override fun getContentView(): Int { + return R.layout.activity_choose_country + } + + override fun initView() { + tv_title.text = getString(R.string.country_or_place) + presenter = ChooseCountryPresenter(this) + loadLastCountryInfo() + showBirthDayDlg() + } + + override fun setListener() { + iv_back.setOnClickListener { finish() } + tv_register_to_country.setOnClickListener(this) + btn_bind_get_code.setOnClickListener(this) + } + + override fun onClick(v: View?) { + when (v) { + tv_register_to_country -> { + startActivityForResult(Intent(this, RegionActivity::class.java), 100) + } + btn_bind_get_code -> { + Intent(this, RegisterActivity::class.java).run { + startActivity(this) + } + } + } + } + + override fun showCountryCode(countryCode: String, countryName: String) { + tv_register_to_country.text = countryName + getString(R.string.conutry_code_num, countryCode) + } + + private fun shouldShowBirthdayDlg(): Boolean { + var lastTimeJson = Utils.getStringValueFromXml(this@ChooseCountryActivity, CommonField.USA_USER_REG_TIME_INFO, CommonField.USA_USER_REG_TIME_INFO) + // 不存在上一次的注册信息 + if (TextUtils.isEmpty(lastTimeJson) || lastTimeJson == "{}") return true + + var json = JSONObject.parseObject(lastTimeJson) + var currentDate = Date() + var currentYear = currentDate.year + 1900 + var currentMonth = currentDate.month + 1 + var currentDay = currentDate.day + var tagYear = json.getIntValue(CommonField.USA_USER_REG_TIME_INFO_YEAR) + var tagMonth = json.getIntValue(CommonField.USA_USER_REG_TIME_INFO_MONTH) + var tagDay = json.getIntValue(CommonField.USA_USER_REG_TIME_INFO_DAY) + if (currentYear - tagYear > 0 && currentMonth - tagMonth == 0 && currentDay - tagDay == 0) { // 满周年 + return true + } + + return false + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == 100) { + data?.let { + it.getStringExtra(CommonField.REGION_ID)?.run { + presenter.setCountry(this) + Utils.setXmlStringValue(T.getContext(), CommonField.REG_COUNTRY_INFO, CommonField.REG_COUNTRY_INFO, this) + showBirthDayDlg() + } + } + } + } + + private fun showBirthDayDlg() { + if (presenter.getCountryCode() == "1" && shouldShowBirthdayDlg()) { + var dlg = InputBirthdayDialog(this@ChooseCountryActivity) + dlg.show() + dlg.setOnDismissListener(object: InputBirthdayDialog.OnDismisListener { + override fun onOkClicked(year: Int, month: Int, day: Int) { + + // 是否满13周岁 + if (!ifOver13YearsOld(year, month, day)) { + T.show(resources.getString(R.string.too_young_to_use)) + finish() + return + } + + var timeJson = JSONObject() + var currentDate = Date() + var currentYear = currentDate.year + 1900 + var currentMonth = currentDate.month + 1 + var currentDay = currentDate.day + // 记录本次使用的日期 + timeJson.put(CommonField.USA_USER_REG_TIME_INFO_YEAR, currentYear) + timeJson.put(CommonField.USA_USER_REG_TIME_INFO_MONTH, currentMonth) + timeJson.put(CommonField.USA_USER_REG_TIME_INFO_DAY, currentDay) + Utils.setXmlStringValue(T.getContext(), CommonField.USA_USER_REG_TIME_INFO, + CommonField.USA_USER_REG_TIME_INFO, timeJson.toJSONString()) + } + + override fun onCancelClicked() { finish() } + }) + } + } + + private fun ifOver13YearsOld(year: Int, month: Int, day: Int): Boolean { + var currentDate = Date() + var currentYear = currentDate.year + 1900 + var currentMonth = currentDate.month + 1 + var currentDay = currentDate.day + if (currentYear - year < 13 || (currentYear - year == 13 && currentMonth - month < 0) || + (currentYear - year == 13 && currentMonth - month == 0 && currentDay - day < 0)) { + return false + } + return true + } + + private fun loadLastCountryInfo() { + var countryInfo = Utils.getStringValueFromXml(T.getContext(), CommonField.REG_COUNTRY_INFO, CommonField.REG_COUNTRY_INFO) + if (TextUtils.isEmpty(countryInfo)) return + + presenter.setCountry(countryInfo!!) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/CommentDetailsActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/CommentDetailsActivity.kt index 3974a687d..0cfc7a2bc 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/CommentDetailsActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/CommentDetailsActivity.kt @@ -119,8 +119,7 @@ class CommentDetailsActivity: BaseActivity(), View.OnClickListener, MyCallback { } private fun loadContent() { - var extraInfo = intent.getStringExtra(CommonField.EXTRA_INFO) - + val extraInfo = intent.getStringExtra(CommonField.EXTRA_INFO) ?: return var uri = Uri.parse(extraInfo) if (uri == null) return diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ControlPermissionActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ControlPermissionActivity.kt new file mode 100644 index 000000000..a2dcd5c65 --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ControlPermissionActivity.kt @@ -0,0 +1,100 @@ +package com.tencent.iot.explorer.link.kitlink.activity + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.tencent.iot.explorer.link.R +import com.tencent.iot.explorer.link.kitlink.adapter.PermissionsAdapter +import com.tencent.iot.explorer.link.kitlink.entity.PermissionAccessInfo +import kotlinx.android.synthetic.main.activity_permissions.* +import kotlinx.android.synthetic.main.activity_select_point.* +import kotlinx.android.synthetic.main.menu_back_layout.* + +class ControlPermissionActivity : BaseActivity() { + private var REQUEST_CODE = 0x1110 + private var permissionsList = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.RECEIVE_SMS, + Manifest.permission.READ_SMS, + Manifest.permission.SEND_SMS, + Manifest.permission.WRITE_SETTINGS) + + private var permissionsData: MutableList = ArrayList() + private var adapter: PermissionsAdapter? = null + + override fun getContentView(): Int { + return R.layout.activity_permissions + } + + override fun onResume() { + super.onResume() + // 刷新当前的权限列表,避免用户手动进入到后台,调整权限的情况 + if (permissionsData.isEmpty()) return + for (i in permissionsData.indices) { + permissionsData[i].permissionAccessed = + ActivityCompat.checkSelfPermission(this, permissionsData[i].permission) == PackageManager.PERMISSION_GRANTED + } + adapter?.notifyDataSetChanged() + } + + override fun initView() { + tv_title.setText(R.string.controller_of_permission) + checkPermissionsInfo(permissionsList) + + var linearLayoutManager = LinearLayoutManager(this@ControlPermissionActivity) + adapter = PermissionsAdapter(permissionsData) + lv_permissions.layoutManager = linearLayoutManager + lv_permissions.adapter = adapter + adapter?.notifyDataSetChanged() + } + + override fun setListener() { + iv_back.setOnClickListener { finish() } + adapter?.setOnItemClicked(onItemClicked) + } + + private var onItemClicked = object: PermissionsAdapter.OnItemActionListener { + override fun onItemSwitched(pos: Int, permissionAccessInfo: PermissionAccessInfo) { + if (!permissionAccessInfo.permissionAccessed) { // 没有对应的权限,尝试开启权限 + ActivityCompat.requestPermissions(this@ControlPermissionActivity, arrayOf(permissionAccessInfo.permission), REQUEST_CODE) + return + } + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.data = Uri.parse("package:$packageName") + startActivityForResult(intent, REQUEST_CODE) // 申请权限返回执行 + } + } + + private fun checkPermissionsInfo(permissions: Array) { + val pm = this@ControlPermissionActivity.packageManager + for (permission in permissions) { + var accessInfo = PermissionAccessInfo() + if (ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + accessInfo.permissionAccessed = true + } + + try { + var permissionInfo = pm.getPermissionInfo(permission, 0) + accessInfo.permissionName = permissionInfo.loadLabel(pm).toString() + accessInfo.permission = permission + } catch (e: Exception) { + e.printStackTrace() + } + permissionsData.add(accessInfo) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/DeviceCategoryActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/DeviceCategoryActivity.kt index 873998049..eee1432f2 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/DeviceCategoryActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/DeviceCategoryActivity.kt @@ -38,6 +38,7 @@ import com.tencent.iot.explorer.link.core.link.service.BleConfigService import com.tencent.iot.explorer.link.core.log.L import com.tencent.iot.explorer.link.core.utils.Utils import com.tencent.iot.explorer.link.customview.MyScrollView +import com.tencent.iot.explorer.link.customview.dialog.PermissionDialog import com.tencent.iot.explorer.link.customview.recyclerview.CRecyclerView import com.tencent.iot.explorer.link.customview.verticaltab.* import com.tencent.iot.explorer.link.kitlink.adapter.BleDeviceAdapter @@ -317,7 +318,18 @@ class DeviceCategoryActivity : PActivity(), MyCallback, CRecyclerView.RecyclerI intent.putExtra(Constant.EXTRA_IS_ENABLE_SCAN_FROM_PIC,true) startActivityForResult(intent, CommonField.QR_CODE_REQUEST_CODE) } else { - requestPermission(permissions) + var dlg = PermissionDialog(this@DeviceCategoryActivity, getString(R.string.permission_of_wifi), getString(R.string.permission_of_wifi_lips)) + dlg.show() + dlg.setOnDismisListener(object : PermissionDialog.OnDismisListener { + override fun OnClickRefuse() { + + } + + override fun OnClickOK() { + requestPermission(permissions) + } + + }) } } // iv_question -> { @@ -366,7 +378,7 @@ class DeviceCategoryActivity : PActivity(), MyCallback, CRecyclerView.RecyclerI productsList.add(productid!!) if (contains("preview=1")) { var intent = Intent(this@DeviceCategoryActivity, ProductIntroduceActivity::class.java) - intent.putExtra(CommonField.EXTRA_INFO, productid) + intent.putExtra(CommonField.PRODUCT_ID, productid) startActivity(intent) } else { HttpRequest.instance.getProductsConfig(productsList, patchProductListener) @@ -408,7 +420,7 @@ class DeviceCategoryActivity : PActivity(), MyCallback, CRecyclerView.RecyclerI if (config != null && !TextUtils.isEmpty(config.Global) && ProductGlobal.isProductGlobalLegal(config.Global)) { var intent = Intent(this@DeviceCategoryActivity, ProductIntroduceActivity::class.java) - intent.putExtra(CommonField.EXTRA_INFO, productId) + intent.putExtra(CommonField.PRODUCT_ID, productId) startActivity(intent) return@run } @@ -473,7 +485,18 @@ class DeviceCategoryActivity : PActivity(), MyCallback, CRecyclerView.RecyclerI private fun beginScanning() { if (!checkPermissions(blueToothPermissions)) { - requestPermission(blueToothPermissions) + var dlg = PermissionDialog(this@DeviceCategoryActivity, getString(R.string.permission_of_wifi), getString(R.string.permission_of_wifi_lips)) + dlg.show() + dlg.setOnDismisListener(object : PermissionDialog.OnDismisListener { + override fun OnClickRefuse() { + + } + + override fun OnClickOK() { + requestPermission(blueToothPermissions) + } + + }) return } diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/GuideActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/GuideActivity.kt index 8721f5591..8376cc80d 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/GuideActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/GuideActivity.kt @@ -23,12 +23,6 @@ import kotlinx.android.synthetic.main.activity_guide.* class GuideActivity : PActivity(), View.OnClickListener{ - private val permissions = arrayOf( - Manifest.permission.RECEIVE_SMS, - Manifest.permission.READ_SMS, - Manifest.permission.SEND_SMS - ) - private val counts = 5 //点击次数 private val duration = 3 * 1000.toLong() //规定有效时间 private val hits = LongArray(counts) @@ -43,11 +37,6 @@ class GuideActivity : PActivity(), View.OnClickListener{ } override fun initView() { - if (!checkPermissions(permissions)) { - requestPermission(permissions) - } else { - permissionAllGranted() - } if (!TextUtils.isEmpty(App.data.getToken())) { startActivity(Intent(this, MainActivity::class.java)) return @@ -73,7 +62,7 @@ class GuideActivity : PActivity(), View.OnClickListener{ override fun onClick(v: View?) { when (v) { btn_create_new_account -> { - Intent(this, RegisterActivity::class.java).run { + Intent(this, ChooseCountryActivity::class.java).run { startActivity(this) } } diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/MainActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/MainActivity.kt index 02bb45cc4..297df1881 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/MainActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/MainActivity.kt @@ -152,7 +152,20 @@ class MainActivity : PActivity(), MyCallback { FirebaseAnalytics.getInstance(this).setUserId(userId) openXGPush() home_bottom_view.addUnclickAbleItem(2) // 限定2号位置不可选中 - requestPermission(permissions) + if (!checkPermissions(permissions)) { + var dlg = PermissionDialog(this@MainActivity, getString(R.string.permission_of_mic_camera), getString(R.string.permission_of_mic_camera_lips)) + dlg.show() + dlg.setOnDismisListener(object : PermissionDialog.OnDismisListener { + override fun OnClickRefuse() { + + } + + override fun OnClickOK() { + requestPermission(permissions) + } + + }) + } LogcatHelper.getInstance(this).start() home_bottom_view.addMenu( BottomItemEntity( @@ -511,7 +524,7 @@ class MainActivity : PActivity(), MyCallback { if (config != null && !TextUtils.isEmpty(config.Global) && ProductGlobal.isProductGlobalLegal(config.Global)) { var intent = Intent(this@MainActivity, ProductIntroduceActivity::class.java) - intent.putExtra(CommonField.EXTRA_INFO, productId) + intent.putExtra(CommonField.PRODUCT_ID, productId) startActivity(intent) return@run } @@ -570,7 +583,7 @@ class MainActivity : PActivity(), MyCallback { productsList.add(productid!!) if (contains("preview=1")) { var intent = Intent(this@MainActivity, ProductIntroduceActivity::class.java) - intent.putExtra(CommonField.EXTRA_INFO, productid) + intent.putExtra(CommonField.PRODUCT_ID, productid) startActivity(intent) } else { HttpRequest.instance.getProductsConfig(productsList, patchProductListener) diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/OpensourceLicenseActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/OpensourceLicenseActivity.kt index 7b9305a73..7bc2465ef 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/OpensourceLicenseActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/OpensourceLicenseActivity.kt @@ -30,7 +30,7 @@ class OpensourceLicenseActivity : BaseActivity(), MyCustomCallBack { var urlSet = Arrays.asList(CommonField.OPENSOURCE_LICENSE_URL_EN, CommonField.OPENSOURCE_LICENSE_URL_ZH, CommonField.PRIVACY_POLICY_URL_CN_EN, CommonField.PRIVACY_POLICY_URL_US_ZH, CommonField.PRIVACY_POLICY_URL_US_EN, CommonField.SERVICE_AGREEMENT_URL_CN_EN, CommonField.SERVICE_AGREEMENT_URL_US_ZH, CommonField.SERVICE_AGREEMENT_URL_US_EN, - CommonField.DELET_ACCOUNT_POLICY_EN) + CommonField.DELET_ACCOUNT_POLICY_EN, CommonField.THIRD_SDK_URL_US_ZH, CommonField.THIRD_SDK_URL_US_EN) fun startWebWithExtra(context: Context, title: String, url: String) { val intent = Intent(context, OpensourceLicenseActivity::class.java) diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ProductIntroduceActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ProductIntroduceActivity.kt index 3b9366240..25a2db8eb 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ProductIntroduceActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/ProductIntroduceActivity.kt @@ -26,8 +26,10 @@ class ProductIntroduceActivity : BaseActivity(), MyCallback { override fun initView() { tv_title.setText(getString(R.string.bind_dev)) - productId = intent.getStringExtra(CommonField.EXTRA_INFO) - HttpRequest.instance.getProductsConfig(arrayListOf(productId), this) + if (intent.hasExtra(CommonField.PRODUCT_ID)) { + productId = intent.getStringExtra(CommonField.PRODUCT_ID) + HttpRequest.instance.getProductsConfig(arrayListOf(productId), this) + } } private fun loadRemoteRes(productGlobalStr: String) { diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/RegisterActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/RegisterActivity.kt index 6c2d0ea00..79074b182 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/RegisterActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/RegisterActivity.kt @@ -1,5 +1,6 @@ package com.tencent.iot.explorer.link.kitlink.activity +import android.Manifest import android.content.Intent import android.text.Spannable import android.text.SpannableStringBuilder @@ -16,6 +17,7 @@ import com.tencent.iot.explorer.link.T import com.tencent.iot.explorer.link.core.utils.KeyBoardUtils import com.tencent.iot.explorer.link.core.utils.Utils import com.tencent.iot.explorer.link.customview.dialog.InputBirthdayDialog +import com.tencent.iot.explorer.link.customview.dialog.UserAgreeDialog import com.tencent.iot.explorer.link.kitlink.consts.CommonField import com.tencent.iot.explorer.link.kitlink.consts.SocketConstants import com.tencent.iot.explorer.link.mvp.IPresenter @@ -32,6 +34,12 @@ import java.util.* */ class RegisterActivity : PActivity(), RegisterView, View.OnClickListener { + private val permissions = arrayOf( + Manifest.permission.RECEIVE_SMS, + Manifest.permission.READ_SMS, + Manifest.permission.SEND_SMS + ) + companion object { const val ACCOUNT_TYPE = "account_type" const val ACCOUNT_NUMBER = "account_number" @@ -88,9 +96,102 @@ class RegisterActivity : PActivity(), RegisterView, View.OnClickListener { phoneView.tv_register_to_country.text = getString(R.string.country_china) + getString(R.string.conutry_code_num, presenter.getCountryCode()) emailView.tv_register_to_country_email.text = getString(R.string.country_china) + getString(R.string.conutry_code_num, presenter.getCountryCode()) + phoneView.layout_phone_country.visibility = View.GONE + emailView.layout_email_country.visibility = View.GONE + + iv_register_agreement.visibility = View.INVISIBLE + loadLastCountryInfo() - showBirthDayDlg() - formatTipText() +// showBirthDayDlg() +// formatTipText() + checkIfShowAgreeDlg() + } + + private fun checkIfShowAgreeDlg() { + var agreed = Utils.getStringValueFromXml(this@RegisterActivity, CommonField.AGREED_RULE_FLAG, CommonField.AGREED_RULE_FLAG) + agreed?.let { + if (it == "1") { + presenter.agreement() + if (!checkPermissions(permissions)) { + requestPermission(permissions) + } else { + permissionAllGranted() + } + return@checkIfShowAgreeDlg + } + } + var dlg = UserAgreeDialog(this@RegisterActivity) + dlg.show() + dlg.setOnDismisListener(object : UserAgreeDialog.OnDismisListener { + override fun onDismised() { finish() } + override fun onOkClicked() { + presenter.agreement() + Utils.setXmlStringValue(this@RegisterActivity, CommonField.AGREED_RULE_FLAG, CommonField.AGREED_RULE_FLAG, "1") + if (!checkPermissions(permissions)) { + requestPermission(permissions) + } else { + permissionAllGranted() + } + } + + override fun onOkClickedUserAgreement() { + if (presenter.getCountryCode() == "86") { + if (Utils.getLang().contains(CommonField.ZH_TAG)) { + val intent = Intent(this@RegisterActivity, WebActivity::class.java) + intent.putExtra(CommonField.EXTRA_TITLE, getString(R.string.register_agree_2)) + var url = CommonField.POLICY_PREFIX + url += "?uin=$ANDROID_ID" + url += CommonField.SERVICE_POLICY_SUFFIX + intent.putExtra(CommonField.EXTRA_TEXT, url) + startActivity(intent) + } else { + OpensourceLicenseActivity.startWebWithExtra(this@RegisterActivity, getString(R.string.register_agree_2), CommonField.SERVICE_AGREEMENT_URL_CN_EN) + } + } else { + var url = "" + if (Utils.getLang().contains(CommonField.ZH_TAG)) { + url = CommonField.SERVICE_AGREEMENT_URL_US_ZH + } else { + url = CommonField.SERVICE_AGREEMENT_URL_US_EN + } + OpensourceLicenseActivity.startWebWithExtra(this@RegisterActivity, getString(R.string.register_agree_2), url) + } + } + + override fun onOkClickedPrivacyPolicy() { + if (presenter.getCountryCode() == "86") { + if (Utils.getLang().contains(CommonField.ZH_TAG)) { + val intent = Intent(this@RegisterActivity, WebActivity::class.java) + intent.putExtra(CommonField.EXTRA_TITLE, getString(R.string.register_agree_4)) + var url = CommonField.POLICY_PREFIX + url += "?uin=$ANDROID_ID" + url += CommonField.PRIVACY_POLICY_SUFFIX + intent.putExtra(CommonField.EXTRA_TEXT, url) + startActivity(intent) + } else { + OpensourceLicenseActivity.startWebWithExtra(this@RegisterActivity, getString(R.string.register_agree_4), CommonField.PRIVACY_POLICY_URL_CN_EN) + } + } else { + var url = "" + if (Utils.getLang().contains(CommonField.ZH_TAG)) { + url = CommonField.PRIVACY_POLICY_URL_US_ZH + } else { + url = CommonField.PRIVACY_POLICY_URL_US_EN + } + OpensourceLicenseActivity.startWebWithExtra(this@RegisterActivity, getString(R.string.register_agree_4), url) + } + } + + override fun onOkClickedThirdSDKList() { + var url = "" + if (Utils.getLang().contains(CommonField.ZH_TAG)) { + url = CommonField.THIRD_SDK_URL_US_ZH + } else { + url = CommonField.THIRD_SDK_URL_US_EN + } + OpensourceLicenseActivity.startWebWithExtra(this@RegisterActivity, getString(R.string.rule_content_list), url) + } + }) } private fun formatTipText() { @@ -301,6 +402,13 @@ class RegisterActivity : PActivity(), RegisterView, View.OnClickListener { } override fun unselectedAgreement() { + var agreed = Utils.getStringValueFromXml(this@RegisterActivity, CommonField.AGREED_RULE_FLAG, CommonField.AGREED_RULE_FLAG) + agreed?.let { + if (it == "1") { +// presenter.agreement() + return@unselectedAgreement + } + } T.show(getString(R.string.toast_register_agreement)) } @@ -324,7 +432,7 @@ class RegisterActivity : PActivity(), RegisterView, View.OnClickListener { var tagMonth = json.getIntValue(CommonField.USA_USER_REG_TIME_INFO_MONTH) var tagDay = json.getIntValue(CommonField.USA_USER_REG_TIME_INFO_DAY) if (currentYear - tagYear > 0 && currentMonth - tagMonth == 0 && currentDay - tagDay == 0) { // 满周年 - return true +// return true } return false diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/WebActivity.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/WebActivity.kt index ff97575f7..6eb35d925 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/WebActivity.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/activity/WebActivity.kt @@ -2,11 +2,11 @@ package com.tencent.iot.explorer.link.kitlink.activity import android.view.View import android.view.ViewGroup -import android.webkit.WebChromeClient -import android.webkit.WebSettings -import android.webkit.WebViewClient +import android.webkit.* import com.tencent.iot.explorer.link.R import com.tencent.iot.explorer.link.kitlink.consts.CommonField +import com.tencent.iot.explorer.link.kitlink.consts.CommonField.THIRD_SDK_URL_US_EN +import com.tencent.iot.explorer.link.kitlink.consts.CommonField.THIRD_SDK_URL_US_ZH import kotlinx.android.synthetic.main.activity_web.* import kotlinx.android.synthetic.main.menu_back_layout.* @@ -50,7 +50,20 @@ class WebActivity : BaseActivity() { wv_web.settings.useWideViewPort = true wv_web.settings.loadWithOverviewMode = true wv_web.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING - wv_web.webViewClient = WebViewClient() + val mWebViewClient = object : WebViewClient(){ + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + if (request?.url.toString().contains(THIRD_SDK_URL_US_ZH)) { + OpensourceLicenseActivity.startWebWithExtra(this@WebActivity, getString(R.string.rule_content_list), THIRD_SDK_URL_US_ZH) + return true + } else if (request?.url.toString().contains(THIRD_SDK_URL_US_EN)) { + OpensourceLicenseActivity.startWebWithExtra(this@WebActivity, getString(R.string.rule_content_list), THIRD_SDK_URL_US_EN) + return true + } else { + return super.shouldOverrideUrlLoading(view, request) + } + } + } + wv_web.webViewClient = mWebViewClient wv_web.webChromeClient = WebChromeClient() wv_web.visibility = View.VISIBLE sv_help.visibility = View.GONE diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/adapter/PermissionsAdapter.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/adapter/PermissionsAdapter.kt new file mode 100644 index 000000000..962b463c8 --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/adapter/PermissionsAdapter.kt @@ -0,0 +1,55 @@ +package com.tencent.iot.explorer.link.kitlink.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Switch +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.tencent.iot.explorer.link.R +import com.tencent.iot.explorer.link.kitlink.entity.PermissionAccessInfo + +class PermissionsAdapter(list: MutableList) : RecyclerView.Adapter() { + var list: MutableList = ArrayList() + + init { + this.list = list + } + + class ViewHolder(layoutView: View) : RecyclerView.ViewHolder(layoutView) { + var permissionName: TextView = layoutView.findViewById(R.id.tv_permission) + var permissionSwitch: Switch = layoutView.findViewById(R.id.switch_permission) + var swicth: View = layoutView.findViewById(R.id.v_real_switch) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_permission_list, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.permissionName.setText(list.get(position)?.permissionName) + holder.permissionSwitch.isClickable = false + holder.permissionSwitch.isChecked = list.get(position)?.permissionAccessed + holder.swicth.setOnClickListener { + onItemActionListener?.onItemSwitched(position, list.get(position)) + } + } + + override fun getItemCount(): Int { + if (list == null) { + return 0 + } + return list.size + } + + interface OnItemActionListener { + fun onItemSwitched(pos: Int, permissionAccessInfo: PermissionAccessInfo) + } + + private var onItemActionListener: OnItemActionListener? = null + + fun setOnItemClicked(onItemActionListener: OnItemActionListener?) { + this.onItemActionListener = onItemActionListener + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/consts/CommonField.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/consts/CommonField.kt index 057ebc67b..11c3b4c7c 100644 --- a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/consts/CommonField.kt +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/consts/CommonField.kt @@ -102,6 +102,7 @@ object CommonField { const val USA_USER_REG_TIME_INFO_DAY = "RegTimeInfoDay" const val COUNTRY_INFO = "CountryInfo" const val COUNTRY_CODE = "CountryCode" + const val AGREED_RULE_FLAG = "agreeRule" const val AGREE_TAG = "agreeTag" const val AGREED_TAG = "agreed" const val REGION_ID = "RegionID" @@ -129,6 +130,8 @@ object CommonField { const val SERVICE_AGREEMENT_URL_US_ZH = "http://qzonestyle.gtimg.cn/qzone/qzactStatics/qcloud/data/42/config7.js" const val SERVICE_AGREEMENT_URL_US_EN = "http://qzonestyle.gtimg.cn/qzone/qzactStatics/qcloud/data/42/config3.js" const val DELET_ACCOUNT_POLICY_EN = "http://qzonestyle.gtimg.cn/qzone/qzactStatics/qcloud/data/42/config6.js" + const val THIRD_SDK_URL_US_ZH = "http://qzonestyle.gtimg.cn/qzone/qzactStatics/qcloud/data/42/config12.js" + const val THIRD_SDK_URL_US_EN = "http://qzonestyle.gtimg.cn/qzone/qzactStatics/qcloud/data/42/config13.js" /************返回结果通用字段*************/ const val RESPONSE = "Response" diff --git a/app/src/main/java/com/tencent/iot/explorer/link/kitlink/entity/PermissionAccessInfo.kt b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/entity/PermissionAccessInfo.kt new file mode 100644 index 000000000..0586fda87 --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/kitlink/entity/PermissionAccessInfo.kt @@ -0,0 +1,8 @@ +package com.tencent.iot.explorer.link.kitlink.entity + +class PermissionAccessInfo { + var permissionId = 0 + var permissionName = "" + var permission = "" + var permissionAccessed = false +} \ No newline at end of file diff --git a/app/src/main/java/com/tencent/iot/explorer/link/mvp/model/ChooseCountryModel.kt b/app/src/main/java/com/tencent/iot/explorer/link/mvp/model/ChooseCountryModel.kt new file mode 100644 index 000000000..17da802e3 --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/mvp/model/ChooseCountryModel.kt @@ -0,0 +1,43 @@ +package com.tencent.iot.explorer.link.mvp.model + +import com.tencent.iot.explorer.link.App +import com.tencent.iot.explorer.link.R +import com.tencent.iot.explorer.link.kitlink.consts.SocketConstants +import com.tencent.iot.explorer.link.kitlink.util.HttpRequest +import com.tencent.iot.explorer.link.core.auth.callback.MyCallback +import com.tencent.iot.explorer.link.kitlink.util.RequestCode +import com.tencent.iot.explorer.link.mvp.ParentModel +import com.tencent.iot.explorer.link.mvp.view.RegisterView +import com.tencent.iot.explorer.link.T +import com.tencent.iot.explorer.link.core.auth.response.BaseResponse +import com.tencent.iot.explorer.link.mvp.view.ChooseCountryView + +class ChooseCountryModel(view: ChooseCountryView) : ParentModel(view), MyCallback { + + private var countryCode = "86" + private var countryName = T.getContext().getString(R.string.china_main_land) //"中国大陆" + private var regionId = "1" + private val type = SocketConstants.register + + fun setCountry(country: String) { + country.split("+").let { + this.countryName = it[0] + this.regionId = it[1] + this.countryCode = it[2] + App.data.regionId = regionId + App.data.region = it[3] + view?.showCountryCode(this.countryCode, this.countryName) + } + } + + fun getCountryCode(): String { + return countryCode + } + + override fun fail(msg: String?, reqCode: Int) { + } + + override fun success(response: BaseResponse, reqCode: Int) { + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tencent/iot/explorer/link/mvp/presenter/ChooseCountryPresenter.kt b/app/src/main/java/com/tencent/iot/explorer/link/mvp/presenter/ChooseCountryPresenter.kt new file mode 100644 index 000000000..dfa0eb17d --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/mvp/presenter/ChooseCountryPresenter.kt @@ -0,0 +1,23 @@ +package com.tencent.iot.explorer.link.mvp.presenter + +import com.tencent.iot.explorer.link.mvp.model.RegisterModel +import com.tencent.iot.explorer.link.mvp.ParentPresenter +import com.tencent.iot.explorer.link.mvp.model.ChooseCountryModel +import com.tencent.iot.explorer.link.mvp.view.ChooseCountryView +import com.tencent.iot.explorer.link.mvp.view.RegisterView + +class ChooseCountryPresenter(view: ChooseCountryView) : ParentPresenter(view) { + + override fun getIModel(view: ChooseCountryView): ChooseCountryModel { + return ChooseCountryModel(view) + } + + fun setCountry(country: String) { + model?.setCountry(country) + } + + fun getCountryCode(): String { + return model!!.getCountryCode() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tencent/iot/explorer/link/mvp/view/ChooseCountryView.kt b/app/src/main/java/com/tencent/iot/explorer/link/mvp/view/ChooseCountryView.kt new file mode 100644 index 000000000..fb54a05ce --- /dev/null +++ b/app/src/main/java/com/tencent/iot/explorer/link/mvp/view/ChooseCountryView.kt @@ -0,0 +1,9 @@ +package com.tencent.iot.explorer.link.mvp.view + +import com.tencent.iot.explorer.link.mvp.ParentView + +interface ChooseCountryView : ParentView { + + fun showCountryCode(countryCode: String, countryName: String) + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/background_permission.xml b/app/src/main/res/drawable/background_permission.xml new file mode 100644 index 000000000..7213b6484 --- /dev/null +++ b/app/src/main/res/drawable/background_permission.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/switch_track_on.xml b/app/src/main/res/drawable/switch_track_on.xml index 9fdeb4f88..12e37e5ed 100644 --- a/app/src/main/res/drawable/switch_track_on.xml +++ b/app/src/main/res/drawable/switch_track_on.xml @@ -4,7 +4,7 @@ - + diff --git a/app/src/main/res/layout/activity_about_us.xml b/app/src/main/res/layout/activity_about_us.xml index a0250d90d..b66f2213e 100644 --- a/app/src/main/res/layout/activity_about_us.xml +++ b/app/src/main/res/layout/activity_about_us.xml @@ -5,23 +5,23 @@ android:id="@+id/about_us_constraintLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" + android:background="@color/gray_F5F5F5" tools:context=".kitlink.activity.AboutUsActivity"> + android:id="@+id/about_us_sbhv" + layout="@layout/menu_back_layout" /> + android:id="@+id/iv_about_logo" + android:layout_width="50dp" + android:layout_height="50dp" + android:layout_marginTop="42dp" + android:src="@mipmap/app_png" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@+id/about_us_sbhv" /> --> + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="@id/tv_title_privacy_policy" + app:layout_constraintBottom_toBottomOf="@id/tv_title_version" + android:background="@color/white" + /> + + + android:id="@+id/tv_title_privacy_policy" + android:layout_width="match_parent" + android:layout_height="48dp" + android:background="@color/white" + android:gravity="center_vertical" + android:layout_marginTop="1dp" + android:paddingStart="20dp" + android:paddingEnd="20dp" + android:text="@string/register_agree_4" + android:textColor="@color/black_15161A" + android:textSize="@dimen/ts_14" + app:layout_constraintTop_toBottomOf="@+id/about_us_line" /> + android:id="@+id/iv_privacy_policy" + android:layout_width="18dp" + android:layout_height="18dp" + android:layout_marginEnd="20dp" + android:src="@mipmap/icon_arrow" + android:rotation="180" + app:layout_constraintBottom_toBottomOf="@+id/tv_title_privacy_policy" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="@+id/tv_title_privacy_policy" /> + android:id="@+id/line_privacy_policy" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" + app:layout_constraintTop_toBottomOf="@+id/tv_title_privacy_policy" + android:background="@color/gray_E7E8EB" /> + android:id="@+id/tv_title_user_agreement" + android:layout_width="match_parent" + android:layout_height="48dp" + android:background="@color/white" + android:gravity="center_vertical" + android:layout_marginTop="1dp" + android:paddingStart="20dp" + android:paddingEnd="20dp" + android:text="@string/register_agree_2" + android:textColor="@color/black_15161A" + android:textSize="@dimen/ts_14" + app:layout_constraintTop_toBottomOf="@+id/line_privacy_policy" /> + android:id="@+id/iv_user_agreement" + android:layout_width="18dp" + android:layout_height="18dp" + android:layout_marginEnd="20dp" + android:src="@mipmap/icon_arrow" + android:rotation="180" + app:layout_constraintBottom_toBottomOf="@+id/tv_title_user_agreement" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="@+id/tv_title_user_agreement" /> + android:id="@+id/line_user_agreement" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" + app:layout_constraintTop_toBottomOf="@+id/tv_title_user_agreement" + android:background="@color/gray_E7E8EB" /> + android:id="@+id/tv_title_version" + android:layout_width="match_parent" + android:layout_height="48dp" + android:background="@color/white" + android:gravity="center_vertical" + android:layout_marginTop="1dp" + android:paddingStart="20dp" + android:paddingEnd="20dp" + android:text="@string/version_code" + android:textColor="@color/black_15161A" + android:textSize="@dimen/ts_14" + app:layout_constraintTop_toBottomOf="@+id/line_opensource" /> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/tv_about_app_version" + app:layout_constraintRight_toRightOf="parent" + android:layout_marginEnd="20dp" + android:text="V1.0.0" + android:gravity="center_horizontal" + android:textSize="@dimen/ts_14" + android:textColor="@color/gray_A1A7B2" + app:layout_constraintTop_toTopOf="@+id/tv_title_version" + app:layout_constraintBottom_toBottomOf="@+id/tv_title_version"/> + android:id="@+id/line_version" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" + android:visibility="invisible" + app:layout_constraintTop_toBottomOf="@+id/tv_title_version" + android:background="@color/gray_E7E8EB" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account_and_safety.xml b/app/src/main/res/layout/activity_account_and_safety.xml index b5e801e4b..19f86e1cf 100644 --- a/app/src/main/res/layout/activity_account_and_safety.xml +++ b/app/src/main/res/layout/activity_account_and_safety.xml @@ -144,6 +144,41 @@ android:visibility="invisible" android:background="@color/gray_E7E8EB" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_permissions.xml b/app/src/main/res/layout/activity_permissions.xml new file mode 100644 index 000000000..41be919c9 --- /dev/null +++ b/app/src/main/res/layout/activity_permissions.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_register.xml b/app/src/main/res/layout/activity_register.xml index 2329d0745..c62175d67 100644 --- a/app/src/main/res/layout/activity_register.xml +++ b/app/src/main/res/layout/activity_register.xml @@ -102,6 +102,6 @@ android:text="@string/get_code" android:textColor="@color/white" android:textSize="@dimen/ts_16" - app:layout_constraintTop_toBottomOf="@+id/ll_register_agreement" /> + app:layout_constraintTop_toBottomOf="@+id/vp_register" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_permission_list.xml b/app/src/main/res/layout/item_permission_list.xml new file mode 100644 index 000000000..9b6138a68 --- /dev/null +++ b/app/src/main/res/layout/item_permission_list.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_email_register.xml b/app/src/main/res/layout/layout_email_register.xml index 69d491537..72f1b0a07 100644 --- a/app/src/main/res/layout/layout_email_register.xml +++ b/app/src/main/res/layout/layout_email_register.xml @@ -10,91 +10,109 @@ android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" android:background="@color/white" - app:layout_constraintBottom_toBottomOf="@id/line_register_email" + app:layout_constraintBottom_toBottomOf="@id/layout_email" /> - + android:background="@color/white" + android:id="@+id/layout_email_country" + > - - - + - + - + - + + + + - - + + + + + + - + app:layout_constraintTop_toTopOf="parent" /> + + + android:layout_marginLeft="20dp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@+id/layout_email" /> - + android:background="@color/white" + android:id="@+id/layout_phone_country" + > - - - + - + - + - + + + + + android:id="@+id/layout_phone" + > - + + + + - + /> + + + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@+id/layout_phone" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_user_agree_layout.xml b/app/src/main/res/layout/popup_user_agree_layout.xml new file mode 100644 index 000000000..96683dd56 --- /dev/null +++ b/app/src/main/res/layout/popup_user_agree_layout.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/icon_logo_small.png b/app/src/main/res/mipmap-hdpi/icon_logo_small.png new file mode 100644 index 000000000..37beab59e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/icon_logo_small.png differ diff --git a/app/src/main/res/mipmap-mdpi/icon_logo_small.png b/app/src/main/res/mipmap-mdpi/icon_logo_small.png new file mode 100644 index 000000000..9f49c4bf8 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/icon_logo_small.png differ diff --git a/app/src/main/res/mipmap-xhdpi/icon_logo_small.png b/app/src/main/res/mipmap-xhdpi/icon_logo_small.png new file mode 100644 index 000000000..ad8cb0273 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/icon_logo_small.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/icon_logo_small.png b/app/src/main/res/mipmap-xxhdpi/icon_logo_small.png new file mode 100644 index 000000000..e1b2bffe3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/icon_logo_small.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/icon_logo_small.png b/app/src/main/res/mipmap-xxxhdpi/icon_logo_small.png new file mode 100644 index 000000000..c21c24d8e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/icon_logo_small.png differ diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml index 257d7757f..ac0a6c119 100644 --- a/app/src/main/res/values-en-rUS/strings.xml +++ b/app/src/main/res/values-en-rUS/strings.xml @@ -26,6 +26,7 @@ Failed to edit Save Reject + Allow Accept Exit Agree @@ -680,7 +681,23 @@ The device is offline 0 devices Popup title - + No + Yes + and + Privacy protection + Third-party information sharing checklist + In order to protect your personal rights and interests, before you use our products, please be sure to carefully read Tencent LLink + ,to help you understand our collection/storage/use/external provision/protection of your personal information and your rights. \nWe will not actively share or transfer your personal information to a third party. If there are other situations in which your personal information is shared or transferred or you need us to share or transfer that to a third party, we will confirm that the third party obtains your agreement to the above actions. For personal information that needs to be shared with third-party service providers in our products, please refer to + . \nIf you have any questions about the above agreement, you can send the question to the official email fanyi@tencent.com of Tencent LLink or to our dedicated department for personal information protection.The mailing address is:Tencent Building, Keji Zhong Road, Nanshan District, Shenzhen, Guangdong, China. Recipient:Legal Department,Data and Privacy Protection Center\n If you agree to the content of the above agreement, please click "Yes" to accept our product service! + Authority management + Please authorize Tencent LLink the following permissions + In order to provide you with the user agreement and privacy policy of your country/region, please select your country/region + In order to use the real-time communication of the device, Tencent LLink needs to obtain the following permissions: + · camera/microphone + This step will configure the WiFi network for the hardware device to achieve communication, and bind the relationship between the hardware device and the APP user. Therefore, Tencent LLink needs to obtain the following permissions: + · WiFi name、WiFi password\n· the IP of the mobile phone + You will authorize the following personal information to be bound to your WeChat account: + · nickname、profile photo、region、gender\n· device information diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9ff891e5c..942a218ad 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -26,6 +26,7 @@ 修改失败 保存 拒绝 + 始终允许 接受 退出 同意 @@ -680,7 +681,23 @@ 设备已经离线 0个设备 弹框标题 - + 暂不使用 + 同意 + + 隐私保护 + 第三方信息共享清单 + 为了更好地保障您的个人权益,在您使用我们的产品前,请务必谨慎阅读腾讯连连 + ,以帮助您了解我们对您的个人信息的收集/保存/使用/对外提供/保护等情况以及您享有的权利。\n我们不会主动共享或转让您的个人信息至第三方,如存在其他共享或转让您的个人信息或您需要我们将您的个人信息共享或转让至第三方情形时,我们会直接或确认第三方征得您对上述行为的明示同意。我们的产品中需要与第三方服务商共享的个人信息,请您查阅 + 。\n如您对以上协议有任何疑问,您可将问题发送至腾讯连连官方邮箱fanyi@tencent.com或寄给我们设立的个人信息保护专职部门。邮寄地址为:中国广东省深圳市南山区科技中一路腾讯大厦 法务部 数据及隐私保护中心(收)\n如您同意以上协议内容,请点击“同意”开始接受我们的产品服务! + 权限管理 + 请授权腾讯连连以下权限 + 为了向您提供对应国家/地区的用户协议和隐私政策,请您选择所在的国家及地区 + 为了使用设备的实时语音/视频通话功能,因此腾讯连连需 获取以下权限: + · 摄像头/麦克风 + 该步骤将为硬件设备配置WiFi网络实现通信,绑定硬件设备与APP用户之间的关系。因此 腾讯连连 需获取以下权限: + · WiFi名称、WiFi密码\n· 手机IP地址 + 您将授权以下个人信息与微信账号绑定: + · 昵称、头像、地区、性别\n· 设备信息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6fa4709ad..942a218ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,686 +1,703 @@ - - - @string/app_name_zh - 首页 - 场景 - 我的 - 评测 - 智能 - 腾讯连连 - - - 成功 - 失败 - 错误 - 下一步 - 重新发送 - 重新发送(%ss) - 完成 - 取消 - 关闭 - 确定 - 确认 - 绑定 - 提交 - 删除 - 修改失败 - 保存 - 拒绝 - 接受 - 退出 - 同意 - 返回 - 返回首页 - 问题反馈 - 已复制 - 已复制到剪切板 - 请打开 WIFI - 下载成功 - 下载失败 - 微信好友 - 复制链接 - 我的智能 - 执行日志 - 请先登录后,再查看或添加智能 - 当前暂无智能,点击添加智能 - 立即登录 - 立即添加 - 手动智能 - 自动智能 - 添加手动智能 - 编辑手动智能 - 添加自动智能 - 编辑自动智能 - 自动 - 手动 - 暂无手动任务 - 当前暂无日志 - 添加任务 - 设备控制 - 延时 - 执行以下任务 - 延时不能设置为最后一个任务 - 至少包含一个条件 - 没有任务 - 完善智能信息 - 智能图片 - 智能名称 - 选择智能图片 - 填写智能名称 - 选择设备 - 该设备暂无功能可作为智能任务,试试其他设备吧 - 请设置智能图片 - 请设置智能名称 - 添加条件 - 满足以下所有条件 - 满足以下任一条件 - 生效时间段 - 执行以下任务 - 设备状态发生变化 - 定时 - 选择手动 - 发送通知 - 重复周期 - 自定义 - 自定义(%s) - 全天(24小时) - 全天 - 时间段 - 结束时间不能早于开始时间 - 请至少选择一天 - 选择通知类型 - 选择手动智能 - 全选 - 取消全选 - 请至少选择一种通知类型 - 等于 - 执行手动智能成功 - 修改成功 - 请先进行设置后保存 - 智能失效 - 名称不能超过20个字符 - 最多添加20个任务 - 最多添加20个条件 - 关闭自动智能成功 - 打开自动智能成功 - 未知设备 - 请添加任务 - 暂无手动智能,请先添加 - 选择其他设备 - 延时时长至少为1分钟 - 大于 - 等于 - 小于 - 删除设备 - 长度非法 - 国家/地区 - (产品的名称) - 设备共享 - 确定切换时区吗 - 您切换时区将会同步更改您为家里的智能设备设置的定时、场景等任务时区,请知悉! - 添加设备 - 扫描设备 - 添加智能 - 相对湿度 %s - 实况风向 %s - ° - - - - - - 干燥 - 舒适 - 潮湿 - 搜索地点 - 地图选点 - %s个房间 - 暂无天气信息 - 请先设置当前家庭位置 - 更多操作 - 搜索 - 还没有历史记录哦~ - 清空历史记录 - 没有找到匹配的结果 - 请输入密码 - 获取验证码 -  (+%s) - - - 微信登录 - 邮箱登录 - 手机登录 - 中国大陆 - 使用邮箱账号 - 使用手机账号 - 使用邮箱注册 - 使用手机注册 - 手机注册 - 邮箱注册 - 同意并遵守腾讯云 - 用户协议 - - 隐私政策 - 隐私政策 - 开源软件信息 - 获取验证码 - 验证码已经发送到您的手机: - 验证码已经发送到您的邮箱: - 未安装微信客户端 - 微信未安装,请安装后重试 - 取消授权 - 出生日期 - 很遗憾,我们目前无法向您提供腾讯连连 - 定位失败 - - 登录 - 退出登录 - 注册 - 注册账号 - 账号 - 手机验证码 - 验证码 - 输入验证码 - 手机号码 - 手机号 - 邮箱地址 - 邮箱地址 - 密码 - 新密码 - 确认密码 - 新密码不能与旧密码相同 - 再次输入密码 - 忘记密码 - 找回密码 - 设置密码 - 密码设置成功 - 选择国家和地区 - 热门国家和地区 - Copyright @ 2013–2020 Tencent Cloud.All Right Reserved.\n腾讯云 版权所有 - - - - 消息通知 - 帮助中心 - 意见反馈 - 关于我们 - 当前版本 %s - - 个人信息 - 设置头像 - 拍照 - 从手机相册选取 - 昵称 - 电话号码 - 用户 ID - 修改密码 - 修改昵称 - 手机绑定 - 旧密码 - 请先绑定手机号 - 当前未绑定手机号,无法进行修改密码 - 暂无消息 - 请填写10个字以上文字以便我们更好的帮助您 - 相关截图 - 输入有效联系方式以便开发者联系您(选填) - - - - 请输入验证码 - 验证码格式不正确 - 请阅读并同意用户协议和隐私政策 - 登录成功 - 手机号绑定成功 - 获取用户 ID 失败 - 您的应用为最新版本 - 没有可下载路径 - 未知错误 - - - 我的设备 - 添加设备 - 共享的设备 - 扫码添加 - 智能配网 - 请点击箭头按钮选择WIFI - 请连接WIFI - 自助配网 - 消息 - 连接中 - 连接成功 - 连接失败 - 继续添加新设备 - 继续添加其他设备 - 返回首页 - 按步骤重试 - 切换配网方式 - 查看更多失败原因 - 1、检查设备是否通电,并按照指引进入配网模式\n -2、检查WIFI是否正常(暂时只支持2.4G路由器)\n -3、检查WIFI密码是否错误 - - 当前暂无家庭,点击按钮进行添加 - 当前暂无房间,点击按钮进行添加 - 没有可选的房间 - 当前暂无设备,点击按钮进行添加 - 无添加权限 - 您不是房间所有者,没有此权限 - 将设备设置为一键配网模式 - 若指示灯已经在快闪,可跳过该步骤。 - 1.接通设备电源;\n2.长按复位键(开关),切换设备配网模式到一键配网(不同设备操作方式有所不同);\n3.指示灯快闪即进入一键配网模式。 - 请将设备与手机尽量靠近 - 提示灯在快闪 - 连接WI-FI - 输入WI-FI密码 - 请输入密码 - 请输入密码(非必填) - 配网进度 - 一键配网 - 热点配网 - 将设备设置为热点配网模式 - 重置设备 - 重置设备教程 - 我已确认上述操作 - 请打开手机蓝牙功能,后将设备置于可发现状态。 - 没有产品信息 - 若指示灯已经在慢闪,可跳过该步骤。 - 1.接通设备电源;\n2.长按复位键(开关),切换设备配网模式到热点配网(不同设备操作方式有所不同);\n3.指示灯慢闪即进入热点配网模式。 - 下一步 - 将手机WIFI连接设备热点 - 请前往手机WiFi设置页面,连接上图所示设备WiFi - 2.返回APP,添加设备 - 1.将手机连接到上图所示的WI-FI;\n2.或者手动去系统WI-FI设置页连接 - 连接设备热点 - 重新连接 - 连接正确,进入下一步 - 操作方式 - 1.点击WiFi名称右侧的下拉按钮,前往手机WiFi设置界面选择设备热点后,返回APP。\n2.填写设备密码,若设备热点无密码则无需填写。\n3.点击下一步,开始配网。 - tcloud_XXX - - 离线 - 设备在线 - 设备昵称 - 电源开关:开启 - 电源开关:关闭 - 开启 - 关闭 - 提示 - 去开启 - 手机WIFI未开启,请开启并连接WIFI - 请开启手机定位服务,以便获取附近WIFI信号 - - 设备详情 - 备注名称 - 设备名称 - 产品ID - 位置管理 - 设备信息 - 常见问题 - 暂未分享设备,点击按钮进行分享 - 暂未分享给其他用户 - 添加分享 - 修改备注名称 - 确定要删除设备码? - 删除后数据无法直接恢复 - 退出添加设备 - 当前正在添加设备,是否退出? - 分享发送成功 - - 厂家名称 - 产品型号 - MAC地址 - IP地址 - 固件版本 - 配置硬件 - 选择目标WIFI - 设置目标WIFI - 开始配网 - 请输入WIFI密码 - 将手机WIFI连接设备热点 - WIFI - 建议使用2.4G WIFI - 刷新 - 连接设备 - 热点配网 - 蓝牙设备绑定 - 点击获取WIFI列表 - 请打开手机GPS定位后获取WIFI列表。 - 连接失败 - 连接成功,请继续 - 请检查您的WIFI密码是否正确 - 我知道了 - WIFI连接中... - 手机与设备连接成功 - 向设备发送消息成功 - 设备连接云端成功 - 初始化成功 - 配网失败 - 1.确认设备处于一键配网模式(指示灯慢闪)\n2.核对家庭WIFI密码是否正确\n3.确认路由设备是否为2.4GWIFI频段 - 1.确认设备处于热点模式(指示灯慢闪)\n2.确认是否连接到设备热点\n3.核对家庭WIFI密码是否正确\n4.确认路由设备是否为2.4GWIFI频段 - 查看更多失败原因 - 切换到热点配网 - 切换到一键配网 - 配网完成,添加设备成功 - 执行一次 - 消息中心 - 当前智能设备丢失,请重新设置动作 - 立即绑定 - 绑定设备 - 通过将设备与腾讯连连连接,即表示您同意共享有关您设备和设备日志的信息,第三方可以根据其条款和政策使用该设备。有关更多信息,请阅读我们的 %s。 - 有关更多信息,请阅读我们的 - 设定位置 - - - 云端定时 - 添加定时 - 修改定时 - 定时名称 - 动作名称 - 重复 - 工作日 - 周末 - 设备动作 - 删除动作 - 名称设置 - 当前暂无云端定时,点击按钮进行添加 - 每周日 - 每周一 - 每周二 - 每周三 - 每周四 - 每周五 - 每周六 - - - 设备分享 - 分享用户 - 分享用户列表 - 设备已经单独分享给以下用户 - - - 家庭管理 - 家庭名称 - 邀请家庭成员 - 家庭成员 - 添加家庭 - 家庭位置 - 请输入家庭名称 - 请输入房间名称 - 添加房间 - 房间管理 - 家庭详情 - 删除家庭 - 退出家庭 - 邀请成员 - 房间设置 - 删除房间 - 家庭名称 - 家庭设置 - 成员设置 - 成员名称 - 名称 - 角色 - 关联账号 - 所有者 - 成员 - 移除成员 - 邀请发送成功 - 成功加入家庭 - 成功绑定共享设备 - - - 您确定要删除该家庭吗? - 删除家庭后,系统将清除所有成员与家庭数据,该家庭下的设备也将被删除 - 您确定要退出该家庭吗? - 退出家庭后,可重新接受邀请 - 您确定要删除该房间吗? - 删除房间后,系统将清除房间数据,该房间下的设备也将被删除 - 您确定要删除该成员吗? - 删除成员后,可重新邀请 - 昵称长度为1-20 - 设备名称长度为1-20 - 删除成功 - 设备已离线 - 请检查:\n -1.设备是否有电\n -2.设备连接的路由器是否正常工作,网络通畅\n -3.是否修改了路由器的名称或密码,可以尝试重新连接\n -4.设备是否与路由器距离过远、隔墙或有其他遮挡物 - - 设备 - 家庭 - 通知 - 开启定位获取WiFi信息 - 版本升级V2.0 - 1、修复了一些已知 bug - 版本号 - 安装包大小 - 发布时间 - 下次再说 - 立即升级 - 正在更新,请稍后... - - - - 已发现如下设备 - 正在扫描附近的蓝牙设备 - 继续扫描附近设备 - 发现设备需开启手机蓝牙(10001) - 未发现设备,请确认设备已开启 - 重试 - 连接 - 全部 - 所有设备(%s) - 共享设备(%s) - 当前暂无设备,请添加设备 - 客厅 - 卧室 - 厨房 - 推荐 - - - - Settings - - 所有图片 - 预览 - 完成 - 没有系统相机 - 已经达到最高选择数量 - 拍摄照片 - - 取消 - 确定 - 权限需求 - 选择图片时需要读取权限 - 拍照时需要存储权限 - - - - 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ - 请输入手机号 - 手机号格式不正确 - 请输入邮箱地址 - 邮箱地址格式不正确 - 密码支持8–16位,必须包含字母和数字 - 两次输入的密码不一致 - 密码修改成功,请重新登录 - - - 欢迎使用%s - 欢迎使用 - 创建新账号 - 使用已有账号登录 - 账号密码登录 - 验证码登录 - 手机号码/邮箱地址 - 其他登录方式 - 微信 - 账号与安全 - 手机号码 - 未绑定 - 已绑定 - 邮箱 - 账号注销 - 请输入手机号 - 请输入邮箱号 - 请输入验证码 - 请设置您的密码 - 请再次确认您的密码 - 再次确认 - 确认绑定 - 绑定手机号 - 绑定邮箱号 - 确认修改 - 修改手机号 - 修改邮箱号 - 邮箱验证码修改 - 手机验证码修改 - - 注销 - 注销账号 - 注销须知 - 请您确保账号处于安全状态下且是本人申请注销。注销账号是不可恢复的操作,账号被注销后,您账号下的所有信息、数据将被永久删除,无法找回。为避免您的损失,请谨慎进行账号注销操作。 - 如果您确定“注销账号”,账号将注销于 - 2020年8月3日 00:00:00 - 若您在注销日期前登录腾讯连连,则自动撤销“注销账号”申请。 - 注:如腾讯连连账号是通过第三方软件(微信) 进行登录的,您所注销的账号仅影响腾讯连连内的账号与数据,并不会影响您在微信的账号信息。 - 《腾讯连连账号注销协议》 - 腾讯连连账号注销协议 - 请阅读并同意腾讯连连账号注销协议 - 我已了解 - 确定注销账号吗 - 注销后,此账户下的所有用户数据也将被永久删除 - 账号已申请注销 - 如需撤销,请在7日内登录腾讯连连 - 知道了 - 账号注销已终止 - 由于你在申请“账号注销”后的7天内重新登录,该账号在date提交的“账号注销”申请已被撤销 - - - 密码修改成功,请重新登录 - 验证码发送成功 - 更新手机号成功 - 更新邮箱号成功 - 短信验证码为空 - 邮箱验证码为空 - 手机号为空 - 邮箱号为空 - 两次输入的密码不一致 - 绑定成功 - - - 头像 - 修改登录密码 - 账户所在地 - 温度单位 - 时区 - 未设置 - - - C - F - 注销账号 - 中国大陆 - Chinese Mainland - 美国 - 选择时区 - 设置成功 - 设置失败,错误码: - 删除成功 - 删除失败,错误码: - 反注册成功 - 反注册失败 - 收到消息: - 注册成功1. token: - 注册成功 - 注册失败,错误码: - 通知被打开 : - 通知被清除 : - 您有1条新消息,通知被展示, - ,推送频道: - yyyy年MM月dd日 HH:mm:ss - yyyy年MM月dd日 - - 分钟 - 小时 - - - - - - 图片加载错误,请重新选择图片 - 图片裁剪异常,请重新选择图片 - 未检测到SD卡 - 图片已保存至目录下 - 该图片已存在目录下 - 北京 - 天津 - 上海 - 重庆 - 深圳 - 解析响应数据时,Response为空 - 使用RespFailMessage类解析数据失败 - 解析响应数据时发生错误 - 微信已经绑定过了, 请勿重复绑定 - 添加成功 - 请填写定时名称 - 我的定时 - 中国大陆 - 请填写问题描述 - 请填写不少于10个字的问题描述 - 提交成功 - 再按一下退出应用 - 请您打开手机位置以便获取WIFI名称 - 请输入昵称 - 仅一次 - 每天 - 周日 - 周一 - 周二 - 周三 - 周四 - 周五 - 周六 - 多云转晴 - 网络连接失败,请检查密码是否正确 - 连接到网络:%s 失败,请手动连接 - 全部设备 - 全部 - %s个设备 - 请确保已按官网文档接入微信登录 - 服务器出错 - 授权失败 - 获取设备与 token 的绑定状态失败 - 获取家庭与设备绑定关系失败 - 选择房间 - %s 的家 - 填写家庭名称 - 填写房间名称 - 房间名称 - 标题 - 云端定时 - 手机号码/邮箱 - 天天 - 大厅 - 颜色 - 红色 - 亮度 - 电源开关:关闭 - 电源开关 - 正在开发中 - 我的家 - 3个房间 - 123的家 - 图片 - 爸爸 - 1张 - 预览(1) - 警告 - 设备已经离线 - 0个设备 - 弹框标题 - - - - + + + @string/app_name_zh + 首页 + 场景 + 我的 + 评测 + 智能 + 腾讯连连 + + + 成功 + 失败 + 错误 + 下一步 + 重新发送 + 重新发送(%ss) + 完成 + 取消 + 关闭 + 确定 + 确认 + 绑定 + 提交 + 删除 + 修改失败 + 保存 + 拒绝 + 始终允许 + 接受 + 退出 + 同意 + 返回 + 返回首页 + 问题反馈 + 已复制 + 已复制到剪切板 + 请打开 WIFI + 下载成功 + 下载失败 + 微信好友 + 复制链接 + 我的智能 + 执行日志 + 请先登录后,再查看或添加智能 + 当前暂无智能,点击添加智能 + 立即登录 + 立即添加 + 手动智能 + 自动智能 + 添加手动智能 + 编辑手动智能 + 添加自动智能 + 编辑自动智能 + 自动 + 手动 + 暂无手动任务 + 当前暂无日志 + 添加任务 + 设备控制 + 延时 + 执行以下任务 + 延时不能设置为最后一个任务 + 至少包含一个条件 + 没有任务 + 完善智能信息 + 智能图片 + 智能名称 + 选择智能图片 + 填写智能名称 + 选择设备 + 该设备暂无功能可作为智能任务,试试其他设备吧 + 请设置智能图片 + 请设置智能名称 + 添加条件 + 满足以下所有条件 + 满足以下任一条件 + 生效时间段 + 执行以下任务 + 设备状态发生变化 + 定时 + 选择手动 + 发送通知 + 重复周期 + 自定义 + 自定义(%s) + 全天(24小时) + 全天 + 时间段 + 结束时间不能早于开始时间 + 请至少选择一天 + 选择通知类型 + 选择手动智能 + 全选 + 取消全选 + 请至少选择一种通知类型 + 等于 + 执行手动智能成功 + 修改成功 + 请先进行设置后保存 + 智能失效 + 名称不能超过20个字符 + 最多添加20个任务 + 最多添加20个条件 + 关闭自动智能成功 + 打开自动智能成功 + 未知设备 + 请添加任务 + 暂无手动智能,请先添加 + 选择其他设备 + 延时时长至少为1分钟 + 大于 + 等于 + 小于 + 删除设备 + 长度非法 + 国家/地区 + (产品的名称) + 设备共享 + 确定切换时区吗 + 您切换时区将会同步更改您为家里的智能设备设置的定时、场景等任务时区,请知悉! + 添加设备 + 扫描设备 + 添加智能 + 相对湿度 %s + 实况风向 %s + ° + + + + + + 干燥 + 舒适 + 潮湿 + 搜索地点 + 地图选点 + %s个房间 + 暂无天气信息 + 请先设置当前家庭位置 + 更多操作 + 搜索 + 还没有历史记录哦~ + 清空历史记录 + 没有找到匹配的结果 + 请输入密码 + 获取验证码 +  (+%s) + + + 微信登录 + 邮箱登录 + 手机登录 + 中国大陆 + 使用邮箱账号 + 使用手机账号 + 使用邮箱注册 + 使用手机注册 + 手机注册 + 邮箱注册 + 同意并遵守腾讯云 + 用户协议 + + 隐私政策 + 隐私政策 + 开源软件信息 + 获取验证码 + 验证码已经发送到您的手机: + 验证码已经发送到您的邮箱: + 未安装微信客户端 + 微信未安装,请安装后重试 + 取消授权 + 出生日期 + 很遗憾,我们目前无法向您提供腾讯连连 + 定位失败 + + 登录 + 退出登录 + 注册 + 注册账号 + 账号 + 手机验证码 + 验证码 + 输入验证码 + 手机号码 + 手机号 + 邮箱地址 + 邮箱地址 + 密码 + 新密码 + 确认密码 + 新密码不能与旧密码相同 + 再次输入密码 + 忘记密码 + 找回密码 + 设置密码 + 密码设置成功 + 选择国家和地区 + 热门国家和地区 + Copyright @ 2013–2020 Tencent Cloud.All Right Reserved.\n腾讯云 版权所有 + + + + 消息通知 + 帮助中心 + 意见反馈 + 关于我们 + 当前版本 %s + + 个人信息 + 设置头像 + 拍照 + 从手机相册选取 + 昵称 + 电话号码 + 用户 ID + 修改密码 + 修改昵称 + 手机绑定 + 旧密码 + 请先绑定手机号 + 当前未绑定手机号,无法进行修改密码 + 暂无消息 + 请填写10个字以上文字以便我们更好的帮助您 + 相关截图 + 输入有效联系方式以便开发者联系您(选填) + + + + 请输入验证码 + 验证码格式不正确 + 请阅读并同意用户协议和隐私政策 + 登录成功 + 手机号绑定成功 + 获取用户 ID 失败 + 您的应用为最新版本 + 没有可下载路径 + 未知错误 + + + 我的设备 + 添加设备 + 共享的设备 + 扫码添加 + 智能配网 + 请点击箭头按钮选择WIFI + 请连接WIFI + 自助配网 + 消息 + 连接中 + 连接成功 + 连接失败 + 继续添加新设备 + 继续添加其他设备 + 返回首页 + 按步骤重试 + 切换配网方式 + 查看更多失败原因 + 1、检查设备是否通电,并按照指引进入配网模式\n +2、检查WIFI是否正常(暂时只支持2.4G路由器)\n +3、检查WIFI密码是否错误 + + 当前暂无家庭,点击按钮进行添加 + 当前暂无房间,点击按钮进行添加 + 没有可选的房间 + 当前暂无设备,点击按钮进行添加 + 无添加权限 + 您不是房间所有者,没有此权限 + 将设备设置为一键配网模式 + 若指示灯已经在快闪,可跳过该步骤。 + 1.接通设备电源;\n2.长按复位键(开关),切换设备配网模式到一键配网(不同设备操作方式有所不同);\n3.指示灯快闪即进入一键配网模式。 + 请将设备与手机尽量靠近 + 提示灯在快闪 + 连接WI-FI + 输入WI-FI密码 + 请输入密码 + 请输入密码(非必填) + 配网进度 + 一键配网 + 热点配网 + 将设备设置为热点配网模式 + 重置设备 + 重置设备教程 + 我已确认上述操作 + 请打开手机蓝牙功能,后将设备置于可发现状态。 + 没有产品信息 + 若指示灯已经在慢闪,可跳过该步骤。 + 1.接通设备电源;\n2.长按复位键(开关),切换设备配网模式到热点配网(不同设备操作方式有所不同);\n3.指示灯慢闪即进入热点配网模式。 + 下一步 + 将手机WIFI连接设备热点 + 请前往手机WiFi设置页面,连接上图所示设备WiFi + 2.返回APP,添加设备 + 1.将手机连接到上图所示的WI-FI;\n2.或者手动去系统WI-FI设置页连接 + 连接设备热点 + 重新连接 + 连接正确,进入下一步 + 操作方式 + 1.点击WiFi名称右侧的下拉按钮,前往手机WiFi设置界面选择设备热点后,返回APP。\n2.填写设备密码,若设备热点无密码则无需填写。\n3.点击下一步,开始配网。 + tcloud_XXX + + 离线 + 设备在线 + 设备昵称 + 电源开关:开启 + 电源开关:关闭 + 开启 + 关闭 + 提示 + 去开启 + 手机WIFI未开启,请开启并连接WIFI + 请开启手机定位服务,以便获取附近WIFI信号 + + 设备详情 + 备注名称 + 设备名称 + 产品ID + 位置管理 + 设备信息 + 常见问题 + 暂未分享设备,点击按钮进行分享 + 暂未分享给其他用户 + 添加分享 + 修改备注名称 + 确定要删除设备码? + 删除后数据无法直接恢复 + 退出添加设备 + 当前正在添加设备,是否退出? + 分享发送成功 + + 厂家名称 + 产品型号 + MAC地址 + IP地址 + 固件版本 + 配置硬件 + 选择目标WIFI + 设置目标WIFI + 开始配网 + 请输入WIFI密码 + 将手机WIFI连接设备热点 + WIFI + 建议使用2.4G WIFI + 刷新 + 连接设备 + 热点配网 + 蓝牙设备绑定 + 点击获取WIFI列表 + 请打开手机GPS定位后获取WIFI列表。 + 连接失败 + 连接成功,请继续 + 请检查您的WIFI密码是否正确 + 我知道了 + WIFI连接中... + 手机与设备连接成功 + 向设备发送消息成功 + 设备连接云端成功 + 初始化成功 + 配网失败 + 1.确认设备处于一键配网模式(指示灯慢闪)\n2.核对家庭WIFI密码是否正确\n3.确认路由设备是否为2.4GWIFI频段 + 1.确认设备处于热点模式(指示灯慢闪)\n2.确认是否连接到设备热点\n3.核对家庭WIFI密码是否正确\n4.确认路由设备是否为2.4GWIFI频段 + 查看更多失败原因 + 切换到热点配网 + 切换到一键配网 + 配网完成,添加设备成功 + 执行一次 + 消息中心 + 当前智能设备丢失,请重新设置动作 + 立即绑定 + 绑定设备 + 通过将设备与腾讯连连连接,即表示您同意共享有关您设备和设备日志的信息,第三方可以根据其条款和政策使用该设备。有关更多信息,请阅读我们的 %s。 + 有关更多信息,请阅读我们的 + 设定位置 + + + 云端定时 + 添加定时 + 修改定时 + 定时名称 + 动作名称 + 重复 + 工作日 + 周末 + 设备动作 + 删除动作 + 名称设置 + 当前暂无云端定时,点击按钮进行添加 + 每周日 + 每周一 + 每周二 + 每周三 + 每周四 + 每周五 + 每周六 + + + 设备分享 + 分享用户 + 分享用户列表 + 设备已经单独分享给以下用户 + + + 家庭管理 + 家庭名称 + 邀请家庭成员 + 家庭成员 + 添加家庭 + 家庭位置 + 请输入家庭名称 + 请输入房间名称 + 添加房间 + 房间管理 + 家庭详情 + 删除家庭 + 退出家庭 + 邀请成员 + 房间设置 + 删除房间 + 家庭名称 + 家庭设置 + 成员设置 + 成员名称 + 名称 + 角色 + 关联账号 + 所有者 + 成员 + 移除成员 + 邀请发送成功 + 成功加入家庭 + 成功绑定共享设备 + + + 您确定要删除该家庭吗? + 删除家庭后,系统将清除所有成员与家庭数据,该家庭下的设备也将被删除 + 您确定要退出该家庭吗? + 退出家庭后,可重新接受邀请 + 您确定要删除该房间吗? + 删除房间后,系统将清除房间数据,该房间下的设备也将被删除 + 您确定要删除该成员吗? + 删除成员后,可重新邀请 + 昵称长度为1-20 + 设备名称长度为1-20 + 删除成功 + 设备已离线 + 请检查:\n +1.设备是否有电\n +2.设备连接的路由器是否正常工作,网络通畅\n +3.是否修改了路由器的名称或密码,可以尝试重新连接\n +4.设备是否与路由器距离过远、隔墙或有其他遮挡物 + + 设备 + 家庭 + 通知 + 开启定位获取WiFi信息 + 版本升级V2.0 + 1、修复了一些已知 bug + 版本号 + 安装包大小 + 发布时间 + 下次再说 + 立即升级 + 正在更新,请稍后... + + + + 已发现如下设备 + 正在扫描附近的蓝牙设备 + 继续扫描附近设备 + 发现设备需开启手机蓝牙(10001) + 未发现设备,请确认设备已开启 + 重试 + 连接 + 全部 + 所有设备(%s) + 共享设备(%s) + 当前暂无设备,请添加设备 + 客厅 + 卧室 + 厨房 + 推荐 + + + + Settings + + 所有图片 + 预览 + 完成 + 没有系统相机 + 已经达到最高选择数量 + 拍摄照片 + + 取消 + 确定 + 权限需求 + 选择图片时需要读取权限 + 拍照时需要存储权限 + + + + 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ + 请输入手机号 + 手机号格式不正确 + 请输入邮箱地址 + 邮箱地址格式不正确 + 密码支持8–16位,必须包含字母和数字 + 两次输入的密码不一致 + 密码修改成功,请重新登录 + + + 欢迎使用%s + 欢迎使用 + 创建新账号 + 使用已有账号登录 + 账号密码登录 + 验证码登录 + 手机号码/邮箱地址 + 其他登录方式 + 微信 + 账号与安全 + 手机号码 + 未绑定 + 已绑定 + 邮箱 + 账号注销 + 请输入手机号 + 请输入邮箱号 + 请输入验证码 + 请设置您的密码 + 请再次确认您的密码 + 再次确认 + 确认绑定 + 绑定手机号 + 绑定邮箱号 + 确认修改 + 修改手机号 + 修改邮箱号 + 邮箱验证码修改 + 手机验证码修改 + + 注销 + 注销账号 + 注销须知 + 请您确保账号处于安全状态下且是本人申请注销。注销账号是不可恢复的操作,账号被注销后,您账号下的所有信息、数据将被永久删除,无法找回。为避免您的损失,请谨慎进行账号注销操作。 + 如果您确定“注销账号”,账号将注销于 + 2020年8月3日 00:00:00 + 若您在注销日期前登录腾讯连连,则自动撤销“注销账号”申请。 + 注:如腾讯连连账号是通过第三方软件(微信) 进行登录的,您所注销的账号仅影响腾讯连连内的账号与数据,并不会影响您在微信的账号信息。 + 《腾讯连连账号注销协议》 + 腾讯连连账号注销协议 + 请阅读并同意腾讯连连账号注销协议 + 我已了解 + 确定注销账号吗 + 注销后,此账户下的所有用户数据也将被永久删除 + 账号已申请注销 + 如需撤销,请在7日内登录腾讯连连 + 知道了 + 账号注销已终止 + 由于你在申请“账号注销”后的7天内重新登录,该账号在date提交的“账号注销”申请已被撤销 + + + 密码修改成功,请重新登录 + 验证码发送成功 + 更新手机号成功 + 更新邮箱号成功 + 短信验证码为空 + 邮箱验证码为空 + 手机号为空 + 邮箱号为空 + 两次输入的密码不一致 + 绑定成功 + + + 头像 + 修改登录密码 + 账户所在地 + 温度单位 + 时区 + 未设置 + + + C + F + 注销账号 + 中国大陆 + Chinese Mainland + 美国 + 选择时区 + 设置成功 + 设置失败,错误码: + 删除成功 + 删除失败,错误码: + 反注册成功 + 反注册失败 + 收到消息: + 注册成功1. token: + 注册成功 + 注册失败,错误码: + 通知被打开 : + 通知被清除 : + 您有1条新消息,通知被展示, + ,推送频道: + yyyy年MM月dd日 HH:mm:ss + yyyy年MM月dd日 + + 分钟 + 小时 + + + + + + 图片加载错误,请重新选择图片 + 图片裁剪异常,请重新选择图片 + 未检测到SD卡 + 图片已保存至目录下 + 该图片已存在目录下 + 北京 + 天津 + 上海 + 重庆 + 深圳 + 解析响应数据时,Response为空 + 使用RespFailMessage类解析数据失败 + 解析响应数据时发生错误 + 微信已经绑定过了, 请勿重复绑定 + 添加成功 + 请填写定时名称 + 我的定时 + 中国大陆 + 请填写问题描述 + 请填写不少于10个字的问题描述 + 提交成功 + 再按一下退出应用 + 请您打开手机位置以便获取WIFI名称 + 请输入昵称 + 仅一次 + 每天 + 周日 + 周一 + 周二 + 周三 + 周四 + 周五 + 周六 + 多云转晴 + 网络连接失败,请检查密码是否正确 + 连接到网络:%s 失败,请手动连接 + 全部设备 + 全部 + %s个设备 + 请确保已按官网文档接入微信登录 + 服务器出错 + 授权失败 + 获取设备与 token 的绑定状态失败 + 获取家庭与设备绑定关系失败 + 选择房间 + %s 的家 + 填写家庭名称 + 填写房间名称 + 房间名称 + 标题 + 云端定时 + 手机号码/邮箱 + 天天 + 大厅 + 颜色 + 红色 + 亮度 + 电源开关:关闭 + 电源开关 + 正在开发中 + 我的家 + 3个房间 + 123的家 + 图片 + 爸爸 + 1张 + 预览(1) + 警告 + 设备已经离线 + 0个设备 + 弹框标题 + 暂不使用 + 同意 + + 隐私保护 + 第三方信息共享清单 + 为了更好地保障您的个人权益,在您使用我们的产品前,请务必谨慎阅读腾讯连连 + ,以帮助您了解我们对您的个人信息的收集/保存/使用/对外提供/保护等情况以及您享有的权利。\n我们不会主动共享或转让您的个人信息至第三方,如存在其他共享或转让您的个人信息或您需要我们将您的个人信息共享或转让至第三方情形时,我们会直接或确认第三方征得您对上述行为的明示同意。我们的产品中需要与第三方服务商共享的个人信息,请您查阅 + 。\n如您对以上协议有任何疑问,您可将问题发送至腾讯连连官方邮箱fanyi@tencent.com或寄给我们设立的个人信息保护专职部门。邮寄地址为:中国广东省深圳市南山区科技中一路腾讯大厦 法务部 数据及隐私保护中心(收)\n如您同意以上协议内容,请点击“同意”开始接受我们的产品服务! + 权限管理 + 请授权腾讯连连以下权限 + 为了向您提供对应国家/地区的用户协议和隐私政策,请您选择所在的国家及地区 + 为了使用设备的实时语音/视频通话功能,因此腾讯连连需 获取以下权限: + · 摄像头/麦克风 + 该步骤将为硬件设备配置WiFi网络实现通信,绑定硬件设备与APP用户之间的关系。因此 腾讯连连 需获取以下权限: + · WiFi名称、WiFi密码\n· 手机IP地址 + 您将授权以下个人信息与微信账号绑定: + · 昵称、头像、地区、性别\n· 设备信息 + + + diff --git a/build.gradle b/build.gradle index 94fe5285a..792f8fe74 100644 --- a/build.gradle +++ b/build.gradle @@ -26,9 +26,7 @@ allprojects { url "https://maven.aliyun.com/repository/public" } // SNAPSHOT - maven { - url "https://oss.sonatype.org/content/repositories/snapshots" - } + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } //左滑删除 maven { url "https://jitpack.io" diff --git a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/consts/RequestCode.kt b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/consts/RequestCode.kt index de69df39e..c687bb0a3 100644 --- a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/consts/RequestCode.kt +++ b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/consts/RequestCode.kt @@ -68,7 +68,7 @@ object RequestCode { const val get_bind_device_token = 3012 const val check_device_bind_token_state = 3013 const val trtc_call_device = 3015 - + const val all_device = 3016 /*************设备接口结束**************/ /*************云端定时接口开始**************/ diff --git a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/entity/VirtualBindDevice.kt b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/entity/VirtualBindDevice.kt new file mode 100644 index 000000000..38e5ea78e --- /dev/null +++ b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/entity/VirtualBindDevice.kt @@ -0,0 +1,10 @@ +package com.tencent.iot.explorer.link.core.auth.entity + +class VirtualBindDevice { + + var userId = "" + var productId = "" + var deviceName = "" + var deviceId = "" + var platformId = "" +} \ No newline at end of file diff --git a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/entity/VirtualBindDeviceList.kt b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/entity/VirtualBindDeviceList.kt new file mode 100644 index 000000000..15adf23f0 --- /dev/null +++ b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/entity/VirtualBindDeviceList.kt @@ -0,0 +1,7 @@ +package com.tencent.iot.explorer.link.core.auth.entity + +class VirtualBindDeviceList { + var requestId = "" + var totalCount = 0 + var virtualBindDeviceList: MutableList = ArrayList() +} \ No newline at end of file diff --git a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/impl/DeviceImpl.kt b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/impl/DeviceImpl.kt index c168cade9..48881e87e 100644 --- a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/impl/DeviceImpl.kt +++ b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/impl/DeviceImpl.kt @@ -140,4 +140,9 @@ interface DeviceImpl { */ fun trtcCallDevice(deviceId: String, callback: MyCallback) -} \ No newline at end of file + /** + * 获取所有设备 + */ + fun allDevices(token: String, platformId: String, offset: Int, limit: Int, callback: MyCallback) + + } \ No newline at end of file diff --git a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/service/DeviceService.kt b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/service/DeviceService.kt index 424077322..44ae8c918 100644 --- a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/service/DeviceService.kt +++ b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/service/DeviceService.kt @@ -147,6 +147,18 @@ internal class DeviceService : BaseService(), DeviceImpl { }) } + /** + * 获取所有设备 + */ + override fun allDevices(token: String, platformId: String, offset: Int, limit: Int, callback: MyCallback) { + val param = tokenParams("AppGetVirtualBindDeviceList") + param["BindPlatformId"] = platformId + param["Offset"] = offset + param["Limit"] = limit + param["AccessToken"] = token + tokenPost(param, callback, RequestCode.all_device) + } + /** * 获取设备在线状态 */ diff --git a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/socket/JWebSocketClient.kt b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/socket/JWebSocketClient.kt index 7c8172f5f..7f04bf62f 100644 --- a/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/socket/JWebSocketClient.kt +++ b/sdk/explorer-link-android/src/main/java/com/tencent/iot/explorer/link/core/auth/socket/JWebSocketClient.kt @@ -1,6 +1,7 @@ package com.tencent.iot.explorer.link.core.auth.socket import com.tencent.iot.explorer.link.core.auth.socket.callback.ConnectionCallback +import com.tencent.iot.explorer.link.core.log.L import org.java_websocket.client.WebSocketClient import org.java_websocket.handshake.ServerHandshake import java.net.URI @@ -16,6 +17,7 @@ class JWebSocketClient(serverUri: URI, handler: DispatchMsgHandler, connectionCa } override fun onClose(code: Int, reason: String, remote: Boolean) { + L.e("onClose code:$code, reason:$reason, remote:$remote") disconnect() } @@ -26,6 +28,7 @@ class JWebSocketClient(serverUri: URI, handler: DispatchMsgHandler, connectionCa } override fun onError(ex: Exception?) { + L.e("onError exception:${ex?.message}") isConnected = true disconnect() } diff --git a/sdk/explorer-link-rtc/README.md b/sdk/explorer-link-rtc/README.md index a9702c314..a982c0c0b 100644 --- a/sdk/explorer-link-rtc/README.md +++ b/sdk/explorer-link-rtc/README.md @@ -41,3 +41,16 @@ TRTCVideoCallActivity.startBeingCall(Context context, RoomKey roomKey, String be * 首页轮询TRTC设备状态查看App是否被呼叫 可参考app module下的[requestDeviceList](https://github.com/tencentyun/iot-link-android/blob/master/app/src/main/java/com/tencent/iot/explorer/link/App.kt)接口 + +## 通话流程梳理 +##### 连连 APP 呼叫设备端 + +时序图: + +![UserCallDeviceUML](media/UserCallDeviceUML.jpg) + +##### 设备端呼叫连连 APP + +时序图: + +![UserCallDeviceUML](media/DeviceCallUserUML.jpg) \ No newline at end of file diff --git a/sdk/explorer-link-rtc/build.gradle b/sdk/explorer-link-rtc/build.gradle index 573fa59dd..2d582ad4a 100644 --- a/sdk/explorer-link-rtc/build.gradle +++ b/sdk/explorer-link-rtc/build.gradle @@ -42,7 +42,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.squareup.retrofit2:converter-gson:2.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'com.tencent.liteav:LiteAVSDK_TRTC:8.1.9717' + implementation 'com.tencent.liteav:LiteAVSDK_TRTC:9.2.10641' implementation 'com.squareup.picasso:picasso:2.71828' implementation 'com.alibaba:fastjson:1.2.73' diff --git a/sdk/explorer-link-rtc/media/DeviceCallUserUML.jpg b/sdk/explorer-link-rtc/media/DeviceCallUserUML.jpg new file mode 100644 index 000000000..96f3750b9 Binary files /dev/null and b/sdk/explorer-link-rtc/media/DeviceCallUserUML.jpg differ diff --git a/sdk/explorer-link-rtc/media/UserCallDeviceUML.jpg b/sdk/explorer-link-rtc/media/UserCallDeviceUML.jpg new file mode 100644 index 000000000..ad774df61 Binary files /dev/null and b/sdk/explorer-link-rtc/media/UserCallDeviceUML.jpg differ diff --git "a/sdk/video-link-android/doc/\350\256\276\345\244\207\344\270\216APP\344\272\244\344\272\222\346\214\207\345\274\225.md" "b/sdk/video-link-android/doc/\350\256\276\345\244\207\344\270\216APP\344\272\244\344\272\222\346\214\207\345\274\225.md" index 47fc0c2ba..73c3e4b5b 100644 --- "a/sdk/video-link-android/doc/\350\256\276\345\244\207\344\270\216APP\344\272\244\344\272\222\346\214\207\345\274\225.md" +++ "b/sdk/video-link-android/doc/\350\256\276\345\244\207\344\270\216APP\344\272\244\344\272\222\346\214\207\345\274\225.md" @@ -2,9 +2,9 @@ ## 基于信令进行双向交互 ### 信令使用场景 -* app需要向ipc设备发送控制(镜头移动等)、查询(录像列表等)等命令以操控、获取ip设备信息等需求可以使用信令来完成 +* app需要向ipc设备发送控制(镜头移动等)、查询(录像列表等)、查询设备状态是否可响应直播、获取ip设备信息 等需求可以使用信令来完成 -### 接口及命令格式 +### 接口及命令格式(c 层与 native 层接口名称略有区别) #### SDK信令接口 ##### 同步方式 * 接口描述:同步方式发送信令消息给camera设备并等待回复。同步阻塞方式。 @@ -47,8 +47,70 @@ int postCommandRequestWithAsync(const char *id, const unsigned char *command, si #### ipc设备 * 设备查询(channel固定为0) - * 查询设备本地录像列表:`action=inner_define&channel=0&cmd=get_record_index` + * 查询设备本地录像列表: + * 按时间查询 `action=inner_define&channel=0&cmd=get_record_index&start_time=000&end_time=111` + + ``` + 参数说明:start_time 和 end_time 秒为单位,差值不得小于5s,UNIX时间戳 + 设备返回的json结构: + { + "video_list":[ + { + "start_time":"<unix时间戳>", + "end_time":"<unix时间戳>" + }, + { + "start_time":"<unix时间戳>", + "end_time":"<unix时间戳>" + } + ] + } + ``` + * 按月查询 `action=inner_define&channel=0&cmd=get_month_record&time=yyyymm` + + ``` + 参数说明: + yyyymm中前四位是年份,后两位是月份; + xxxx:表示32位的数字,从低位到高位每一比特代表月份的第几天是否有录像;例如:8320(0010000010000000)表示8号和14号有录像; + 设备返回的json结构: + {"video_list":"xxxx"} + ``` + * 暂停回放 `action=inner_define&channel=xxx&cmd=playback_pause` + + ``` + 设备返回的json结构: + {"status":"code"} + ``` + * 继续回放 `action=inner_define&channel=xxx&cmd=playback_resume` + + ``` + 设备返回的json结构: + {"status":"code"} + ``` + * 录像进度条滑动 `action=inner_define&channel=xxx&cmd=playback_seek&time=ssss` + + ``` + 参数说明:ssss是UNIX时间戳,单位s + 设备返回的json结构: + {"status":"code"} + ``` + + * 获取ipc设备状态,判断是否可以请求视频流(type区分直播(live)和对讲(voice)):`action=inner_define&channel=0&cmd=get_device_st&type=(voice/live)&quality=standard` + + ``` + 参数说明:app_connect_num 表示:已连接到设备的APP数量 + 返回的json结构: + [{"status":"code","appConnectNum":"2"}] + ``` + | 取值 | 含义 | + |:-|:-| + | 0 | 接收请求 | + | 1 | 拒绝请求 | + | 404 | 错误请求 | + | 405 | 连接APP数量超过最大连接数 | + | 406 | 信令不支持 | + * 云台控制信令(channel固定为0) * 控制ipc左移:`action=user_define&channel=0&cmd=ptz_left` @@ -59,7 +121,23 @@ int postCommandRequestWithAsync(const char *id, const unsigned char *command, si #### nvr设备 * 设备查询:返回`channel`和`devicename` * 查询nvr设备子设备:`action=inner_define&cmd=get_nvr_list&nvr=$nvrname` - * 查询设备本地录像列表:`action=inner_define&channel=xxx&cmd=get_record_index` + + ``` + 返回的json结构: + [ +    {"DeviceName":"name1", +      "Channel":"1", +      "Online":"0" + }, +   {"DeviceName":"name2", +      "Channel":"2", +      "Online":"1" + } + ] + + ``` + + * 查询设备本地录像列表:`action=inner_define&channel=xxx&cmd=get_record_index&start_time=000&end_time=111` * 获取ipc设备状态,判断是否可以请求视频流(type区分直播(live)和对讲(voice)):`action=inner_define&channel=xxx&cmd=get_device_st&type=(voice/live)&quality=standard` * 云台控制信令(channel通过查询指令获取) @@ -70,6 +148,7 @@ int postCommandRequestWithAsync(const char *id, const unsigned char *command, si #### 使用示例 * ipc设备 + ```shell /* 通过云台控制ipc左移 */ char ipc_ctl_cmd[] = "action=user_define&channel=0&cmd=ptz_left"; @@ -82,6 +161,7 @@ postCommandRequestWithAsync($id, ipc_ctl_cmd, strlen(ipc_ctl_cmd)); ``` * nvr设备(nvr设备发送控制信令前需发送查询信令获取channel) + ```shell /* 查询nvr设备子设备 */ char nvr_get_cmd[] = "action=inner_define&cmd=get_nvr_list"; @@ -110,9 +190,11 @@ postCommandRequestWithAsync($id, nvr_ctl_cmd, strlen(nvr_ctl_cmd)); #### SDK语音对讲及直播接口 ##### 语音对讲 * 接口描述:启动向camera设备发送语音或自定义数据服务。异步非阻塞方式。 + ``` void *runSendService(const char *id, const char *params, bool crypto) ``` + | 参数 | 类型 | 描述 | 输入/输出 | |:-|:-|:-|:-| | id | const char * | 目标camera在app端的唯一标识符 | 输入 | diff --git a/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/http/VideoHttpUtil.kt b/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/http/VideoHttpUtil.kt index c445d1ba4..9785e348e 100644 --- a/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/http/VideoHttpUtil.kt +++ b/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/http/VideoHttpUtil.kt @@ -53,7 +53,7 @@ object VideoHttpUtil { // 添加通用header参数 for ((key, value) in headerParams.entries) { - addRequestProperty(key, value.toString() + "") + addRequestProperty(key, (value.toString() + "").replace("\n", "")) } outputStream?.run { diff --git a/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/util/audio/AudioRecordUtil.java b/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/util/audio/AudioRecordUtil.java index 2c6186767..b4caf0300 100644 --- a/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/util/audio/AudioRecordUtil.java +++ b/sdk/video-link-android/src/main/java/com/tencent/iot/video/link/util/audio/AudioRecordUtil.java @@ -61,7 +61,9 @@ private void reset() { */ public void stop() { recorderState = false; - audioRecord.stop(); + if (audioRecord != null) { + audioRecord.stop(); + } audioRecord = null; pcmEncoder = null; flvPacker = null; diff --git a/sdk/video-link-android/src/test/java/com/tencent/iot/explorer/video/SignForV3Test.kt b/sdk/video-link-android/src/test/java/com/tencent/iot/explorer/video/SignForV3Test.kt new file mode 100644 index 000000000..f5a6e9dae --- /dev/null +++ b/sdk/video-link-android/src/test/java/com/tencent/iot/explorer/video/SignForV3Test.kt @@ -0,0 +1,169 @@ +package com.tencent.iot.explorer.video + +import android.util.Log +import com.alibaba.fastjson.JSON +import junit.framework.TestCase +import org.junit.Test +import java.lang.Long +import java.nio.charset.Charset +import java.security.InvalidKeyException +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.text.SimpleDateFormat +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.and + +class SignForV3Test { + + private var secretId = "YourSecretId" + private var secretKey = "YourSecretKey" + private var VIDEO_SERVICE = "iotvideo" // video + + companion object { + val UTF8: Charset = Charset.forName("UTF-8") + + val REST_HOST_URL = ".tencentcloudapi.com" + } + + @Test + fun testSign() { + try { + var headerParams = commonHeaderParams("DescribeCloudStorageDate", "2020-12-15") + val param = TreeMap() + param["ProductId"] = "productId" + param["DeviceName"] = "devName" + val authorization = generateAuthorization(VIDEO_SERVICE, headerParams, param) + println(authorization) + TestCase.assertTrue(true) + } catch (e: java.lang.Exception) { + e.printStackTrace() + TestCase.fail() + } + } + + fun commonHeaderParams(action: String, version: String): HashMap { + val param = HashMap() + param["X-TC-Action"] = action + param["X-TC-Version"] = version + param["X-TC-Region"] = "ap-guangzhou" + param["X-TC-Timestamp"] = (System.currentTimeMillis() / 1000).toString() + return param + } + + open fun generateAuthorization( + service: String, + headers: Map, + param: TreeMap + ): String? { + val algorithm = "TC3-HMAC-SHA256" + val timestamp: String = headers?.get("X-TC-Timestamp") as String + val sdf = SimpleDateFormat("yyyy-MM-dd") + // 注意时区,否则容易出错 + sdf.timeZone = TimeZone.getTimeZone("UTC") + val date = + sdf.format(Date(Long.valueOf(timestamp + "000"))) + + // ************* 步骤 1:拼接规范请求串 ************* + val httpRequestMethod = "POST" + val canonicalUri = "/" + val canonicalQueryString = "" + val canonicalHeadersBuilder = "content-type:application/json; charset=utf-8\nhost:${service}${REST_HOST_URL}\n" + val signedHeadersBuilder = "content-type;host" + val canonicalHeaders = canonicalHeadersBuilder + val signedHeaders = signedHeadersBuilder.toLowerCase() + + // 将Extra参数加到待签名字符串中,否则会签名失败 + var payload: String? = toJson(param) + val hashedRequestPayload: String? = payload?.let { sha256Hex(it) } + val canonicalRequest = "${httpRequestMethod}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${hashedRequestPayload}" + println(canonicalRequest) + + // ************* 步骤 2:拼接待签名字符串 ************* + val credentialScope = "$date/$service/tc3_request" + val hashedCanonicalRequest: String? = sha256Hex(canonicalRequest) + val stringToSign = + """ + $algorithm + $timestamp + $credentialScope + $hashedCanonicalRequest + """.trimIndent() + println(stringToSign) + + // ************* 步骤 3:计算签名 ************* SecretKey + val secretDate: ByteArray = hmac256( + ("TC3" + secretKey).toByteArray(UTF8), + date + ) + val secretService: ByteArray = hmac256(secretDate, service) + val secretSigning: ByteArray = hmac256( + secretService, + "tc3_request" + ) + val byteArray: ByteArray = hmac256(secretSigning, stringToSign) + val signature: String? = encodeHexString(byteArray) + + // ************* 步骤 4:拼接 Authorization ************* SecretId + return algorithm + " " + "Credential=" + secretId + "/" + credentialScope + ", " + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature + } + + /** + * 功能描述:把指定的java对象转为json数据 + */ + fun toJson(clazz: Any?): String? { + return try { + JSON.toJSONString(clazz) + } catch (e: Exception) { + Log.e("ToJsonEntity", "fastjson转换错误:${e.message} \n原因是:${e.cause}") + null + } + } + + open fun sha256Hex(s: String): String? { + var md: MessageDigest? = null + var d: ByteArray? = null + try { + md = MessageDigest.getInstance("SHA-256") + d = md.digest(s.toByteArray(UTF8)) + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return d?.let { encodeHexString(it)?.toLowerCase() } + } + + open fun encodeHexString(byteArray: ByteArray): String? { + val hexStringBuffer = StringBuffer() + for (i in byteArray.indices) { + hexStringBuffer.append( + byteToHex( + byteArray[i] + ) + ) + } + return hexStringBuffer.toString() + } + + open fun byteToHex(num: Byte): String? { + val hexDigits = CharArray(2) + hexDigits[0] = Character.forDigit((num.toInt() shr 4) and 0xF, 16) + hexDigits[1] = Character.forDigit((num and 0xF).toInt(), 16) + return String(hexDigits) + } + + open fun hmac256(key: ByteArray, msg: String): ByteArray { + var mac: Mac? = null + try { + mac = Mac.getInstance("HmacSHA256") + val secretKeySpec = + SecretKeySpec(key, mac.algorithm) + mac.init(secretKeySpec) + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } catch (e: InvalidKeyException) { + e.printStackTrace() + } + return mac!!.doFinal(msg.toByteArray(UTF8)) + } +} \ No newline at end of file diff --git a/sdkdemo/src/main/AndroidManifest.xml b/sdkdemo/src/main/AndroidManifest.xml index ef1620df1..35f0a5a05 100644 --- a/sdkdemo/src/main/AndroidManifest.xml +++ b/sdkdemo/src/main/AndroidManifest.xml @@ -51,6 +51,7 @@ + diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/customView/CalendarView.java b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/customView/CalendarView.java index e4de2ec3e..75ad1f575 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/customView/CalendarView.java +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/customView/CalendarView.java @@ -216,7 +216,7 @@ public boolean onTouchEvent(MotionEvent event) { int upY = (int) event.getY(); int diffX = Math.abs(upX - mDownX); int diffY = Math.abs(upY - mDownY); - if(diffX < mSlop && diffY < mSlop){ + if(diffX < mSlop && diffY < mSlop && mColumnWidth > 0 && mRowHeight > 0){ int column = upX / mColumnWidth; int row = upY / mRowHeight; onClick(mDays[row][column]); diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/CommonUtils.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/CommonUtils.kt index 1ded950b1..28443daff 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/CommonUtils.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/CommonUtils.kt @@ -7,10 +7,10 @@ import android.net.Uri import android.os.Environment import android.provider.MediaStore import com.tencent.iot.explorer.link.demo.R -import com.tencent.iot.explorer.link.demo.core.entity.TimeBlock -import com.tencent.iot.explorer.link.demo.video.utils.ToastDialog import com.tencent.iot.explorer.link.demo.common.customView.CalendarView import com.tencent.iot.explorer.link.demo.common.customView.timeline.TimeBlockInfo +import com.tencent.iot.explorer.link.demo.core.entity.TimeBlock +import com.tencent.iot.explorer.link.demo.video.utils.ToastDialog import java.io.File import java.text.ParseException import java.text.SimpleDateFormat @@ -109,5 +109,40 @@ class CommonUtils { // localContentValues.put("_size", paramLong) return localContentValues } + + fun formatedDurationMilli(duration: Long): String { + return if (duration >= 1000) { + String.format(Locale.US, "%.2f sec", duration.toFloat() / 1000) + } else { + String.format(Locale.US, "%d msec", duration) + } + } + + fun formatedSpeed(bytes: Long, elapsed_milli: Long): String { + if (elapsed_milli <= 0) { + return "0 B/s" + } + if (bytes <= 0) { + return "0 B/s" + } + val bytes_per_sec = bytes.toFloat() * 1000f / elapsed_milli + return if (bytes_per_sec >= 1000 * 1000) { + String.format(Locale.US, "%.2f MB/s", bytes_per_sec / 1000 / 1000) + } else if (bytes_per_sec >= 1000) { + String.format(Locale.US, "%.1f KB/s", bytes_per_sec / 1000) + } else { + String.format(Locale.US, "%d B/s", bytes_per_sec.toLong()) + } + } + + fun formatedSize(bytes: Long): String? { + return if (bytes >= 100 * 1000) { + String.format(Locale.US, "%.2f MB", bytes.toFloat() / 1000 / 1000) + } else if (bytes >= 100) { + String.format(Locale.US, "%.1f KB", bytes.toFloat() / 1000) + } else { + String.format(Locale.US, "%d B", bytes) + } + } } } \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/ImageSelect.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/ImageSelect.kt index 75e46fa4d..74c0299bf 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/ImageSelect.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/common/util/ImageSelect.kt @@ -1,6 +1,7 @@ package com.tencent.iot.explorer.link.demo.common.util import android.app.Activity +import android.content.ContentValues import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -118,7 +119,11 @@ object ImageSelect { context?.let { if (!file.exists()) return@let - MediaStore.Images.Media.insertImage(it.getContentResolver(), file.absolutePath, file.name, null) + + val values = ContentValues() + values.put(MediaStore.Images.Media.DATA, file.absolutePath) + values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + it.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) it.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))) } return file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/activity/ShowAllDeviceActivity.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/activity/ShowAllDeviceActivity.kt new file mode 100644 index 000000000..9ff9eca87 --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/activity/ShowAllDeviceActivity.kt @@ -0,0 +1,66 @@ +package com.tencent.iot.explorer.link.demo.core.activity + +import android.text.TextUtils +import android.widget.Toast +import com.alibaba.fastjson.JSON +import com.tencent.iot.explorer.link.core.auth.IoTAuth +import com.tencent.iot.explorer.link.core.auth.callback.MyCallback +import com.tencent.iot.explorer.link.core.auth.entity.VirtualBindDeviceList +import com.tencent.iot.explorer.link.core.auth.response.BaseResponse +import com.tencent.iot.explorer.link.demo.BaseActivity +import com.tencent.iot.explorer.link.demo.R +import com.tencent.iot.explorer.link.demo.common.log.L +import kotlinx.android.synthetic.main.activity_show_all_device.* +import kotlinx.android.synthetic.main.menu_back_layout.* + +class ShowAllDeviceActivity : BaseActivity(), MyCallback { + override fun getContentView(): Int { + return R.layout.activity_show_all_device + } + + override fun initView() { + tv_title.text = getString(R.string.show_all_device) + } + + override fun setListener() { + iv_back.setOnClickListener { finish() } + btn_search.setOnClickListener { + tv_all_devices.text = "" + var token = IoTAuth.user.Token + var platformId = ev_platformId_2_search.text.toString() + if (!TextUtils.isEmpty(ev_token_2_search.text.toString())) { + token = ev_token_2_search.text.toString() + } + + IoTAuth.deviceImpl.allDevices(token, platformId, 0, 99, this) + } + } + + override fun fail(msg: String?, reqCode: Int) { + L.e(msg ?: "") + } + + override fun success(response: BaseResponse, reqCode: Int) { + + runOnUiThread { + if (response.code == 0) { + var virtualBindDeviceList = JSON.parseObject(response.data.toString(), VirtualBindDeviceList::class.java) + virtualBindDeviceList?.let { + if (it.totalCount <= 0) { + Toast.makeText(this@ShowAllDeviceActivity, R.string.no_devices, Toast.LENGTH_SHORT).show() + return@let + } + var content2Show = "" + + if (it.virtualBindDeviceList.size <= 0) return@let + for (item in it.virtualBindDeviceList) { + content2Show += item.deviceName + "\n" + } + tv_all_devices.text = content2Show + } + } else { + Toast.makeText(this@ShowAllDeviceActivity, response?.msg, Toast.LENGTH_SHORT).show() + } + } + } +} diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/entity/DevTimeBlock.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/entity/DevTimeBlock.kt new file mode 100644 index 000000000..357352bd8 --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/entity/DevTimeBlock.kt @@ -0,0 +1,6 @@ +package com.tencent.iot.explorer.link.demo.core.entity + +class DevTimeBlock { + var start_time = 0L + var end_time = 0L +} \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/entity/DevVideoHistory.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/entity/DevVideoHistory.kt new file mode 100644 index 000000000..18ec949d5 --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/entity/DevVideoHistory.kt @@ -0,0 +1,19 @@ +package com.tencent.iot.explorer.link.demo.core.entity + +class DevVideoHistory { + var video_list: MutableList? = null + + fun getTimeBlocks(): MutableList { + video_list?.let { + var ret: MutableList = ArrayList() + for (i in it.indices) { + var item = TimeBlock() + item.StartTime = it[i].start_time + item.EndTime = it[i].end_time + ret.add(item) + } + return ret + } + return ArrayList() + } +} \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/fragment/DeviceFragment.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/fragment/DeviceFragment.kt index cafc4fbb3..5803a180d 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/fragment/DeviceFragment.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/core/fragment/DeviceFragment.kt @@ -1,5 +1,6 @@ package com.tencent.iot.explorer.link.demo.core.fragment +import android.content.Intent import android.util.Log import android.view.View import androidx.recyclerview.widget.LinearLayoutManager @@ -22,6 +23,7 @@ import com.tencent.iot.explorer.link.demo.core.activity.ControlPanelActivity import com.tencent.iot.explorer.link.demo.core.adapter.OnItemListener import com.tencent.iot.explorer.link.demo.core.holder.BaseHolder import com.tencent.iot.explorer.link.demo.common.customView.MyDivider +import com.tencent.iot.explorer.link.demo.core.activity.ShowAllDeviceActivity import kotlinx.android.synthetic.main.fragment_device.* class DeviceFragment : BaseFragment(), MyCallback { @@ -60,6 +62,9 @@ class DeviceFragment : BaseFragment(), MyCallback { } private fun setListener() { + tv_show_all_device.setOnClickListener { + jumpActivity(ShowAllDeviceActivity::class.java) + } tv_add_device.setOnClickListener { jumpActivity(AddDeviceActivity::class.java) } diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/Command.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/Command.kt index 8923a93dc..5c71b8658 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/Command.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/Command.kt @@ -1,5 +1,7 @@ package com.tencent.iot.explorer.link.demo.video +import java.util.* + // 信令 class Command { companion object { @@ -45,5 +47,39 @@ class Command { fun getTwoWayRadio(channel: Int): String { return "channel=${channel}" } + + fun getMonthDates(channel: Int, time: String): String { + // yyyymm 年月 + return "action=inner_define&channel=${channel}&cmd=get_month_record&time=${time}" + } + + fun getDayTimeBlocks(channel: Int, date: Date): String { + var dateStart = Date(date.time) + dateStart.hours = 0 + dateStart.minutes = 0 + dateStart.seconds = 0 + var dateEnd = Date(date.time) + dateEnd.hours = 23 + dateEnd.minutes = 59 + dateEnd.seconds = 59 + return "action=inner_define&channel=${channel}&cmd=get_record_index" + + "&start_time=${dateStart.time/1000}&end_time=${dateEnd.time/1000}" + } + + fun getLocalVideoUrl(channel: Int, startTime: Long, endTime: Long): String { + return "ipc.flv?action=playback&channel=${channel}&start_time=${startTime}&end_time=${endTime}" + } + + fun pauseLocalVideoUrl(channel: Int): String { + return "action=inner_define&channel=${channel}&cmd=playback_pause" + } + + fun resumeLocalVideoUrl(channel: Int): String { + return "action=inner_define&channel=${channel}&cmd=playback_resume" + } + + fun seekLocalVideo(channel: Int, offset: Long): String { + return "action=inner_define&channel=${channel}&cmd=playback_seek&time=${offset}}" + } } } \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/CommandResp.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/CommandResp.kt new file mode 100644 index 000000000..65282c90e --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/CommandResp.kt @@ -0,0 +1,5 @@ +package com.tencent.iot.explorer.link.demo.video + +class CommandResp { + var status = -1 +} \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/BottomPlaySpeedDialog.java b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/BottomPlaySpeedDialog.java new file mode 100644 index 000000000..9af2259c4 --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/BottomPlaySpeedDialog.java @@ -0,0 +1,108 @@ +package com.tencent.iot.explorer.link.demo.video.playback; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.tencent.iot.explorer.link.demo.R; +import com.tencent.iot.explorer.link.demo.video.utils.IosCenterStyleDialog; + +import java.util.ArrayList; +import java.util.List; + +public class BottomPlaySpeedDialog extends IosCenterStyleDialog { + + private TextView cancel; + private RecyclerView options; + private ConstraintLayout outsideLayout; + private GridOptionsAdapter adapter; + private Context context; + private List content = new ArrayList(); + private TextView title; + private View titleLine; + private String titleStr = ""; + private int index = -1; + + public BottomPlaySpeedDialog(Context context, int index) { + this(context); + this.index = index; + } + + public BottomPlaySpeedDialog(Context context) { + super(context, R.layout.popup_grid_options_layout); + this.context = context; + content.add(context.getString(R.string.play_speed_0_5)); + content.add(context.getString(R.string.play_speed_0_75)); + content.add(context.getString(R.string.play_speed_1)); + content.add(context.getString(R.string.play_speed_1_25)); + content.add(context.getString(R.string.play_speed_1_5)); + content.add(context.getString(R.string.play_speed_2)); + } + + public BottomPlaySpeedDialog(Context context, String title, int index) { + this(context); + titleStr = title; + this.index = index; + } + + + @Override + public void initView() { + outsideLayout = view.findViewById(R.id.outside_dialog_layout); + cancel = view.findViewById(R.id.tv_cancel); + options = view.findViewById(R.id.lv_options); + title = view.findViewById(R.id.tv_title); + titleLine = view.findViewById(R.id.v_space_3); + + if (TextUtils.isEmpty(titleStr)) { + title.setVisibility(View.GONE); + titleLine.setVisibility(View.GONE); + } + title.setText(titleStr); + + adapter = new GridOptionsAdapter(this.content, index); + adapter.setOnItemClicked(onItemClicked); + GridLayoutManager layoutManager = new GridLayoutManager(this.context, 4); + options.setLayoutManager(layoutManager); + options.setAdapter(adapter); + + cancel.setOnClickListener(onClickListener); + outsideLayout.setOnClickListener(onClickListener); + } + + private GridOptionsAdapter.OnItemClicked onItemClicked = new GridOptionsAdapter.OnItemClicked() { + @Override + public void onItemClicked(int postion, String option) { + if (onDismisListener != null) { + onDismisListener.onItemClicked(postion); + } + dismiss(); + } + }; + + private View.OnClickListener onClickListener = v -> { + switch (v.getId()) { } + dismiss(); + }; + + private OnDismisListener onDismisListener; + + public interface OnDismisListener { + void onItemClicked(int pos); + } + + public void setOnDismisListener(OnDismisListener onDismisListener) { + this.onDismisListener = onDismisListener; + } + + public void show() { + super.show(); + } + +} diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/CalendarDialog.java b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/CalendarDialog.java index 9e88ebeec..0bbb6af3f 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/CalendarDialog.java +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/CalendarDialog.java @@ -23,21 +23,38 @@ public class CalendarDialog extends IosCenterStyleDialog implements View.OnClick private ConstraintLayout outsideLayout; private ConstraintLayout insideLayout; private OnClickedListener onClickedListener; + private OnMonthChanged onMonthChanged; private List allDate2Tag; public List getAllDate2Tag() { return this.allDate2Tag; } + public void addTagDates(List tagDates) { + allDate2Tag.addAll(tagDates); + if (calendar != null) { + calendar.invalidate(); + } + } + public void setOnClickedListener(OnClickedListener onClickedListener) { this.onClickedListener = onClickedListener; } + public void setOnMonthChanged(OnMonthChanged onMonthChanged) { + this.onMonthChanged = onMonthChanged; + } + public CalendarDialog(Context context, List allDate2Tag) { super(context, R.layout.popup_calendar_layout); this.allDate2Tag = allDate2Tag; } + public CalendarDialog(Context context, List allDate2Tag, OnMonthChanged onMonthChanged) { + this(context, allDate2Tag); + this.onMonthChanged = onMonthChanged; + } + @Override public void initView() { calendar = view.findViewById(R.id.calendar_view); @@ -78,6 +95,9 @@ public void last(){ private void setCurDate() { month2Show.setText(getContext().getString(R.string.year_and_month_unit, String.valueOf(calendar.getYear()), String.valueOf((calendar.getMonth() + 1)))); + if (onMonthChanged != null) { + onMonthChanged.onMonthChanged(this, String.format("%04d-%02d", calendar.getYear(), calendar.getMonth() + 1)); + } } @Override @@ -119,4 +139,8 @@ public interface OnClickedListener { void onOkClickedWithoutDateChecked(); void onOkClickedCheckedDateWithoutData(); } + + public interface OnMonthChanged { + void onMonthChanged(CalendarDialog dlg, String dateStr); + } } diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/GridOptionsAdapter.java b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/GridOptionsAdapter.java new file mode 100644 index 000000000..d5869a616 --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/GridOptionsAdapter.java @@ -0,0 +1,79 @@ +package com.tencent.iot.explorer.link.demo.video.playback; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tencent.iot.explorer.link.demo.R; + +import java.util.List; + +public class GridOptionsAdapter extends RecyclerView.Adapter { + private List options; + private int index = -1; + + class ViewHolder extends RecyclerView.ViewHolder { + View layout; + TextView option; + + ViewHolder(View view) { + super(view); + layout = view; + option = view.findViewById(R.id.tv_option); + } + + } + + public GridOptionsAdapter(List options, int index) { + this.options = options; + this.index = index; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_grid_option, parent, false); + final ViewHolder holder = new ViewHolder(view); + + holder.layout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int position = holder.getAdapterPosition(); + if (onItemClicked != null) { + onItemClicked.onItemClicked(position, options.get(position)); + } + } + }); + return holder; + } + + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.option.setText(options.get(position)); + if (position == index) { + holder.option.setBackgroundResource(R.drawable.background_blue_cell); + } else { + holder.option.setBackgroundResource(R.drawable.background_gray_cell); + } + } + + @Override + public int getItemCount() { + return options.size(); + } + + public interface OnItemClicked { + void onItemClicked(int postion, String option); + } + + private OnItemClicked onItemClicked; + + public void setOnItemClicked(OnItemClicked onItemClicked) { + this.onItemClicked = onItemClicked; + } + +} diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/RightOptionsAdapter.java b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/RightOptionsAdapter.java new file mode 100644 index 000000000..18dc045b2 --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/RightOptionsAdapter.java @@ -0,0 +1,83 @@ +package com.tencent.iot.explorer.link.demo.video.playback; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tencent.iot.explorer.link.demo.R; + +import java.util.List; + +public class RightOptionsAdapter extends RecyclerView.Adapter { + private List options; + private int index = -1; + private Context context; + + class ViewHolder extends RecyclerView.ViewHolder { + View layout; + TextView option; + + ViewHolder(View view) { + super(view); + layout = view; + option = view.findViewById(R.id.tv_option); + } + + } + + public RightOptionsAdapter(Context context, List options, int index) { + this.options = options; + this.index = index; + this.context = context; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_right_option, parent, false); + final ViewHolder holder = new ViewHolder(view); + + holder.layout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int position = holder.getAdapterPosition(); + index = position; + if (onItemClicked != null) { + onItemClicked.onItemClicked(position, options.get(position)); + } + notifyDataSetChanged(); + } + }); + return holder; + } + + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.option.setText(options.get(position)); + if (position == index) { + holder.option.setTextColor(context.getResources().getColor(R.color.green_0ABF5B)); + } else { + holder.option.setTextColor(context.getResources().getColor(R.color.white)); + } + } + + @Override + public int getItemCount() { + return options.size(); + } + + public interface OnItemClicked { + void onItemClicked(int postion, String option); + } + + private OnItemClicked onItemClicked; + + public void setOnItemClicked(OnItemClicked onItemClicked) { + this.onItemClicked = onItemClicked; + } + +} diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/RightPlaySpeedDialog.java b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/RightPlaySpeedDialog.java new file mode 100644 index 000000000..362d6f351 --- /dev/null +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/RightPlaySpeedDialog.java @@ -0,0 +1,87 @@ +package com.tencent.iot.explorer.link.demo.video.playback; + +import android.content.Context; +import android.view.View; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.tencent.iot.explorer.link.demo.R; +import com.tencent.iot.explorer.link.demo.video.utils.IosCenterStyleDialog; + +import java.util.ArrayList; +import java.util.List; + +public class RightPlaySpeedDialog extends IosCenterStyleDialog { + + private ConstraintLayout outsideLayout; + private ConstraintLayout insideLayout; + private RecyclerView options; + private Context context; + private int pos = 0; + private OnDismisListener onDismisListener; + private RightOptionsAdapter adapter; + private List optionsData = new ArrayList(); + + public RightPlaySpeedDialog(Context context, int pos) { + super(context, R.layout.popup_right_options_layout); + this.context = context; + this.pos = pos; + } + + @Override + public void initView() { + outsideLayout = view.findViewById(R.id.outside_dialog_layout); + insideLayout = view.findViewById(R.id.inside_layout); + options = view.findViewById(R.id.lv_options); + + optionsData.add(context.getString(R.string.play_speed_0_5)); + optionsData.add(context.getString(R.string.play_speed_0_75)); + optionsData.add(context.getString(R.string.play_speed_1)); + optionsData.add(context.getString(R.string.play_speed_1_25)); + optionsData.add(context.getString(R.string.play_speed_1_5)); + optionsData.add(context.getString(R.string.play_speed_2)); + adapter = new RightOptionsAdapter(context, optionsData, pos); + adapter.setOnItemClicked(onItemClicked); + LinearLayoutManager layoutManager = new LinearLayoutManager(this.context); + options.setLayoutManager(layoutManager); + options.setAdapter(adapter); + + outsideLayout.setOnClickListener(onClickListener); + insideLayout.setOnClickListener(onClickListener); + } + + private RightOptionsAdapter.OnItemClicked onItemClicked = (postion, option) -> { + if (onDismisListener != null) { + onDismisListener.onItemClicked(postion); + } + dismiss(); + }; + + private View.OnClickListener onClickListener = v -> { + switch (v.getId()) { + case R.id.inside_layout: + return; + } + dismiss(); + if (onDismisListener != null) { + onDismisListener.onDismiss(); + } + }; + + public interface OnDismisListener { + void onItemClicked(int pos); + void onDismiss(); + } + + public void setOnDismisListener(OnDismisListener onDismisListener) { + this.onDismisListener = onDismisListener; + } + + public void show() { + super.show(); + } + +} diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackActivity.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackActivity.kt index 29b79a7c3..e9213a47c 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackActivity.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackActivity.kt @@ -6,24 +6,21 @@ import android.content.pm.ActivityInfo import android.os.Bundle import android.text.TextUtils import android.view.View -import android.widget.LinearLayout import androidx.constraintlayout.widget.ConstraintLayout -import androidx.viewpager.widget.ViewPager import com.alibaba.fastjson.JSON import com.google.android.material.tabs.TabLayout import com.tencent.iot.explorer.link.demo.R -import com.tencent.iot.explorer.link.demo.BaseActivity import com.tencent.iot.explorer.link.demo.VideoBaseActivity +import com.tencent.iot.explorer.link.demo.common.customView.PageAdapter import com.tencent.iot.explorer.link.demo.core.fragment.BaseFragment import com.tencent.iot.explorer.link.demo.video.DevInfo import com.tencent.iot.explorer.link.demo.video.playback.cloudPlayback.VideoCloudPlaybackFragment import com.tencent.iot.explorer.link.demo.video.playback.localPlayback.VideoLocalPlaybackFragment -import com.tencent.iot.explorer.link.demo.common.customView.PageAdapter import com.tencent.iot.video.link.consts.VideoConst import kotlinx.android.synthetic.main.activity_video_playback.* import kotlinx.android.synthetic.main.activity_video_playback.v_title import kotlinx.android.synthetic.main.activity_video_preview.* -import kotlinx.android.synthetic.main.fragment_video_cloud_playback.* +import kotlinx.android.synthetic.main.fragment_video_local_playback.layout_video import kotlinx.android.synthetic.main.title_layout.* import java.util.* @@ -46,23 +43,24 @@ class VideoPlaybackActivity : VideoBaseActivity() { mPageList.add(page1) mPageList.add(page2) mAdapter = PageAdapter(this.supportFragmentManager, mPageList) - fragment_pager.setAdapter(mAdapter) + fragment_pager.adapter = mAdapter fragment_pager.setPagingEnabled(false) - fragment_pager.setOffscreenPageLimit(2) - - val tabStrip = tab_playback.getChildAt(0) as LinearLayout - for (i in 0 until tabStrip.childCount) { - tabStrip.getChildAt(i).setOnTouchListener { v, event -> true } - } + fragment_pager.offscreenPageLimit = 2 +// // 禁止 tab 点击 +// val tabStrip = tab_playback.getChildAt(0) as LinearLayout +// for (i in 0 until tabStrip.childCount) { +// tabStrip.getChildAt(i).setOnTouchListener { v, event -> true } +// } intent.getBundleExtra(VideoConst.VIDEO_CONFIG)?.let { var jsonStr = it.getString(VideoConst.VIDEO_CONFIG) if (TextUtils.isEmpty(jsonStr)) return@let page1.devInfo = JSON.parseObject(jsonStr, DevInfo::class.java) + page2.devInfo = JSON.parseObject(jsonStr, DevInfo::class.java) pageIndex = it.getInt(VideoConst.VIDEO_PAGE_INDEX) } - fragment_pager.setCurrentItem(pageIndex) + fragment_pager.currentItem = pageIndex } override fun setListener() { @@ -71,25 +69,25 @@ class VideoPlaybackActivity : VideoBaseActivity() { override fun onTabReselected(tab: TabLayout.Tab) {} override fun onTabUnselected(tab: TabLayout.Tab) {} override fun onTabSelected(tab: TabLayout.Tab) { - fragment_pager?.setCurrentItem(tab.position) + fragment_pager?.currentItem = tab.position } }) - fragment_pager.setOnPageChangeListener(pageSelectListener) - page1.onOrientationChangedListener = onOrientationChangedListener page2.onOrientationChangedListener = onOrientationChangedListener + page1.onOrientationChangedListener = onOrientationChangedListener } private var onOrientationChangedListener = object : OnOrientationChangedListener { override fun onOrientationChanged(portrait: Boolean) { - var layoutParams = layout_video.layoutParams as ConstraintLayout.LayoutParams + var layoutParams = page1.layout_video.layoutParams as ConstraintLayout.LayoutParams + var localLayoutParams = page2.layout_video.layoutParams as ConstraintLayout.LayoutParams var fitSize = 0 var visibility = View.VISIBLE if (portrait) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE visibility = View.GONE fitSize = ConstraintLayout.LayoutParams.MATCH_PARENT } @@ -99,19 +97,10 @@ class VideoPlaybackActivity : VideoBaseActivity() { layoutParams.height = fitSize layoutParams.width = fitSize - layout_video.layoutParams = layoutParams - } - } - - private var pageSelectListener = object : ViewPager.OnPageChangeListener{ - override fun onPageScrollStateChanged(state: Int) {} - - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - tab_playback?.setScrollPosition(position, positionOffset, true) - } - - override fun onPageSelected(position: Int) { - tab_playback.getTabAt(position)?.select() + localLayoutParams.height = fitSize + localLayoutParams.width = fitSize + page1.layout_video.layoutParams = layoutParams + page2.layout_video.layoutParams = localLayoutParams } } diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackBaseFragment.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackBaseFragment.kt index b944f5adf..754867820 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackBaseFragment.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/VideoPlaybackBaseFragment.kt @@ -5,7 +5,13 @@ import androidx.constraintlayout.widget.ConstraintLayout import com.tencent.iot.explorer.link.demo.R import com.tencent.iot.explorer.link.demo.core.fragment.BaseFragment import com.tencent.iot.explorer.link.demo.common.customView.CalendarView -import kotlinx.android.synthetic.main.fragment_video_cloud_playback.* +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.layout_control +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.layout_video +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.palayback_video +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.playback_control +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.playback_control_orientation +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.v_space +import kotlinx.android.synthetic.main.fragment_video_local_playback.* import kotlinx.coroutines.* import java.text.SimpleDateFormat import java.util.* @@ -22,11 +28,14 @@ open class VideoPlaybackBaseFragment: BaseFragment(), CoroutineScope by MainScop } override fun startHere(view: View) { + playback_control.visibility = View.GONE + top_control_layout?.visibility = View.GONE playback_control_orientation.setOnClickListener { portrait = !portrait onOrientationChangedListener?.onOrientationChanged(portrait) var moreSpace = 12 var marginWidth = 0 + var topMoreSpace = 12 if (portrait) { layout_control.visibility = View.VISIBLE v_space.visibility = View.VISIBLE @@ -35,34 +44,58 @@ open class VideoPlaybackBaseFragment: BaseFragment(), CoroutineScope by MainScop v_space.visibility = View.GONE moreSpace = 32 marginWidth = 73 + topMoreSpace = 32 } var btnLayoutParams = playback_control_orientation.layoutParams as ConstraintLayout.LayoutParams btnLayoutParams.bottomMargin = dp2px(moreSpace) playback_control_orientation.layoutParams = btnLayoutParams - var videoLayoutParams = palayback_video.layoutParams as ConstraintLayout.LayoutParams - videoLayoutParams.marginStart = dp2px(marginWidth) - videoLayoutParams.marginEnd = dp2px(marginWidth) - palayback_video.layoutParams = videoLayoutParams + iv_video_record?.let { + var spaceParams = iv_video_record.layoutParams as ConstraintLayout.LayoutParams + spaceParams.topMargin = dp2px(topMoreSpace) + iv_video_record.layoutParams = spaceParams + } + + videoViewNeeResize(dp2px(marginWidth), dp2px(marginWidth)) + + if (!portrait) { + iv_video_back?.visibility = View.VISIBLE + tv_video_title?.visibility = View.VISIBLE + } else { + iv_video_back?.visibility = View.GONE + tv_video_title?.visibility = View.GONE + } } layout_video.setOnClickListener { if (playback_control.visibility == View.VISIBLE) { playback_control.visibility = View.GONE + top_control_layout?.visibility = View.GONE job?.let { it.cancel() } } else { playback_control.visibility = View.VISIBLE + top_control_layout?.visibility = View.GONE // 暂时屏蔽录像/拍照/倍速的功能 + if (!portrait) { + iv_video_back?.visibility = View.VISIBLE + tv_video_title?.visibility = View.VISIBLE + } else { + iv_video_back?.visibility = View.GONE + tv_video_title?.visibility = View.GONE + } job = launch { delay(5 * 1000) playback_control.visibility = View.GONE + top_control_layout?.visibility = View.GONE } } } } + open fun videoViewNeeResize(marginStart: Int, marginEnd: Int) {} + override fun onDestroy() { super.onDestroy() cancel() diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/cloudPlayback/VideoCloudPlaybackFragment.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/cloudPlayback/VideoCloudPlaybackFragment.kt index a24de964f..9828448e4 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/cloudPlayback/VideoCloudPlaybackFragment.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/cloudPlayback/VideoCloudPlaybackFragment.kt @@ -3,9 +3,11 @@ package com.tencent.iot.explorer.link.demo.video.playback.cloudPlayback import android.media.MediaPlayer import android.net.Uri import android.text.TextUtils +import android.util.Log import android.view.View import android.widget.SeekBar import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.LinearLayoutManager import com.alibaba.fastjson.JSONArray import com.alibaba.fastjson.JSONObject @@ -31,6 +33,17 @@ import com.tencent.iot.video.link.consts.VideoRequestCode import com.tencent.iot.video.link.service.VideoBaseService import kotlinx.android.synthetic.main.activity_video_preview.* import kotlinx.android.synthetic.main.fragment_video_cloud_playback.* +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.iv_left_go +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.iv_right_go +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.iv_start +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.layout_select_date +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.pause_tip_layout +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.playback_control +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.time_line +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.tv_all_time +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.tv_current_pos +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.tv_date +import kotlinx.android.synthetic.main.fragment_video_cloud_playback.video_seekbar import kotlinx.coroutines.* import java.net.URLDecoder import java.text.SimpleDateFormat @@ -46,6 +59,18 @@ class VideoCloudPlaybackFragment: VideoPlaybackBaseFragment(), EventView, VideoC private lateinit var presenter: EventPresenter private var URL_FORMAT = "%s?starttime_epoch=%s&endtime_epoch=%s" private var seekBarJob : Job? = null + @Volatile + private var isShowing = false + + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + palayback_video?.let { + if (!isVisibleToUser && palayback_video.isPlaying) { + // 滑动该页面时,如果处于播放状态,暂停播放 + iv_start.performClick() + } + } + isShowing = isVisibleToUser + } override fun startHere(view: View) { super.startHere(view) @@ -138,6 +163,7 @@ class VideoCloudPlaybackFragment: VideoPlaybackBaseFragment(), EventView, VideoC palayback_video.setOnInfoListener(onInfoListener) video_seekbar.setOnSeekBarChangeListener(onSeekBarChangeListener) palayback_video.setOnErrorListener(onErrorListener) + pause_tip_layout.setOnClickListener { iv_start.performClick() } palayback_video.setOnCompletionListener { iv_start.setImageResource(R.mipmap.start) @@ -165,9 +191,15 @@ class VideoCloudPlaybackFragment: VideoPlaybackBaseFragment(), EventView, VideoC it.index = pos it.notifyDataSetChanged() + var endtime = "" + if (it.list.get(pos).endTime != 0L) { + endtime = (it.list.get(pos).endTime).toString() + } else { + endtime = (System.currentTimeMillis() / 1000).toString() + } var url = String.format(URL_FORMAT, presenter.getBaseUrl(), (it.list.get(pos).startTime).toString(), - (it.list.get(pos).endTime).toString()) + endtime) playVideo(url, 0) } } @@ -204,10 +236,16 @@ class VideoCloudPlaybackFragment: VideoPlaybackBaseFragment(), EventView, VideoC date?.let { for (j in 0 until timeLineView!!.timeBlockInfos.size) { var blockTime = timeLineView!!.timeBlockInfos!!.get(j) - if (blockTime.startTime.time <= date.time && blockTime.endTime.time >= date.time) { + if (blockTime.startTime.time <= date.time && (blockTime.endTime.time >= date.time || blockTime.endTime.time == 0L)) { + var endtime = "" + if (blockTime.endTime.time != 0L) { + endtime = (blockTime.endTime.time / 1000).toString() + } else { + endtime = (System.currentTimeMillis() / 1000).toString() + } var url = String.format(URL_FORMAT, baseUrl, (blockTime.startTime.time / 1000).toString(), - (blockTime.endTime.time / 1000).toString()) + endtime) var offest = date.time - blockTime.startTime.time playVideo(url, offest) @@ -261,6 +299,12 @@ class VideoCloudPlaybackFragment: VideoPlaybackBaseFragment(), EventView, VideoC iv_start.isClickable = true it.start() it.seekTo(realOffset.toInt()) + launch(Dispatchers.Main) { + delay(10) + if (!isShowing) { + iv_start.performClick() + } + } } } @@ -330,9 +374,15 @@ class VideoCloudPlaybackFragment: VideoPlaybackBaseFragment(), EventView, VideoC time_line.timeBlockInfos = dataList time_line.invalidate() + var endtime = "" + if (dataList.get(0).endTime.time != 0L) { + endtime = (dataList.get(0).endTime.time / 1000).toString() + } else { + endtime = (System.currentTimeMillis() / 1000).toString() + } var url = String.format(URL_FORMAT, baseUrl, (dataList.get(0).startTime.time / 1000).toString(), - (dataList.get(0).endTime.time / 1000).toString()) + endtime) playVideo(url, 0) } @@ -355,4 +405,11 @@ class VideoCloudPlaybackFragment: VideoPlaybackBaseFragment(), EventView, VideoC } } } + + override fun videoViewNeeResize(marginStart: Int, marginEnd: Int) { + var videoLayoutParams = palayback_video.layoutParams as ConstraintLayout.LayoutParams + videoLayoutParams.marginStart = marginStart + videoLayoutParams.marginEnd = marginEnd + palayback_video.layoutParams = videoLayoutParams + } } \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/localPlayback/VideoLocalPlaybackFragment.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/localPlayback/VideoLocalPlaybackFragment.kt index 48c2c1fb2..86e3a235d 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/localPlayback/VideoLocalPlaybackFragment.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/playback/localPlayback/VideoLocalPlaybackFragment.kt @@ -1,30 +1,514 @@ package com.tencent.iot.explorer.link.demo.video.playback.localPlayback +import android.graphics.SurfaceTexture +import android.text.TextUtils +import android.util.Log +import android.view.Surface +import android.view.TextureView import android.view.View import android.widget.SeekBar -import com.tencent.iot.explorer.link.demo.video.playback.VideoPlaybackBaseFragment -import kotlinx.android.synthetic.main.fragment_video_cloud_playback.* +import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout +import com.alibaba.fastjson.JSON +import com.alibaba.fastjson.JSONObject +import com.tencent.iot.explorer.link.core.log.L +import com.tencent.iot.explorer.link.demo.App +import com.tencent.iot.explorer.link.demo.R +import com.tencent.iot.explorer.link.demo.common.customView.CalendarView +import com.tencent.iot.explorer.link.demo.common.customView.timeline.TimeLineView +import com.tencent.iot.explorer.link.demo.common.customView.timeline.TimeLineViewChangeListener +import com.tencent.iot.explorer.link.demo.common.util.CommonUtils +import com.tencent.iot.explorer.link.demo.common.util.ImageSelect +import com.tencent.iot.explorer.link.demo.core.entity.DevVideoHistory +import com.tencent.iot.explorer.link.demo.video.Command +import com.tencent.iot.explorer.link.demo.video.CommandResp +import com.tencent.iot.explorer.link.demo.video.DevInfo +import com.tencent.iot.explorer.link.demo.video.playback.* +import com.tencent.iot.explorer.link.demo.video.utils.ToastDialog +import com.tencent.xnet.XP2P +import com.tencent.xnet.XP2PCallback +import kotlinx.android.synthetic.main.activity_video_preview.* +import kotlinx.android.synthetic.main.fragment_video_local_playback.* +import kotlinx.android.synthetic.main.fragment_video_local_playback.iv_left_go +import kotlinx.android.synthetic.main.fragment_video_local_playback.iv_right_go +import kotlinx.android.synthetic.main.fragment_video_local_playback.iv_start +import kotlinx.android.synthetic.main.fragment_video_local_playback.layout_select_date +import kotlinx.android.synthetic.main.fragment_video_local_playback.local_palayback_video +import kotlinx.android.synthetic.main.fragment_video_local_playback.pause_tip_layout +import kotlinx.android.synthetic.main.fragment_video_local_playback.playback_control +import kotlinx.android.synthetic.main.fragment_video_local_playback.time_line +import kotlinx.android.synthetic.main.fragment_video_local_playback.tv_date +import kotlinx.android.synthetic.main.fragment_video_local_playback.video_seekbar +import kotlinx.android.synthetic.main.fragment_video_local_playback.tv_current_pos +import kotlinx.android.synthetic.main.fragment_video_local_playback.tv_all_time +import kotlinx.coroutines.* +import tv.danmaku.ijk.media.player.IMediaPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer +import java.lang.Runnable +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.CountDownLatch +import kotlin.collections.ArrayList +private var countDownLatchs : MutableMap = ConcurrentHashMap() +private var keepConnectThreadLock = Object() +@Volatile +private var keepAliveThreadRuning = true -class VideoLocalPlaybackFragment: VideoPlaybackBaseFragment() { +class VideoLocalPlaybackFragment: VideoPlaybackBaseFragment(), TextureView.SurfaceTextureListener, XP2PCallback, CoroutineScope by MainScope() { + private var TAG = VideoLocalPlaybackFragment::class.java.simpleName + var devInfo: DevInfo? = null + private var player : IjkMediaPlayer = IjkMediaPlayer() + private lateinit var surface: Surface + @Volatile + private var currentPostion = -1L // 小于 0 不需要恢复录像的进度,大于等于 0 需要恢复录像的进度 + @Volatile + private var currentPlayerState = true + private var dateDataSet : MutableSet = CopyOnWriteArraySet() + private var dlg: CalendarDialog? = null + private var urlPrefix = "" + @Volatile + private var recordingState = false + private var seekBarJob : Job? = null + private var keepStartTime = 0L + private var keepEndTime = 0L + private var filePath: String? = null + @Volatile + private var connected = false + @Volatile + private var isShowing = false + + private fun sendCmd(id: String, cmd: String): String { + if (connected) + XP2P.postCommandRequestSync(id, cmd.toByteArray(), cmd.toByteArray().size.toLong(), 2 * 1000 * 1000)?.let { + return it + } + return "" + } + + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + player?.let { + if (!isVisibleToUser && currentPlayerState) { + Log.d(TAG, "setUserVisibleHint playVideo isVisibleToUser $isVisibleToUser") + // 滑动该页面时,如果处于播放状态,暂停播放 + launch (Dispatchers.Main) { + iv_start.performClick() + } + } + } + isShowing = isVisibleToUser + Log.d(TAG, "setUserVisibleHint isShowing $isShowing") + } override fun startHere(view: View) { super.startHere(view) - tv_date.setText(dateFormat.format(System.currentTimeMillis())) - action_list_view.visibility = View.GONE + IjkMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG) + tv_date.text = dateFormat.format(System.currentTimeMillis()) setListener() + launch(Dispatchers.Main) { + delay(100) // 延迟一秒再进行连接,保证存在设备信息 + startConnect() + } + + App.data.accessInfo?.let { + XP2P.setQcloudApiCred(it.accessId, it.accessToken) + XP2P.setCallback(this) + } + + dlg = CalendarDialog(context, ArrayList(), onMonthChanged) + dlg?.setOnClickedListener(dlgOnClickedListener) + play_speed.setText(R.string.play_speed_1) + recordView() + local_palayback_video.surfaceTextureListener = this + time_line.setTimelineChangeListener(timeLineViewChangeListener) + + iv_start.setOnClickListener { + devInfo?:let { return@setOnClickListener } + if (TextUtils.isEmpty(tv_all_time.text.toString())) return@setOnClickListener + + var id = "${App.data.accessInfo?.productId}/${devInfo?.deviceName}" + Log.d(TAG, "setOnClickListener currentPlayerState $currentPlayerState") + + if (currentPlayerState) { + launch (Dispatchers.IO) { + var stopCommand = Command.pauseLocalVideoUrl(devInfo!!.channel) + var resp = sendCmd(id, stopCommand) + var commandResp = JSONObject.parseObject(resp, CommandResp::class.java) + if (commandResp != null && commandResp.status == 0) { + launch (Dispatchers.Main) { + iv_start.setImageResource(R.mipmap.start) + pause_tip_layout.visibility = View.VISIBLE + seekBarJob?.cancel() + currentPlayerState = false + player?.pause() + } + } + } + + } else { + launch (Dispatchers.IO) { + var startCommand = Command.resumeLocalVideoUrl(devInfo!!.channel) + var resp = sendCmd(id, startCommand) + var commandResp = JSONObject.parseObject(resp, CommandResp::class.java) + if (commandResp != null && commandResp.status == 0) { + launch (Dispatchers.Main) { + iv_start.setImageResource(R.mipmap.stop) + pause_tip_layout.visibility = View.GONE + startJobRereshTimeAndProgress() + currentPlayerState = true + player?.start() + } + } + } + } + } + video_seekbar.setOnSeekBarChangeListener(onSeekBarChangeListener) + } + + private var onCompletionListener = object: IMediaPlayer.OnCompletionListener { + override fun onCompletion(mp: IMediaPlayer?) { + Log.d(TAG, "onCompletion") + iv_start.setImageResource(R.mipmap.start) + pause_tip_layout.visibility = View.VISIBLE + seekBarJob?.cancel() + currentPlayerState = false + playVideo(keepStartTime, keepEndTime, 0) + } + } + + private var timeLineViewChangeListener = object : TimeLineViewChangeListener { + override fun onChange(date: Date?, timeLineView: TimeLineView?) { + currentPlayerState = true + if (timeLineView == null) return + date?.let { + for (j in 0 until timeLineView!!.timeBlockInfos.size) { + var blockTime = timeLineView!!.timeBlockInfos!!.get(j) + if (blockTime.startTime.time <= date.time && blockTime.endTime.time >= date.time) { + var offest = date.time - blockTime.startTime.time + playVideo(blockTime.startTime.time/1000, blockTime.endTime.time/1000, offest/1000) + return@onChange + } + } + } + } + } + + override fun videoViewNeeResize(marginStart: Int, marginEnd: Int) { + var videoLayoutParams = local_palayback_video.layoutParams as ConstraintLayout.LayoutParams + videoLayoutParams.marginStart = marginStart + videoLayoutParams.marginEnd = marginEnd + local_palayback_video.layoutParams = videoLayoutParams + } + + private fun startJobRereshTimeAndProgress() { + seekBarJob = launch { + while (isActive) { + delay(1000) + tv_current_pos.text = CommonUtils.formatTime(player.currentPosition) + video_seekbar.progress = (player.currentPosition / 1000).toInt() + } + } + } + + private var onErrorListener = object : IMediaPlayer.OnErrorListener { + override fun onError(mp: IMediaPlayer?, what: Int, extra: Int): Boolean { + video_seekbar.progress = 0 + video_seekbar.max = 0 + tv_current_pos.setText("00:00:00") + tv_all_time.setText("00:00:00") + iv_start.setImageResource(R.mipmap.start) + pause_tip_layout.visibility = View.GONE + Toast.makeText(context, getString(R.string.no_data), Toast.LENGTH_SHORT).show() + iv_start.isClickable = false + currentPlayerState = false + return true + } + } + + private var onInfoListener = object : IMediaPlayer.OnInfoListener { + override fun onInfo(mp: IMediaPlayer?, what: Int, extra: Int): Boolean { + mp?.let { + if (it.isPlaying) { + iv_start.setImageResource(R.mipmap.stop) + currentPlayerState = true + pause_tip_layout.visibility = View.GONE + return true + } + } + iv_start.setImageResource(R.mipmap.start) + pause_tip_layout.visibility = View.VISIBLE + currentPlayerState = false + return true + } + } + + private var dlgOnClickedListener = object : CalendarDialog.OnClickedListener { + override fun onOkClicked(checkedDates: MutableList?) { + checkedDates?.let { + if (it.size <= 0) return@let + tv_date.text = CommonUtils.dateConvertionWithSplit(it[0]) + var parseDate = SimpleDateFormat(CalendarView.SECOND_DATE_FORMAT_PATTERN).parse(tv_date.text.toString()) + launch(Dispatchers.Main) { + refreshDateTime(parseDate) + } + } + } + + override fun onOkClickedCheckedDateWithoutData() { + ToastDialog(context, ToastDialog.Type.WARNING, getString(R.string.checked_date_no_video), 2000).show() + } + + override fun onOkClickedWithoutDateChecked() { + ToastDialog(context, ToastDialog.Type.WARNING, getString(R.string.checked_date_first), 2000).show() + } + } + + private var onMonthChanged = object: CalendarDialog.OnMonthChanged { + override fun onMonthChanged(dlg: CalendarDialog, it: String) { + // 包含对应数据,不再重新获取当前日期的数据 + if (dateDataSet.contains(it)) return + + launch(Dispatchers.Main) { + var resp = requestTagDateInfo(it) + Log.d(TAG, "resp $resp") + if (TextUtils.isEmpty(resp)) return@launch + + try { + var respJson = JSONObject.parseObject(resp) + if (!respJson.containsKey("video_list")) return@launch + + // 转换成实际有录像的日期 + dateDataSet.add(it) + var datesStrArr = Integer.toBinaryString(respJson.getInteger("video_list")) + datesStrArr = datesStrArr.reversed() + var dateParam: MutableList = ArrayList() + for (i in datesStrArr.indices) { + if (datesStrArr[i].toString() == "1") { + var eleDate = it.replace("-", "") + String.format("%02d", i + 1) + dateParam.add(eleDate) + } + } + dlg.addTagDates(dateParam) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + + override fun getContentView(): Int { + return R.layout.fragment_video_local_playback } private fun setListener() { + pause_tip_layout.setOnClickListener { iv_start.performClick() } + iv_video_back.setOnClickListener { } playback_control.setOnClickListener { } iv_left_go.setOnClickListener { time_line.last() } iv_right_go.setOnClickListener { time_line.next() } + layout_select_date.setOnClickListener { showCalendarDialog() } + play_speed.setOnClickListener { + if (!portrait) { + var dlg = RightPlaySpeedDialog(context, getIndexByText()) + dlg.setOnDismisListener(rightPlaySpeedDialogListener) + dlg.show() + } else { + var dlg = BottomPlaySpeedDialog(context, getString(R.string.play_speed_title), getIndexByText()) + dlg.setOnDismisListener(bottomPlaySpeedDialogListener) + dlg.show() + } + } + iv_video_photo.setOnClickListener { + ImageSelect.saveBitmap(this.context, local_palayback_video.bitmap) + ToastDialog(context, ToastDialog.Type.SUCCESS, getString(R.string.capture_successed), 2000).show() + } + iv_video_record.setOnClickListener { + recordingState = !recordingState + recordView() + if (recordingState) { + filePath = CommonUtils.generateFileDefaultPath() + var ret = player.startRecord(filePath) + if (ret != 0) { + ToastDialog(context, ToastDialog.Type.WARNING, getString(R.string.record_failed), 2000).show() + } + } else { + player.stopRecord() + context?.let { ctx -> + CommonUtils.refreshVideoList(ctx, filePath) + } + } + } + } + + private fun recordView() { + if (recordingState) { + iv_video_record.setImageResource(R.mipmap.recording) + recording_layout.visibility = View.VISIBLE + } else { + iv_video_record.setImageResource(R.mipmap.record_white) + recording_layout.visibility = View.GONE + } + } + + private var rightPlaySpeedDialogListener = object: RightPlaySpeedDialog.OnDismisListener { + override fun onItemClicked(pos: Int) { + play_speed.setText(getTextByIndex(pos)) + } + override fun onDismiss() {} + } + + private var bottomPlaySpeedDialogListener = object: BottomPlaySpeedDialog.OnDismisListener { + override fun onItemClicked(pos: Int) { + play_speed.setText(getTextByIndex(pos)) + } + } + + private fun getTextByIndex(index: Int): String { + when(index) { + 0 -> return getString(R.string.play_speed_0_5) + 1 -> return getString(R.string.play_speed_0_75) + 2 -> return getString(R.string.play_speed_1) + 3 -> return getString(R.string.play_speed_1_25) + 4 -> return getString(R.string.play_speed_1_5) + 5 -> return getString(R.string.play_speed_2) + } + return getString(R.string.play_speed_1) + } + + private fun getIndexByText(): Int { + when(play_speed.text.toString()) { + getString(R.string.play_speed_0_5) -> return 0 + getString(R.string.play_speed_0_75) -> return 1 + getString(R.string.play_speed_1) -> return 2 + getString(R.string.play_speed_1_25) -> return 3 + getString(R.string.play_speed_1_5) -> return 4 + getString(R.string.play_speed_2) -> return 5 + } + return -1 + } + + private fun formateDateParam(dateStr: String): String { + var timeStr = dateStr + timeStr = timeStr.replace("-", "") + if (timeStr.length >= 6) { + return timeStr.substring(0, 6) + } + return "" + } + + private fun requestTagDateInfo(dateStr: String): String { + if (App.data.accessInfo == null || devInfo == null || + TextUtils.isEmpty(devInfo?.deviceName)) return "" + + var id = "${App.data.accessInfo?.productId}/${devInfo?.deviceName}" + var timeStr = formateDateParam(dateStr) + Log.d(TAG, "request timeStr $timeStr") + var command = Command.getMonthDates(devInfo!!.channel, timeStr) + if (TextUtils.isEmpty(command)) return "" + return sendCmd(id, command) + } + + private fun refreshDateTime(date: Date) { + if (App.data.accessInfo == null || devInfo == null || + TextUtils.isEmpty(devInfo?.deviceName)) return + + var id = "${App.data.accessInfo?.productId}/${devInfo?.deviceName}" + var command = Command.getDayTimeBlocks(devInfo!!.channel, date) + var resp = sendCmd(id, command) + + if (TextUtils.isEmpty(resp)) return + var devVideoHistory = JSONObject.parseObject(resp, DevVideoHistory::class.java) + devVideoHistory?:let { return@refreshDateTime } + if (devVideoHistory?.video_list == null || devVideoHistory.video_list!!.size <= 0) return + var formateDatas = CommonUtils.formatTimeData(devVideoHistory.getTimeBlocks()) + time_line.currentDayTime = Date(formateDatas[0].startTime.time) + time_line.setTimeLineTimeDay(Date(formateDatas[0].startTime.time)) + time_line.timeBlockInfos = formateDatas + time_line.invalidate() + + playVideo(formateDatas[0].startTime.time/1000, formateDatas[0].endTime.time/1000, 0) + } + + private fun playVideo(startTime: Long, endTime: Long, offset: Long) { + Log.d(TAG, "startTime $startTime endTime $endTime offset $offset") + keepStartTime = startTime + keepEndTime = endTime + devInfo?.let { + Log.d(TAG, "isShowing $isShowing") + launch (Dispatchers.Main) { + delay(1000) + if (!isShowing) currentPlayerState = false + Log.d(TAG, "playVideo currentPlayerState $currentPlayerState") + setPlayerUrl(Command.getLocalVideoUrl(it.channel, startTime, endTime), offset) + tv_all_time.text = CommonUtils.formatTime(endTime * 1000 - startTime * 1000) + video_seekbar.max = (endTime - startTime).toInt() + } + } + } + + private fun setPlayerUrl(suffix: String, offset: Long) { + player.release() + launch (Dispatchers.Main) { + layout_video?.removeView(local_palayback_video) + layout_video?.addView(local_palayback_video, 0) + } + + player = IjkMediaPlayer() + player.setOnInfoListener(onInfoListener) + player.setOnErrorListener(onErrorListener) + player.setOnCompletionListener(onCompletionListener) + player?.let { + var url = urlPrefix + suffix + Log.d(TAG, "setPlayerUrl url $url") + it.reset() + + it.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 25 * 1024) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0) + if (!currentPlayerState) { + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0) + } else { + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1) + } + it.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "threads", 1) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec",1) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1) + + + it.setSurface(this.surface) + it.dataSource = url + it.prepareAsync() + it.start() + + if (offset > 0) { + devInfo?.let { dev -> + var seekCommand = Command.seekLocalVideo(dev.channel, offset) + var id = "${App.data.accessInfo?.productId}/${dev.deviceName}" + + var seekResp = sendCmd(id, seekCommand) + var commandResp = JSON.parseObject(seekResp, CommandResp::class.java) + L.e(TAG, "seekCommandResp code " + commandResp?.status) + } + } + + startJobRereshTimeAndProgress() + } + } + + private fun showCalendarDialog() { + dateDataSet.clear() + dlg?.show() } private var onSeekBarChangeListener = object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { if (fromUser) { // 是用户操作的,调整视频到指定的时间点 - palayback_video.seekTo(progress * 1000) + playVideo(keepStartTime, keepEndTime, progress.toLong()) + player.seekTo(progress.toLong() * 1000) return } } @@ -32,4 +516,165 @@ class VideoLocalPlaybackFragment: VideoPlaybackBaseFragment() { override fun onStartTrackingTouch(seekBar: SeekBar?) {} override fun onStopTrackingTouch(seekBar: SeekBar?) {} } + + private fun startConnect() { + if (App.data.accessInfo == null || devInfo == null || + TextUtils.isEmpty(devInfo?.deviceName)) return + + Thread(Runnable { + var id = "${App.data.accessInfo?.productId}/${devInfo?.deviceName}" + var started = XP2P.startServiceWithXp2pInfo(id, + App.data.accessInfo?.productId, devInfo?.deviceName, "") + if (started != 0) { + launch(Dispatchers.Main) { + var errInfo = getString(R.string.error_with_code, id, started.toString()) + Toast.makeText(context, errInfo, Toast.LENGTH_SHORT).show() + } + return@Runnable + } + + devInfo?.let { + var tmpCountDownLatch = CountDownLatch(1) + countDownLatchs.put("${App.data.accessInfo!!.productId}/${it.deviceName}", tmpCountDownLatch) + tmpCountDownLatch.await() + urlPrefix = XP2P.delegateHttpFlv("${App.data.accessInfo!!.productId}/${it.deviceName}") + + // 启动成功后,开始开启守护线程 + keepConnect(id) + } + + }).start() + } + + private fun keepConnect(id: String?) { + if (TextUtils.isEmpty(id)) return + + // 开启守护线程 + Thread{ + var objectLock = Object() + while (true) { + var tmpCountDownLatch = CountDownLatch(1) + countDownLatchs.put(id!!, tmpCountDownLatch) + + Log.d(TAG, "id=${id} keepConnect wait disconnected msg") + synchronized(keepConnectThreadLock) { + keepConnectThreadLock.wait() + } + Log.d(TAG, "id=${id} keepConnect do not wait and keepAliveThreadRuning=${keepAliveThreadRuning}") + if (!keepAliveThreadRuning) break //锁被释放后,检查守护线程是否继续运行 + + // 发现断开尝试恢复视频,每隔一秒尝试一次 + XP2P.stopService(id) + while (XP2P.startServiceWithXp2pInfo(id, App.data.accessInfo!!.productId, devInfo?.deviceName, "") != 0) { + XP2P.stopService(id) + synchronized(objectLock) { + objectLock.wait(1000) + } + Log.d(TAG, "id=${id}, try to call startServiceWithXp2pInfo") + } + + Log.d(TAG, "id=${id}, call startServiceWithXp2pInfo successed") + countDownLatchs.put(id!!, tmpCountDownLatch) + Log.d(TAG, "id=${id}, tmpCountDownLatch start wait") + tmpCountDownLatch.await() + Log.d(TAG, "id=${id}, tmpCountDownLatch do not wait any more") + + urlPrefix = XP2P.delegateHttpFlv(id) + if (TextUtils.isEmpty(urlPrefix)) continue + if (currentPostion >= 0) { // 尝试从上次断掉的时间节点开始恢复录像 + playVideo(keepStartTime, keepEndTime, currentPostion) + } else { + playVideo(keepStartTime, keepEndTime, 0) + } + } + }.start() + } + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) { + surface?.let { + this.surface = Surface(surface) + player.setSurface(this.surface) + } + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {} + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean { return false } + override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {} + override fun fail(msg: String?, errorCode: Int) {} + override fun commandRequest(id: String?, msg: String?) {} + override fun avDataRecvHandle(id: String?, data: ByteArray?, len: Int) {} + override fun avDataCloseHandle(id: String?, msg: String?, errorCode: Int) {} + + override fun xp2pEventNotify(id: String?, msg: String?, event: Int) { + Log.e(TAG, "id=${id}, event=${event}") + if (event == 1003) { + keepConnectThreadLock?.let { + synchronized(it) { + it.notify() + } + } // 唤醒守护线程 + currentPostion = player.currentPosition / 1000 + L.e(TAG, "xp2pEventNotify currentPostion $currentPostion") + launch (Dispatchers.Main) { + Toast.makeText(context, getString(R.string.error_with_code, id, msg), Toast.LENGTH_SHORT).show() + } + connected = false + + } else if (event == 1004 || event == 1005) { + countDownLatchs.get(id)?.let { + Log.d(tag, "id=${id}, countDownLatch=${it}, countDownLatch.count=${it.count}") + it.countDown() + } + + launch (Dispatchers.Main) { + Toast.makeText(context, getString(R.string.connected, id), Toast.LENGTH_SHORT).show() + } + connected = true + } + } + + override fun onPause() { + super.onPause() + if (context is VideoPlaybackActivity) { + if ((context as VideoPlaybackActivity).isFinishing) { + finishAll() + return + } + } + player?.let { + if (currentPlayerState) { + launch (Dispatchers.Main) { + iv_start.performClick() + } + } + } + } + + private fun finishAll() { + player?.release() + + if (recordingState) { + player.stopRecord() + context?.let { + CommonUtils.refreshVideoList(it, filePath) + } + } + + App.data.accessInfo?.let { access -> + devInfo?.let { + XP2P.stopService("${access.productId}/${it.deviceName}") + } + } + XP2P.setCallback(null) + + countDownLatchs.clear() + // 关闭守护线程 + keepAliveThreadRuning = false + keepConnectThreadLock?.let { + synchronized(it) { + it.notify() + } + } + cancel() + } } \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoMultiPreviewActivity.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoMultiPreviewActivity.kt index 444c08678..03b48e5db 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoMultiPreviewActivity.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoMultiPreviewActivity.kt @@ -3,6 +3,7 @@ package com.tencent.iot.explorer.link.demo.video.preview import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo +import android.content.res.Configuration import android.graphics.SurfaceTexture import android.os.Bundle import android.text.TextUtils @@ -43,11 +44,21 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc lateinit var linearLayoutManager : LinearLayoutManager private var adapter : DevPreviewAdapter? = null private var tag = VideoMultiPreviewActivity::class.simpleName + private var orientation = true override fun getContentView(): Int { return R.layout.activity_video_multi_preview } + override fun onResume() { + super.onResume() + adapter = DevPreviewAdapter(this@VideoMultiPreviewActivity, allDevUrl) + gl_video.layoutManager = linearLayoutManager + gl_video.adapter = adapter + switchOrientation(orientation) + playAll() + } + override fun initView() { App.data.accessInfo?.let { XP2P.setQcloudApiCred(it.accessId, it.accessToken) @@ -64,10 +75,6 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc if (allDevUrl.size <= 1) column = 1 // 当只有一个元素的时候,网格只有一列 gridLayoutManager = GridLayoutManager(this@VideoMultiPreviewActivity, column) linearLayoutManager = LinearLayoutManager(this@VideoMultiPreviewActivity) - adapter = DevPreviewAdapter(this@VideoMultiPreviewActivity, allDevUrl) - gl_video.layoutManager = linearLayoutManager - gl_video.adapter = adapter - playAll() } catch (e : Exception) { e.printStackTrace() } @@ -81,14 +88,14 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc if (App.data.accessInfo == null) return for (i in 0 until allDevUrl.size) { - if (allDevUrl.get(i).Status != 1) continue + if (allDevUrl[i].Status != 1) continue var player = IjkMediaPlayer() - allDevUrl.get(i).surfaceTextureListener = object : TextureView.SurfaceTextureListener { + allDevUrl[i].surfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) { surface?.let { - allDevUrl.get(i).surface = Surface(surface) - player.setSurface(allDevUrl.get(i).surface) + allDevUrl[i].surface = Surface(surface) + player.setSurface(allDevUrl[i].surface) } } @@ -97,7 +104,7 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {} } - setPlayerSource(player, allDevUrl.get(i).devName, allDevUrl.get(i).channel) + setPlayerSource(player, allDevUrl[i].devName, allDevUrl[i].channel) allDevUrl.get(i).player = player } } @@ -139,6 +146,10 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc player.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "threads", 1) player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0) player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1) + player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec",1) + player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1) + player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1) + player.dataSource = url player.prepareAsync() player.start() @@ -165,6 +176,7 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc radio_orientation_h.visibility = View.GONE gl_video.layoutManager = gridLayoutManager } + orientation = orientationV adapter?.notifyDataSetChanged() } @@ -296,9 +308,12 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc override fun avDataRecvHandle(id: String?, data: ByteArray?, len: Int) {} override fun avDataCloseHandle(id: String?, msg: String?, errorCode: Int) {} - override fun onDestroy() { - super.onDestroy() + override fun onPause() { + super.onPause() + finishAll() + } + private fun finishAll() { for (devPlayer in allDevUrl) { devPlayer.player?.release() } @@ -309,7 +324,6 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc } } - XP2P.setCallback(null) countDownLatchs.clear() // 关闭所有守护线程 @@ -321,6 +335,12 @@ class VideoMultiPreviewActivity : VideoBaseActivity(), XP2PCallback, CoroutineSc } } } + } + + override fun onDestroy() { + super.onDestroy() + finishAll() + XP2P.setCallback(null) cancel() } diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoPreviewActivity.kt b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoPreviewActivity.kt index 337a37a00..1c407c701 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoPreviewActivity.kt +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/preview/VideoPreviewActivity.kt @@ -8,6 +8,8 @@ import android.content.pm.ActivityInfo import android.graphics.SurfaceTexture import android.media.AudioManager import android.os.Bundle +import android.os.Handler +import android.os.Message import android.text.TextUtils import android.util.Log import android.view.Surface @@ -39,6 +41,7 @@ import com.tencent.iot.video.link.util.audio.AudioRecordUtil import com.tencent.xnet.XP2P import com.tencent.xnet.XP2PCallback import kotlinx.android.synthetic.main.activity_video_preview.* +import kotlinx.android.synthetic.main.dash_board_layout.* import kotlinx.android.synthetic.main.fragment_video_cloud_playback.* import kotlinx.android.synthetic.main.title_layout.* import kotlinx.coroutines.* @@ -82,13 +85,20 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface @Volatile private var showVideoTime = 0L private var volumeChangeObserver: VolumeChangeObserver? = null + private val MSG_UPDATE_HUD = 1 override fun getContentView(): Int { return R.layout.activity_video_preview } - override fun initView() { + override fun onResume() { + super.onResume() + XP2P.setCallback(this) keepAliveThreadRuning = true + startPlayer() + } + + override fun initView() { presenter = EventPresenter(this@VideoPreviewActivity) var bundle = intent.getBundleExtra(VideoConst.VIDEO_CONFIG) bundle?.let { @@ -119,12 +129,9 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface tv_event_status.visibility = View.VISIBLE tv_event_status.setText(R.string.loading) XP2P.setQcloudApiCred(it.accessId, it.accessToken) - XP2P.setCallback(this) audioRecordUtil = AudioRecordUtil(this, "${it.productId}/${presenter.getDeviceName()}", 16000) } - startPlayer() - //实例化对象并设置监听器 volumeChangeObserver = VolumeChangeObserver(this) volumeChangeObserver?.setVolumeChangeListener(this) @@ -134,6 +141,7 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface private fun startPlayer() { if (App.data.accessInfo == null || TextUtils.isEmpty(presenter.getDeviceName())) return player = IjkMediaPlayer() + mHandler.sendEmptyMessageDelayed(MSG_UPDATE_HUD, 500) Thread(Runnable { var id = "${App.data.accessInfo!!.productId}/${presenter.getDeviceName()}" @@ -157,11 +165,13 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface countDownLatchs.put("${App.data.accessInfo!!.productId}/${presenter.getDeviceName()}", tmpCountDownLatch) tmpCountDownLatch.await() - urlPrefix = XP2P.delegateHttpFlv("${App.data.accessInfo!!.productId}/${presenter.getDeviceName()}") - if (!TextUtils.isEmpty(urlPrefix)) { - player?.let { - resetPlayer() - keepPlayerplay("${App.data.accessInfo!!.productId}/${presenter.getDeviceName()}") + XP2P.delegateHttpFlv("${App.data.accessInfo!!.productId}/${presenter.getDeviceName()}")?.let { + urlPrefix = it + if (!TextUtils.isEmpty(urlPrefix)) { + player?.let { + resetPlayer() + keepPlayerplay("${App.data.accessInfo!!.productId}/${presenter.getDeviceName()}") + } } } }).start() @@ -201,8 +211,10 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface tmpCountDownLatch.await() Log.d(tag, "id=${id}, tmpCountDownLatch do not wait any more") - urlPrefix = XP2P.delegateHttpFlv(id) - if (!TextUtils.isEmpty(urlPrefix)) resetPlayer() + XP2P.delegateHttpFlv(id)?.let { + urlPrefix = it + if (!TextUtils.isEmpty(urlPrefix)) resetPlayer() + } } }.start() } @@ -227,7 +239,7 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface if (able) { var command = Command.getNvrIpcStatus(presenter.getChannel(), 0) var repStatus = XP2P.postCommandRequestSync("${accessInfo.productId}/${presenter.getDeviceName()}", - command.toByteArray(), command.toByteArray().size.toLong(), 2 * 1000 * 1000) + command.toByteArray(), command.toByteArray().size.toLong(), 2 * 1000 * 1000) ?:"" launch(Dispatchers.Main) { var retContent = StringBuilder(repStatus).toString() @@ -287,7 +299,6 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface var dev = DevInfo() dev.deviceName = presenter.getDeviceName() VideoPlaybackActivity.startPlaybackActivity(this@VideoPreviewActivity, dev) - finish() } radio_photo.setOnClickListener { ImageSelect.saveBitmap(this@VideoPreviewActivity, v_preview.bitmap) @@ -331,7 +342,7 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface App.data.accessInfo?.let { if (command.length <= 0) return@Runnable var retContent = XP2P.postCommandRequestSync("${it.productId}/${presenter.getDeviceName()}", - command.toByteArray(), command.toByteArray().size.toLong(), 2 * 1000 * 1000) + command.toByteArray(), command.toByteArray().size.toLong(), 2 * 1000 * 1000)?:"" launch(Dispatchers.Main) { if (TextUtils.isEmpty(retContent)) { retContent = getString(R.string.command_with_error, command) @@ -406,6 +417,13 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface private fun setPlayerUrl(suffix: String) { showTip = false startShowVideoTime = System.currentTimeMillis() + player.release() + launch (Dispatchers.Main) { + layout_video_preview?.removeView(v_preview) + layout_video_preview?.addView(v_preview, 0) + } + + player = IjkMediaPlayer() player?.let { val url = urlPrefix + suffix it.reset() @@ -416,6 +434,9 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1) it.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "threads", 1) it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec",1) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1) + it.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1) it.setSurface(this.surface) it.dataSource = url @@ -476,7 +497,7 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {} override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean { return false } override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) { - if (!showTip) { + if (!showTip && startShowVideoTime > 0) { showVideoTime = System.currentTimeMillis() - startShowVideoTime var content = getString(R.string.time_2_show, connectTime.toString(), showVideoTime.toString()) TipToastDialog(this, content, 10000).show() @@ -491,6 +512,7 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface override fun xp2pEventNotify(id: String?, msg: String?, event: Int) { Log.e(tag, "id=${id}, event=${event}") if (event == 1003) { + startShowVideoTime = 0L keepPlayThreadLock?.let { synchronized(it) { it.notify() @@ -514,9 +536,13 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface } } - override fun onDestroy() { - super.onDestroy() + override fun onPause() { + super.onPause() + finishPlayer() + } + private fun finishPlayer() { + mHandler.removeMessages(MSG_UPDATE_HUD) player?.release() if (radio_talk.isChecked) speakAble(false) if (radio_record.isChecked) { @@ -527,7 +553,6 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface App.data.accessInfo?.let { XP2P.stopService("${it.productId}/${presenter.getDeviceName()}") } - XP2P.setCallback(null) countDownLatchs.clear() // 关闭守护线程 @@ -537,6 +562,12 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface it.notify() } } + } + + override fun onDestroy() { + super.onDestroy() + finishPlayer() + XP2P.setCallback(null) cancel() volumeChangeObserver?.unregisterReceiver(); } @@ -562,4 +593,29 @@ class VideoPreviewActivity : VideoBaseActivity(), EventView, TextureView.Surface player?.setVolume(volume.toFloat(), volume.toFloat()) } } + + private val mHandler: Handler = object : Handler() { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_UPDATE_HUD -> { + val videoCachedDuration = player?.videoCachedDuration + val audioCachedDuration = player?.audioCachedDuration + val videoCachedBytes = player?.videoCachedBytes + val audioCachedBytes = player?.audioCachedBytes + val tcpSpeed = player?.tcpSpeed + + tv_a_cache?.text = String.format(Locale.US, "%s, %s", + CommonUtils.formatedDurationMilli(audioCachedDuration), + CommonUtils.formatedSize(audioCachedBytes)) + tv_v_cache?.text = String.format(Locale.US, "%s, %s", + CommonUtils.formatedDurationMilli(videoCachedDuration), + CommonUtils.formatedSize(videoCachedBytes)) + tv_tcp_speed?.text = String.format(Locale.US, "%s", + CommonUtils.formatedSpeed(tcpSpeed, 1000)) + removeMessages(MSG_UPDATE_HUD) + sendEmptyMessageDelayed(MSG_UPDATE_HUD, 500) + } + } + } + } } \ No newline at end of file diff --git a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/utils/TipToastDialog.java b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/utils/TipToastDialog.java index d18a685ce..b611cd7fc 100644 --- a/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/utils/TipToastDialog.java +++ b/sdkdemo/src/main/java/com/tencent/iot/explorer/link/demo/video/utils/TipToastDialog.java @@ -33,7 +33,7 @@ public TipToastDialog(Context context, String content, long duration) { public void initView() { handler.postDelayed(() -> { if (isShowing()) { - super.dismiss(); + dismiss(); } }, duration); diff --git a/sdkdemo/src/main/res/drawable/background_black_radius_alpha_cell.xml b/sdkdemo/src/main/res/drawable/background_black_radius_alpha_cell.xml index d89a8c2d7..dc3a7460c 100644 --- a/sdkdemo/src/main/res/drawable/background_black_radius_alpha_cell.xml +++ b/sdkdemo/src/main/res/drawable/background_black_radius_alpha_cell.xml @@ -3,11 +3,7 @@ - + diff --git a/sdkdemo/src/main/res/drawable/background_black_white_gradient.xml b/sdkdemo/src/main/res/drawable/background_black_white_gradient.xml new file mode 100644 index 000000000..ee9e5c27c --- /dev/null +++ b/sdkdemo/src/main/res/drawable/background_black_white_gradient.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/sdkdemo/src/main/res/drawable/background_blue_cell.xml b/sdkdemo/src/main/res/drawable/background_blue_cell.xml new file mode 100644 index 000000000..920774376 --- /dev/null +++ b/sdkdemo/src/main/res/drawable/background_blue_cell.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/sdkdemo/src/main/res/drawable/background_cell.xml b/sdkdemo/src/main/res/drawable/background_cell.xml index cfbf25d9e..3b2e7e2a9 100644 --- a/sdkdemo/src/main/res/drawable/background_cell.xml +++ b/sdkdemo/src/main/res/drawable/background_cell.xml @@ -2,8 +2,5 @@ + android:radius="8dp"/> diff --git a/sdkdemo/src/main/res/drawable/background_gray_cell.xml b/sdkdemo/src/main/res/drawable/background_gray_cell.xml new file mode 100644 index 000000000..67f514ce3 --- /dev/null +++ b/sdkdemo/src/main/res/drawable/background_gray_cell.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/sdkdemo/src/main/res/drawable/background_white_alpha_cell.xml b/sdkdemo/src/main/res/drawable/background_white_alpha_cell.xml index 3501a7896..ef45af2f7 100644 --- a/sdkdemo/src/main/res/drawable/background_white_alpha_cell.xml +++ b/sdkdemo/src/main/res/drawable/background_white_alpha_cell.xml @@ -1,13 +1,8 @@ - - + + diff --git a/sdkdemo/src/main/res/drawable/background_white_black_right_gradient.xml b/sdkdemo/src/main/res/drawable/background_white_black_right_gradient.xml new file mode 100644 index 000000000..094ca8c59 --- /dev/null +++ b/sdkdemo/src/main/res/drawable/background_white_black_right_gradient.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/sdkdemo/src/main/res/drawable/background_white_radius.xml b/sdkdemo/src/main/res/drawable/background_white_radius.xml index 58a90e31f..7bd0ed7f1 100644 --- a/sdkdemo/src/main/res/drawable/background_white_radius.xml +++ b/sdkdemo/src/main/res/drawable/background_white_radius.xml @@ -3,11 +3,7 @@ - + diff --git a/sdkdemo/src/main/res/layout/activity_show_all_device.xml b/sdkdemo/src/main/res/layout/activity_show_all_device.xml new file mode 100644 index 000000000..7f6114088 --- /dev/null +++ b/sdkdemo/src/main/res/layout/activity_show_all_device.xml @@ -0,0 +1,55 @@ + + + + + + + + + + +