diff --git a/app/build.gradle b/app/build.gradle
index 5bcd6b5..13cf503 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -83,14 +83,21 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.yanzhenjie.recyclerview:x:1.3.2'
- // 动态权限申请 https://github.com/permissions-dispatcher/PermissionsDispatcher
- implementation "com.github.permissions-dispatcher:permissionsdispatcher:4.9.2"
- annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:4.9.2"
+ // 权限请求框架:https://github.com/getActivity/XXPermissions
+ implementation 'com.github.getActivity:XXPermissions:16.5'
// 相机库 https://natario1.github.io/CameraView/about/getting-started
implementation("com.otaliastudios:cameraview:2.7.2")
// 图片压缩算法 https://github.com/Curzibn/Luban
implementation 'top.zibin:Luban:1.1.8'
- // Android工具类库 https://github.com/l123456789jy/Lazy
- implementation 'com.github.lazylibrary:lazylibrary:1.0.2'
+ // Android工具类库 https://github.com/gycold/EasyAndroid
+ implementation 'io.github.gycold:easyandroid:2.0.7'
+ // 日志工具 https://github.com/elvishew/xLog/blob/master/README_ZH.md
+ implementation 'com.elvishew:xlog:1.10.1'
+ //加载图片的依赖包
+ implementation ("com.github.bumptech.glide:glide:4.11.0") {
+ exclude group: "com.android.support"
+ }
+ // 显示错误提示 https://github.com/nhaarman/supertooltips
+ implementation 'com.nhaarman.supertooltips:library:3.0.0'
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6eeda1e..d19fd34 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,13 +1,16 @@
-
-
-
-
-
-
-
+
+
+
+
@@ -18,11 +21,21 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
+ android:requestLegacyExternalStorage="true"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NavinfoVolvo"
tools:targetApi="31">
+
+
+
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/MainActivity.kt b/app/src/main/java/com/navinfo/volvo/MainActivity.kt
index 7bd160f..25d0a28 100644
--- a/app/src/main/java/com/navinfo/volvo/MainActivity.kt
+++ b/app/src/main/java/com/navinfo/volvo/MainActivity.kt
@@ -1,24 +1,42 @@
package com.navinfo.volvo
-import android.Manifest
import android.content.DialogInterface
+import android.content.Intent
import android.os.Bundle
+import android.view.View
import android.widget.Toast
-import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
+import com.easytools.tools.FileUtils
+import com.elvishew.xlog.LogConfiguration
+import com.elvishew.xlog.LogLevel
+import com.elvishew.xlog.XLog
+import com.elvishew.xlog.interceptor.BlacklistTagsFilterInterceptor
+import com.elvishew.xlog.printer.AndroidPrinter
+import com.elvishew.xlog.printer.ConsolePrinter
+import com.elvishew.xlog.printer.Printer
+import com.elvishew.xlog.printer.file.FilePrinter
+import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
+import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
+import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.hjq.permissions.OnPermissionCallback
+import com.hjq.permissions.Permission
+import com.hjq.permissions.XXPermissions
import com.navinfo.volvo.databinding.ActivityMainBinding
-import permissions.dispatcher.*
+import com.navinfo.volvo.ui.message.MessageActivity
+import com.navinfo.volvo.utils.SystemConstant
+
+//@RuntimePermissions
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
- @NeedsPermission(Manifest.permission.CAMERA)
- override fun onCreate(savedInstanceState: Bundle?) {
+ public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@@ -34,12 +52,101 @@ class MainActivity : AppCompatActivity() {
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications, R.id.navigation_obtain_message
)
)
-// setupActionBarWithNavController(navController, appBarConfiguration)
+ setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
+ findViewById(R.id.fab_new_message).apply {
+ this.setOnClickListener {
+ // 跳转到Message的Fragment
+ val messageIntent:Intent = Intent(this@MainActivity, MessageActivity::class.java)
+ startActivity(messageIntent)
+// findNavController(R.id.layer_main_child_fragment).navigate(R.id.navigation_obtain_message)
+ }
+ }
+
+ XXPermissions.with(this)
+ // 申请单个权限
+ .permission(Permission.WRITE_EXTERNAL_STORAGE)
+ .permission(Permission.READ_EXTERNAL_STORAGE)
+ // 设置权限请求拦截器(局部设置)
+ //.interceptor(new PermissionInterceptor())
+ // 设置不触发错误检测机制(局部设置)
+ //.unchecked()
+ .request(object : OnPermissionCallback {
+
+ override fun onGranted(permissions: MutableList, all: Boolean) {
+ if (!all) {
+ Toast.makeText(this@MainActivity, "获取部分权限成功,但部分权限未正常授予", Toast.LENGTH_SHORT).show()
+ return
+ }
+ // 在SD卡创建项目目录
+ createRootFolder()
+ }
+
+ override fun onDenied(permissions: MutableList, never: Boolean) {
+ if (never) {
+ Toast.makeText(this@MainActivity, "永久拒绝授权,请手动授权文件读写权限", Toast.LENGTH_SHORT).show()
+ // 如果是被永久拒绝就跳转到应用权限系统设置页面
+ XXPermissions.startPermissionActivity(this@MainActivity, permissions)
+ } else {
+ onSDCardDenied()
+ showRationaleForSDCard(permissions)
+ }
+ }
+ })
}
- @OnShowRationale(Manifest.permission.CAMERA)
- fun showRationaleForCamera(request: PermissionRequest) {
+// @NeedsPermission(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
+ fun createRootFolder() {
+ // 在SD卡创建项目目录
+ val sdCardPath = getExternalFilesDir(null)
+// SystemConstant.ROOT_PATH = "${sdCardPath}/${SystemConstant.FolderName}"
+ SystemConstant.ROOT_PATH = sdCardPath!!.absolutePath
+ SystemConstant.LogFolder = "${sdCardPath!!.absolutePath}/log"
+ FileUtils.createOrExistsDir(SystemConstant.LogFolder)
+ SystemConstant.CameraFolder = "${sdCardPath!!.absolutePath}/camera"
+ FileUtils.createOrExistsDir(SystemConstant.CameraFolder)
+ SystemConstant.SoundFolder = "${sdCardPath!!.absolutePath}/sound"
+ FileUtils.createOrExistsDir(SystemConstant.SoundFolder)
+ xLogInit(SystemConstant.LogFolder)
+ }
+
+ fun xLogInit(logFolder: String) {
+ val config = LogConfiguration.Builder()
+ .logLevel(
+ if (BuildConfig.DEBUG)
+ LogLevel.ALL // 指定日志级别,低于该级别的日志将不会被打印,默认为 LogLevel.ALL
+ else LogLevel.NONE
+ )
+ .tag("Volvo") // 指定 TAG,默认为 "X-LOG"
+ .enableThreadInfo() // 允许打印线程信息,默认禁止
+ .enableStackTrace(2) // 允许打印深度为 2 的调用栈信息,默认禁止
+ .enableBorder() // 允许打印日志边框,默认禁止
+ .addInterceptor(
+ BlacklistTagsFilterInterceptor( // 添加黑名单 TAG 过滤器
+ "blacklist1", "blacklist2", "blacklist3"
+ )
+ )
+ .build()
+
+ val androidPrinter: Printer = AndroidPrinter(true) // 通过 android.util.Log 打印日志的打印器
+
+ val consolePrinter: Printer = ConsolePrinter() // 通过 System.out 打印日志到控制台的打印器
+
+ val filePrinter: Printer = FilePrinter.Builder("${SystemConstant.ROOT_PATH}/Logs") // 指定保存日志文件的路径
+ .fileNameGenerator(DateFileNameGenerator()) // 指定日志文件名生成器,默认为 ChangelessFileNameGenerator("log")
+ .backupStrategy(NeverBackupStrategy()) // 指定日志文件备份策略,默认为 FileSizeBackupStrategy(1024 * 1024)
+ .build()
+
+ XLog.init( // 初始化 XLog
+ config, // 指定日志配置,如果不指定,会默认使用 new LogConfiguration.Builder().build()
+ androidPrinter, // 添加任意多的打印器。如果没有添加任何打印器,会默认使用 AndroidPrinter(Android)/ConsolePrinter(java)
+ consolePrinter,
+ filePrinter
+ )
+ }
+
+// @OnShowRationale(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
+ fun showRationaleForSDCard(permissions: MutableList) {
// showRationaleDialog(R.string.permission_camera_rationale, request)
// Toast.makeText(context, "当前操作需要您授权相机权限!", Toast.LENGTH_SHORT).show()
MaterialAlertDialogBuilder(this)
@@ -47,19 +154,13 @@ class MainActivity : AppCompatActivity() {
.setMessage("当前操作需要您授权读写SD卡权限!")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
dialogInterface.dismiss()
- // 在SD卡创建项目目录
-
+ XXPermissions.startPermissionActivity(this@MainActivity, permissions)
})
.show()
}
- @OnPermissionDenied(Manifest.permission.CAMERA)
- fun onCameraDenied() {
+// @OnPermissionDenied(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
+ fun onSDCardDenied() {
Toast.makeText(this, "当前操作需要您授权读写SD卡权限!", Toast.LENGTH_SHORT).show()
}
-
- @OnNeverAskAgain(Manifest.permission.CAMERA)
- fun onCameraNeverAskAgain() {
- Toast.makeText(this, "您已永久拒绝授权读写SD卡权限!", Toast.LENGTH_SHORT).show()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/RecorderLifecycleObserver.kt b/app/src/main/java/com/navinfo/volvo/RecorderLifecycleObserver.kt
new file mode 100644
index 0000000..28424e2
--- /dev/null
+++ b/app/src/main/java/com/navinfo/volvo/RecorderLifecycleObserver.kt
@@ -0,0 +1,66 @@
+package com.navinfo.volvo
+
+import android.media.MediaRecorder
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.easytools.tools.DateUtils
+import com.elvishew.xlog.XLog
+import com.navinfo.volvo.utils.SystemConstant
+import java.util.*
+
+class RecorderLifecycleObserver: DefaultLifecycleObserver {
+ private var mediaRecorder: MediaRecorder? = null
+ private lateinit var recorderAudioPath: String
+
+ fun initAndStartRecorder() {
+ recorderAudioPath = "${SystemConstant.SoundFolder}/${DateUtils.date2Str(Date(), DateUtils.FORMAT_YMDHMS)}.m4a"
+ mediaRecorder = MediaRecorder().apply {
+ setAudioSource(MediaRecorder.AudioSource.MIC)
+ setOutputFormat(MediaRecorder.OutputFormat.DEFAULT)
+ setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
+ // 开始录音
+ setOutputFile(recorderAudioPath)
+ try {
+ prepare()
+ } catch (e: Exception) {
+ XLog.e("prepare() failed")
+ }
+ start()
+ }
+ }
+
+ fun stopAndReleaseRecorder(): String {
+ mediaRecorder?.stop()
+ mediaRecorder?.release()
+ mediaRecorder = null
+ return recorderAudioPath
+ }
+
+// override fun onCreate(owner: LifecycleOwner) {
+// super.onCreate(owner)
+//
+// }
+//
+// override fun onStart(owner: LifecycleOwner) {
+// super.onStart(owner)
+// }
+//
+// override fun onResume(owner: LifecycleOwner) {
+// super.onResume(owner)
+// }
+
+ override fun onPause(owner: LifecycleOwner) {
+ super.onPause(owner)
+ mediaRecorder?.pause()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ super.onStop(owner)
+ mediaRecorder?.stop()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ super.onDestroy(owner)
+ mediaRecorder?.release()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/ui/camera/CameraFragment.kt b/app/src/main/java/com/navinfo/volvo/ui/camera/CameraFragment.kt
index 1c84f0c..d4d5bed 100644
--- a/app/src/main/java/com/navinfo/volvo/ui/camera/CameraFragment.kt
+++ b/app/src/main/java/com/navinfo/volvo/ui/camera/CameraFragment.kt
@@ -7,9 +7,17 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.get
+import androidx.navigation.Navigation
+import com.easytools.tools.DateUtils
+import com.easytools.tools.FileUtils
+import com.elvishew.xlog.XLog
import com.navinfo.volvo.databinding.FragmentCameraBinding
+import com.navinfo.volvo.ui.message.ObtainMessageViewModel
+import com.navinfo.volvo.utils.SystemConstant
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.CameraView
+import com.otaliastudios.cameraview.FileCallback
import com.otaliastudios.cameraview.PictureResult
import top.zibin.luban.Luban
import top.zibin.luban.OnCompressListener
@@ -24,6 +32,9 @@ class CameraFragment : Fragment() {
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
+// private val exportFolderPath by lazy {
+// "${SystemConstant.ROOT_PATH}/exportPic/"
+// }
private val cameraLifeCycleObserver: CameraLifeCycleObserver by lazy {
CameraLifeCycleObserver()
@@ -34,7 +45,7 @@ class CameraFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- lifecycle.addObserver(cameraLifeCycleObserver)
+// lifecycle.addObserver(cameraLifeCycleObserver)
val cameraViewModel =
ViewModelProvider(this).get(CameraViewModel::class.java)
@@ -47,41 +58,55 @@ class CameraFragment : Fragment() {
cameraView.addCameraListener(object:CameraListener() { // 添加拍照回调
override fun onPictureTaken(result: PictureResult) {
super.onPictureTaken(result)
- result.toFile()
- // 压缩图片文件
- Luban.with(context)
- .load(photos)
- .ignoreBy(100)
- .setTargetDir(getPath())
- .filter { path ->
- !(TextUtils.isEmpty(path) || path.lowercase(Locale.getDefault())
- .endsWith(".gif"))
+// FileUtils.createOrExistsDir(cameraFolderPath)
+ val resultFile = File("${SystemConstant.CameraFolder}/${DateUtils.date2Str(Date(), DateUtils.FORMAT_YMDHMS)}.jpg")
+ result.toFile(resultFile, object: FileCallback {
+ override fun onFileReady(resultFile: File?) {
+ // 压缩图片文件
+ Luban.with(context)
+ .load(mutableListOf(resultFile) as List?)
+ .ignoreBy(200)
+ .setTargetDir("${SystemConstant.CameraFolder}")
+ .filter { path ->
+ !(TextUtils.isEmpty(path) || path.lowercase(Locale.getDefault())
+ .endsWith(".gif"))
+ }
+ .setCompressListener(object : OnCompressListener {
+ override fun onStart() {
+ XLog.d("开始压缩图片")
+ }
+
+ override fun onSuccess(file: File?) {
+ XLog.d("压缩图片成功:${file?.absolutePath}")
+ // 删除源文件
+ if (!resultFile!!.absolutePath.equals(file!!.absolutePath)) {
+ resultFile!!.delete()
+ }
+ // 跳转回原Fragment,展示拍摄的照片
+ ViewModelProvider(requireActivity()).get(ObtainMessageViewModel::class.java).updateMessagePic(file!!.absolutePath)
+ // 跳转回原界面
+ Navigation.findNavController(root).popBackStack()
+ }
+
+ override fun onError(e: Throwable) {
+ XLog.d("压缩图片失败:${e.message}")
+ }
+ }).launch()
}
- .setCompressListener(object : OnCompressListener {
- override fun onStart() {
- // TODO 压缩开始前调用,可以在方法内启动 loading UI
- }
-
- override fun onSuccess(file: File?) {
- // TODO 压缩成功后调用,返回压缩后的图片文件
- }
-
- override fun onError(e: Throwable) {
- // TODO 当压缩过程出现问题时调用
- }
- }).launch()
+ })
}
})
// 点击拍照
binding.imgStartCamera.setOnClickListener {
cameraView.takePicture()
}
+
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
- lifecycle.removeObserver(cameraLifeCycleObserver)
+// lifecycle.removeObserver(cameraLifeCycleObserver)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/ui/message/MessageActivity.kt b/app/src/main/java/com/navinfo/volvo/ui/message/MessageActivity.kt
new file mode 100644
index 0000000..cbb84f9
--- /dev/null
+++ b/app/src/main/java/com/navinfo/volvo/ui/message/MessageActivity.kt
@@ -0,0 +1,39 @@
+package com.navinfo.volvo.ui.message
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.Navigation
+import androidx.navigation.findNavController
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.navigateUp
+import androidx.navigation.ui.setupActionBarWithNavController
+import com.navinfo.volvo.R
+import com.navinfo.volvo.databinding.ActivityMessageBinding
+
+class MessageActivity : AppCompatActivity() {
+
+ private lateinit var appBarConfiguration: AppBarConfiguration
+ private lateinit var binding: ActivityMessageBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityMessageBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setSupportActionBar(binding.toolbar)
+ binding.toolbar.setOnClickListener {
+ Navigation.findNavController(it).popBackStack()
+ }
+
+ val navController = findNavController(R.id.nav_host_fragment_message)
+ appBarConfiguration = AppBarConfiguration(navController.graph)
+ setupActionBarWithNavController(navController, appBarConfiguration)
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ val navController = findNavController(R.id.nav_host_fragment_message)
+ return navController.navigateUp(appBarConfiguration)
+ || super.onSupportNavigateUp()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageFragment.kt b/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageFragment.kt
index b8ec592..b1cb9c9 100644
--- a/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageFragment.kt
+++ b/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageFragment.kt
@@ -1,27 +1,56 @@
package com.navinfo.volvo.ui.message
-import android.Manifest
import android.content.DialogInterface
import android.os.Bundle
+import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
+import android.view.View.*
import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemSelectedListener
+import android.widget.ArrayAdapter
import android.widget.Toast
+import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.Navigation
+import com.easytools.tools.DateUtils
+import com.easytools.tools.ToastUtils
+import com.elvishew.xlog.XLog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.gredicer.datetimepicker.DateTimePickerFragment
+import com.hjq.permissions.OnPermissionCallback
+import com.hjq.permissions.Permission
+import com.hjq.permissions.XXPermissions
+import com.navinfo.volvo.R
+import com.navinfo.volvo.RecorderLifecycleObserver
import com.navinfo.volvo.databinding.FragmentObtainMessageBinding
+import com.navinfo.volvo.db.dao.entity.AttachmentType
+import com.navinfo.volvo.db.dao.entity.Message
import com.navinfo.volvo.ui.markRequiredInRed
-import permissions.dispatcher.*
+import com.navinfo.volvo.utils.EasyMediaFile
+import com.navinfo.volvo.utils.SystemConstant
+import com.nhaarman.supertooltips.ToolTip
+import top.zibin.luban.Luban
+import top.zibin.luban.OnCompressListener
+import java.io.File
import java.util.*
+
+//@RuntimePermissions
class ObtainMessageFragment: Fragment() {
private var _binding: FragmentObtainMessageBinding? = null
private val obtainMessageViewModel by lazy {
ViewModelProvider(requireActivity()).get(ObtainMessageViewModel::class.java)
}
+ private val photoHelper by lazy {
+ EasyMediaFile().setCrop(true)
+ }
+ private val recorderLifecycleObserver by lazy {
+ RecorderLifecycleObserver()
+ }
// This property is only valid between onCreateView and
// onDestroyView.
@@ -35,16 +64,43 @@ class ObtainMessageFragment: Fragment() {
_binding = FragmentObtainMessageBinding.inflate(inflater, container, false)
val root: View = binding.root
+ obtainMessageViewModel.setCurrentMessage(Message())
+
obtainMessageViewModel?.getMessageLiveData()?.observe(
viewLifecycleOwner, Observer {
// 初始化界面显示内容
- if(it.title!=null)
+ if(it.title?.isNotEmpty() == true)
binding.tvMessageTitle?.setText(it.title)
- if (it.sendDate!=null) {
- binding.btnSendTime.setText(it.sendDate)
+ if (it.sendDate?.isNotEmpty() == true) {
+ binding.btnSendTime.text = it.sendDate
}
+ var hasPhoto = false
+ var hasAudio = false
+ if (it.attachment.isNotEmpty()) {
+ // 展示照片文件或录音文件
+ for (attachment in it.attachment) {
+ if (attachment.attachmentType == AttachmentType.PIC) {
+// Glide.with(context!!)
+// .asBitmap().fitCenter()
+// .load(attachment.pathUrl)
+// .into(binding.imgMessageAttachment)
+ // 显示名称
+ binding.tvPhotoName.text = attachment.pathUrl.replace("\\", "/").substringAfterLast("/")
+ hasPhoto = true
+ }
+ if (attachment.attachmentType == AttachmentType.AUDIO) {
+ binding.tvAudioName.text = attachment.pathUrl.replace("\\", "/").substringAfterLast("/")
+ hasAudio = true
+ }
+ }
+ }
+ binding.layerPhotoResult.visibility = if (hasPhoto) VISIBLE else GONE
+ binding.layerGetPhoto.visibility = if (hasPhoto) GONE else VISIBLE
+ binding.layerAudioResult.visibility = if (hasAudio) VISIBLE else GONE
+ binding.layerGetAudio.visibility = if (hasAudio) GONE else VISIBLE
}
)
+ lifecycle.addObserver(recorderLifecycleObserver)
initView()
return root
}
@@ -52,12 +108,42 @@ class ObtainMessageFragment: Fragment() {
fun initView() {
// 设置问候信息提示的红色星号
binding.tiLayoutTitle.markRequiredInRed()
+ binding.tvMessageTitle.addTextChangedListener {
+ obtainMessageViewModel.updateMessageTitle(it.toString())
+ }
+
+ binding.imgPhotoDelete.setOnClickListener {
+ obtainMessageViewModel.updateMessagePic(null)
+ }
+
+ binding.imgAudioDelete.setOnClickListener {
+ obtainMessageViewModel.updateMessageAudio(null)
+ }
+
+ val sendToArray = mutableListOf("绑定车辆1(LYVXFEFEXNL754427)")
+ binding.edtSendTo.adapter = ArrayAdapter(context!!,
+ android.R.layout.simple_dropdown_item_1line, android.R.id.text1, sendToArray)
+ binding.edtSendTo.onItemSelectedListener = object: OnItemSelectedListener {
+ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
+ obtainMessageViewModel.getMessageLiveData().value?.toId = sendToArray[p2]
+ }
+
+ override fun onNothingSelected(p0: AdapterView<*>?) {
+ }
+
+ }
+
// 设置点击按钮选择发送时间
binding.btnSendTime.setOnClickListener {
val dialog = DateTimePickerFragment.newInstance().mode(0)
dialog.listener = object : DateTimePickerFragment.OnClickListener {
override fun onClickListener(selectTime: String) {
- obtainMessageViewModel.updateMessageSendTime(selectTime)
+ val sendDate = DateUtils.str2Date(selectTime, "yyyy-MM-dd HH:mm")
+ if (sendDate <= Date()) {
+ obtainMessageViewModel.updateMessageSendTime("现在")
+ } else {
+ obtainMessageViewModel.updateMessageSendTime(selectTime)
+ }
}
}
@@ -67,7 +153,195 @@ class ObtainMessageFragment: Fragment() {
// 点击按钮选择拍照
binding.btnStartCamera.setOnClickListener {
// 启动相机
- startCamera()
+ XXPermissions.with(this)
+ // 申请单个权限
+ .permission(Permission.CAMERA)
+ .request(object : OnPermissionCallback {
+
+ override fun onGranted(permissions: MutableList, all: Boolean) {
+ if (!all) {
+ Toast.makeText(activity, "获取部分权限成功,但部分权限未正常授予", Toast.LENGTH_SHORT).show()
+ return
+ }
+ // 开始启动拍照界面
+ photoHelper.setCrop(true).takePhoto(activity!!)
+ }
+
+ override fun onDenied(permissions: MutableList, never: Boolean) {
+ if (never) {
+ Toast.makeText(activity, "永久拒绝授权,请手动授权拍照权限", Toast.LENGTH_SHORT).show()
+ // 如果是被永久拒绝就跳转到应用权限系统设置页面
+ XXPermissions.startPermissionActivity(context!!, permissions)
+ } else {
+ onCameraDenied()
+ showRationaleForCamera(permissions)
+ }
+ }
+ })
+// startCamera(it)
+ }
+
+ binding.btnStartPhoto.setOnClickListener {
+ photoHelper.setCrop(true).selectPhoto(activity!!)
+ }
+
+ // 用户选择录音文件
+ binding.btnSelectSound.setOnClickListener {
+ photoHelper.setCrop(false).selectAudio(activity!!)
+ }
+
+ // 开始录音
+ binding.btnStartRecord.setOnClickListener {
+ // 申请权限
+ XXPermissions.with(this)
+ // 申请单个权限
+ .permission(Permission.RECORD_AUDIO)
+ .request(object : OnPermissionCallback {
+ override fun onGranted(permissions: MutableList, all: Boolean) {
+ if (!all) {
+ Toast.makeText(activity, "获取部分权限成功,但部分权限未正常授予", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ if (it.isSelected) {
+ it.isSelected = false
+ val recorderAudioPath = recorderLifecycleObserver.stopAndReleaseRecorder()
+ if (File(recorderAudioPath).exists()) {
+ obtainMessageViewModel.updateMessageAudio(recorderAudioPath)
+ }
+ } else{
+ it.isSelected = true
+
+ recorderLifecycleObserver.initAndStartRecorder()
+ }
+ }
+
+ override fun onDenied(permissions: MutableList, never: Boolean) {
+ if (never) {
+ Toast.makeText(activity, "永久拒绝授权,请手动授权拍照权限", Toast.LENGTH_SHORT).show()
+ // 如果是被永久拒绝就跳转到应用权限系统设置页面
+ XXPermissions.startPermissionActivity(context!!, permissions)
+ } else {
+ onCameraDenied()
+ showRationaleForCamera(permissions)
+ }
+ }
+ })
+ }
+
+ // 获取照片文件和音频文件
+ photoHelper.setCallback {
+ if (it.exists()) {
+ val fileName = it.name.lowercase()
+ if (fileName.endsWith(".jpg")||fileName.endsWith(".jpeg")||fileName.endsWith(".png")) {
+ // 获取选中的图片,自动压缩图片质量
+ // 压缩图片文件
+ Luban.with(context)
+ .load(mutableListOf(it) as List?)
+ .ignoreBy(200)
+ .setTargetDir("${SystemConstant.CameraFolder}")
+ .filter { path ->
+ !(TextUtils.isEmpty(path) || path.lowercase(Locale.getDefault())
+ .endsWith(".gif"))
+ }
+ .setCompressListener(object : OnCompressListener {
+ override fun onStart() {
+ XLog.d("开始压缩图片/${it.absolutePath}")
+ }
+
+ override fun onSuccess(file: File?) {
+ XLog.d("压缩图片成功:${file?.absolutePath}")
+ // 删除源文件
+ if (!it.absolutePath.equals(file?.absolutePath)) {
+ it?.delete()
+ }
+ // 跳转回原Fragment,展示拍摄的照片
+ ViewModelProvider(requireActivity()).get(ObtainMessageViewModel::class.java).updateMessagePic(file!!.absolutePath)
+ }
+
+ override fun onError(e: Throwable) {
+ XLog.d("压缩图片失败:${e.message}")
+ }
+ }).launch()
+ } else if (fileName.endsWith(".mp3")||fileName.endsWith(".wav")||fileName.endsWith(".amr")||fileName.endsWith(".m4a")) {
+ ToastUtils.showToast(it.absolutePath)
+ obtainMessageViewModel.updateMessageAudio(it.absolutePath)
+ }
+ }
+
+ }
+ photoHelper.setError {
+ ToastUtils.showToast(it.message)
+ }
+
+ binding.btnObtainMessageBack.setOnClickListener {
+ Navigation.findNavController(it).popBackStack()
+ }
+
+ binding.btnObtainMessageConfirm.setOnClickListener {
+ // 检查当前输入数据
+ val messageData = obtainMessageViewModel.getMessageLiveData().value
+ if (messageData?.title?.isEmpty() == true) {
+ val toolTipRelativeLayout =
+ binding.ttTitle
+ val toolTip = ToolTip()
+ .withText("请输入问候信息")
+ .withColor(com.navinfo.volvo.R.color.purple_200)
+ .withShadow()
+ .withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
+ toolTipRelativeLayout.showToolTipForView(toolTip, binding.tiLayoutTitle)
+ }
+ var hasPic = false
+ var hasAudio = false
+ for (attachment in messageData?.attachment!!) {
+ if (attachment.attachmentType == AttachmentType.PIC) {
+ hasPic = true
+ }
+ if (attachment.attachmentType == AttachmentType.AUDIO) {
+ hasAudio = true
+ }
+ }
+ if (!hasPic) {
+ val toolTipRelativeLayout =
+ binding.ttPic
+ val toolTip = ToolTip()
+ .withText("需要提供照片文件")
+ .withColor(com.navinfo.volvo.R.color.purple_200)
+ .withShadow()
+ .withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
+ toolTipRelativeLayout.showToolTipForView(toolTip, binding.tvUploadPic)
+ }
+ if (!hasAudio) {
+ val toolTipRelativeLayout =
+ binding.ttAudio
+ val toolTip = ToolTip()
+ .withText("需要提供音频文件")
+ .withColor(com.navinfo.volvo.R.color.purple_200)
+ .withShadow()
+ .withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
+ toolTipRelativeLayout.showToolTipForView(toolTip, binding.tvUploadPic)
+ }
+
+ if (messageData?.fromId?.isEmpty()==true) {
+ val toolTipRelativeLayout =
+ binding.ttSendFrom
+ val toolTip = ToolTip()
+ .withText("请输入您的名称")
+ .withColor(com.navinfo.volvo.R.color.purple_200)
+ .withShadow()
+ .withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
+ toolTipRelativeLayout.showToolTipForView(toolTip, binding.edtSendFrom)
+ }
+ if (messageData?.toId?.isEmpty()==true) {
+ val toolTipRelativeLayout =
+ binding.ttSendTo
+ val toolTip = ToolTip()
+ .withText("请选择要发送的车辆")
+ .withColor(com.navinfo.volvo.R.color.purple_200)
+ .withShadow()
+ .withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
+ toolTipRelativeLayout.showToolTipForView(toolTip, binding.edtSendTo)
+ }
}
}
@@ -76,32 +350,44 @@ class ObtainMessageFragment: Fragment() {
_binding = null
}
- @NeedsPermission(Manifest.permission.CAMERA)
- fun startCamera() {
-
+ fun startCamera(it: View) {
+ Navigation.findNavController(binding.root).navigate(com.navinfo.volvo.R.id.nav_2_camera)
}
- @OnShowRationale(Manifest.permission.CAMERA)
- fun showRationaleForCamera(request: PermissionRequest) {
+ fun showRationaleForCamera(permissions: MutableList) {
// showRationaleDialog(R.string.permission_camera_rationale, request)
// Toast.makeText(context, "当前操作需要您授权相机权限!", Toast.LENGTH_SHORT).show()
MaterialAlertDialogBuilder(context!!)
.setTitle("提示")
- .setMessage("当前操作需要您授权相机权限!")
+ .setMessage("当前操作需要您授权拍摄权限!")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
- startCamera()
dialogInterface.dismiss()
- })
+ XXPermissions.startPermissionActivity(activity!!, permissions)
+ })
.show()
}
- @OnPermissionDenied(Manifest.permission.CAMERA)
+ // @OnPermissionDenied(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
fun onCameraDenied() {
- Toast.makeText(context, "当前操作需要您授权相机权限!", Toast.LENGTH_SHORT).show()
+ ToastUtils.showToast("当前操作需要您授权拍摄权限!")
}
- @OnNeverAskAgain(Manifest.permission.CAMERA)
- fun onCameraNeverAskAgain() {
- Toast.makeText(context, "您已永久拒绝授权相机权限!", Toast.LENGTH_SHORT).show()
+ fun showRationaleForRecorder(permissions: MutableList) {
+ MaterialAlertDialogBuilder(context!!)
+ .setTitle("提示")
+ .setMessage("当前操作需要您授权录音权限!")
+ .setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
+ dialogInterface.dismiss()
+ XXPermissions.startPermissionActivity(activity!!, permissions)
+ })
+ .show()
+ }
+ fun onRecorderDenied() {
+ ToastUtils.showToast("当前操作需要您授权录音权限!")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ lifecycle.removeObserver(recorderLifecycleObserver)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageViewModel.kt b/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageViewModel.kt
index 2ce11d2..795ef28 100644
--- a/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageViewModel.kt
+++ b/app/src/main/java/com/navinfo/volvo/ui/message/ObtainMessageViewModel.kt
@@ -4,8 +4,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
+import com.navinfo.volvo.db.dao.entity.Attachment
import com.navinfo.volvo.db.dao.entity.Message
import com.navinfo.volvo.db.dao.entity.AttachmentType
+import java.util.UUID
class ObtainMessageViewModel: ViewModel() {
private val msgLiveData: MutableLiveData by lazy {
@@ -27,22 +29,41 @@ class ObtainMessageViewModel: ViewModel() {
}
// 更新消息附件中的照片文件
- fun updateMessagePic(picUrl: String) {
+ fun updateMessagePic(picUrl: String?) {
+ var hasPic = false
+
for (attachment in this.msgLiveData.value!!.attachment) {
if (attachment.attachmentType == AttachmentType.PIC) {
- attachment.pathUrl = picUrl
+ if (picUrl==null||picUrl.isEmpty()) {
+ this.msgLiveData.value!!.attachment.remove(attachment)
+ } else {
+ attachment.pathUrl = picUrl
+ }
+ hasPic = true
}
}
+ if (!hasPic&&picUrl!=null) {
+ this.msgLiveData.value!!.attachment.add(Attachment(UUID.randomUUID().toString(), picUrl, AttachmentType.PIC))
+ }
this.msgLiveData.postValue(this.msgLiveData.value)
}
// 更新消息附件中的录音文件
- fun updateMessageAudio(audioUrl: String) {
+ fun updateMessageAudio(audioUrl: String?) {
+ var hasAudio = false
for (attachment in this.msgLiveData.value!!.attachment) {
if (attachment.attachmentType == AttachmentType.AUDIO) {
- attachment.pathUrl = audioUrl
+ if (audioUrl==null||audioUrl.isEmpty()) {
+ this.msgLiveData.value!!.attachment.remove(attachment)
+ } else {
+ attachment.pathUrl = audioUrl
+ }
+ hasAudio = true
}
}
+ if (!hasAudio&&audioUrl!=null) {
+ this.msgLiveData.value!!.attachment.add(Attachment(UUID.randomUUID().toString(), audioUrl, AttachmentType.AUDIO))
+ }
this.msgLiveData.postValue(this.msgLiveData.value)
}
diff --git a/app/src/main/java/com/navinfo/volvo/ui/widget/SlideRecyclerView.kt b/app/src/main/java/com/navinfo/volvo/ui/widget/SlideRecyclerView.kt
deleted file mode 100644
index 16a9f38..0000000
--- a/app/src/main/java/com/navinfo/volvo/ui/widget/SlideRecyclerView.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-package com.navinfo.volvo.ui.widget
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.graphics.Rect
-import android.util.AttributeSet
-import android.view.*
-import android.widget.Scroller
-import androidx.core.view.forEach
-import androidx.recyclerview.widget.RecyclerView
-import java.lang.Math.abs
-
-class SlideRecyclerView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : RecyclerView(context, attrs, defStyleAttr) {
- //系统最小移动距离
- private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
-
- //最小有效速度
- private val mMinVelocity = 600
-
- //增加手势控制,双击快速完成侧滑
- private var isDoubleClick = false
- private var mGestureDetector: GestureDetector =
- GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
- override fun onDoubleTap(e: MotionEvent?): Boolean {
- e?.let { event ->
- getSelectItem(event)
- mItem?.let {
- val deleteWith = it.getChildAt(it.childCount - 1).width
- //触发移动至完全展开deleteWidth
- if (it.scrollX == 0) {
- mScroller.startScroll(0, 0, deleteWith, 0)
- } else {
- mScroller.startScroll(it.scrollX, 0, -it.scrollX, 0)
- }
- isDoubleClick = true
- invalidate()
- return true
- }
- }
- //不进行拦截,只作为工具判断下双击
- return false
- }
- })
-
- //使用速度控制器,增加侧滑速度判定滑动成功,
- //VelocityTracker 由native实现,需要及时释放内存
- private var mVelocityTracker: VelocityTracker? = null
-
- //流畅滑动
- private var mScroller = Scroller(context)
-
- //当前选中item
- private var mItem: ViewGroup? = null
-
- //上次按下的横坐标
- private var mLastX = 0f
-
- //当前RecyclerView被上层ViewGroup分发到事件,所有事件都会通过dispatchTouchEvent给到
- override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
- mGestureDetector.onTouchEvent(ev)
- return super.dispatchTouchEvent(ev)
- }
-
- //viewGroup对子控件的事件拦截,一旦拦截,后续事件序列不会再调用onInterceptTouchEvent
- override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
- e?.let {
- when (e.action) {
- MotionEvent.ACTION_DOWN -> {
- getSelectItem(e)
- mLastX = e.x
- }
- MotionEvent.ACTION_MOVE -> {
- //移动控件
- return moveItem(e)
- }
-// MotionEvent.ACTION_UP -> {
-// stopMove(e)
-// }
- }
- }
- return super.onInterceptTouchEvent(e)
- }
-
- @SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(e: MotionEvent?): Boolean {
- e?.let {
- when (e.action) {
- MotionEvent.ACTION_MOVE -> {
- moveItem(e)
- mLastX = e.x
- }
- MotionEvent.ACTION_UP -> {
- stopMove()
- }
- }
- }
- return super.onTouchEvent(e)
- }
-
- //活动结束
- //判断一下结束的位置,补充或恢复位置
- private fun stopMove() {
- mItem?.let {
- //如果移动过半,判定左划成功
- val deleteWidth = it.getChildAt(it.childCount - 1).width
- //如果整个移动过程速度大于600,也判定滑动成功
- //注意如果没有拦截ACTION_MOVE,mVelocityTracker是没有初始化的
- var velocity = 0f
- mVelocityTracker?.let { tracker ->
- tracker.computeCurrentVelocity(1000)
- velocity = tracker.xVelocity
- }
- //判断结束情况,移动过半或者向左速度很快都展开
- if ((abs(it.scrollX) >= deleteWidth / 2f) || (velocity < -mMinVelocity)) {
- //触发移动至完全展开
- mScroller.startScroll(it.scrollX, 0, deleteWidth - it.scrollX, 0)
- invalidate()
- } else {
- //如果移动未过半应恢复状态
- mScroller.startScroll(it.scrollX, 0, -it.scrollX, 0)
- invalidate()
- }
- }
- //清除状态
- mLastX = 0f
- //mVeloctityTracker由native实现,需要及时释放
- mVelocityTracker?.apply {
- clear()
- recycle()
- }
- mVelocityTracker = null
- }
-
- //移动Item
- //绝对值小于删除按钮长度随便移动,大于则不移动
- @SuppressLint("Recycle")
- private fun moveItem(e: MotionEvent): Boolean {
- mItem?.let {
- val dx = mLastX - e.x
- //最小的移动距离应该舍弃,onInterceptTouchEvent不拦截,onTouchEvent内才更新mLastX
-// if (abs(dx) > mTouchSlop) {
- //检查mItem移动后应该在【-deleteLength,0】内
- val deleteWith = it.getChildAt(it.childCount - 1).width
- if ((it.scrollX + dx) <= deleteWith && (it.scrollX + dx) >= 0) {
- //触发移动
- it.scrollBy(dx.toInt(), 0)
- //触发速度计算
- //这里Rectycle不存在问题,一旦返回true,就会拦截事件,就会到达ACTION_UP去回收
- mVelocityTracker = mVelocityTracker ?: VelocityTracker.obtain()
- mVelocityTracker!!.addMovement(e)
- return true
-// }
- }
-
-
- }
- return false
- }
-
- //获取点击位置
- //通过点击的y坐标除以Item高度得出
- private fun getSelectItem(e: MotionEvent) {
- val frame = Rect()
- mItem = null
- forEach {
- if (it.visibility != GONE) {
- it.getHitRect(frame)
- if (frame.contains(e.x.toInt(), e.y.toInt())) {
- mItem = it as ViewGroup
- }
- }
- }
- }
-
- //流畅地滑动
- override fun computeScroll() {
- if (mScroller.computeScrollOffset()) {
- mItem?.scrollBy(mScroller.currX, mScroller.currY)
- postInvalidate()
- }
- }
-}
-
diff --git a/app/src/main/java/com/navinfo/volvo/utils/EasyMediaFile.kt b/app/src/main/java/com/navinfo/volvo/utils/EasyMediaFile.kt
new file mode 100644
index 0000000..e04f57d
--- /dev/null
+++ b/app/src/main/java/com/navinfo/volvo/utils/EasyMediaFile.kt
@@ -0,0 +1,648 @@
+package com.navinfo.volvo.utils
+
+import android.app.Activity
+import android.app.Fragment
+import android.content.*
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.*
+import android.os.Environment.*
+import android.provider.MediaStore
+import java.io.File
+import java.util.*
+
+
+/**
+ * 创建日期:2018/8/21 0021on 下午 4:40
+ * 描述:多媒体选择工具类
+ * @author:Vincent
+ */
+class EasyMediaFile {
+ /**
+ * 设置图片选择结果回调
+ */
+ private var callback: ((file: File) -> Unit)? = null
+ private var isCrop: Boolean = false
+ private var error: ((error: Exception) -> Unit)? = null
+
+ /**
+ * 视频录制/音频录制/拍照/剪切后图片的存放位置(参考file_provider_paths.xml中的路径)
+ */
+ private var mFilePath: File? = null
+
+ private val mainHandler = Handler(Looper.getMainLooper())
+
+ fun setError(error: ((error: Exception) -> Unit)?): EasyMediaFile {
+ this.error = error
+ return this
+ }
+
+ fun setCallback(callback: ((file: File) -> Unit)): EasyMediaFile {
+ this.callback = callback
+ return this
+ }
+
+ fun setCrop(isCrop: Boolean): EasyMediaFile {
+ this.isCrop = isCrop
+ return this
+ }
+
+ /**
+ * 修改图片的存储路径(默认的图片存储路径是SD卡上 Android/data/应用包名/时间戳.jpg)
+ *
+ * @param imgPath 图片的存储路径(包括文件名和后缀)
+ */
+ fun setFilePath(imgPath: String?): EasyMediaFile {
+ if (imgPath.isNullOrEmpty()) {
+ this.mFilePath = null
+ } else {
+ this.mFilePath = File(imgPath)
+ this.mFilePath?.parentFile?.mkdirs()
+ }
+ return this
+ }
+
+
+ /**
+ * 选择文件
+ * 支持图片、音频、视频
+ */
+// fun selectFile(activity: Activity) {
+// isCrop = false
+// val intent = Intent(Intent.ACTION_PICK, null).apply {
+// setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "*/*")
+// setDataAndType(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "*/*")
+// setDataAndType(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "*/*")
+// }
+// if (Looper.myLooper() == Looper.getMainLooper()) {
+// selectFileInternal(intent, activity, -1)
+// } else {
+// mainHandler.post { selectFileInternal(intent, activity, -1) }
+// }
+// }
+
+ /**
+ * 选择视频
+ */
+ fun selectVideo(activity: Activity) {
+ isCrop = false
+ val intent = Intent(Intent.ACTION_PICK, null).apply {
+ setDataAndType(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/*")
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ selectFileInternal(intent, activity, 2)
+ } else {
+ mainHandler.post { selectFileInternal(intent, activity, 2) }
+ }
+ }
+
+ /**
+ * 选择音频
+ */
+ fun selectAudio(activity: Activity) {
+ isCrop = false
+ val intent = Intent(Intent.ACTION_PICK, null).apply {
+ setDataAndType(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "audio/*")
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ selectFileInternal(intent, activity, 1)
+ } else {
+ mainHandler.post { selectFileInternal(intent, activity, 1) }
+ }
+ }
+
+ /**
+ * 选择图片
+ */
+ fun selectPhoto(activity: Activity) {
+ val intent = Intent(Intent.ACTION_PICK, null).apply {
+ setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ selectFileInternal(intent, activity, 0)
+ } else {
+ mainHandler.post { selectFileInternal(intent, activity, 0) }
+ }
+ }
+
+
+ /**
+ * 选择文件
+ */
+ private fun selectFileInternal(intent: Intent, activity: Activity, type: Int) {
+ val resolveInfoList = activity.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
+ if (resolveInfoList.isEmpty()) {
+ error?.invoke(IllegalStateException("No Activity found to handle Intent "))
+ } else {
+ PhotoFragment.findOrCreate(activity).start(intent, PhotoFragment.REQ_SELECT_FILE) { requestCode: Int, data: Intent? ->
+ if (requestCode != PhotoFragment.REQ_SELECT_FILE) {
+ return@start
+ }
+ data ?: return@start
+ data.data ?: return@start
+ try {
+ val inputFile = if (type != -1) {
+ uriToFile(activity, data.data!!, type)
+ } else {
+ if (data.data!!.path!!.contains(".")) {
+ File(data.data!!.path!!)
+ } else {
+ when {
+ data.data!!.path!!.contains("images") -> {
+ uriToFile(activity, data.data!!, 0)
+ }
+ data.data!!.path!!.contains("video") -> {
+ uriToFile(activity, data.data!!, 2)
+ }
+ else -> {
+ uriToFile(activity, data.data!!, 1)
+ }
+ }
+ }
+ }
+ if (isCrop) {//裁剪
+ zoomPhoto(inputFile, mFilePath
+ ?: File(generateFilePath(activity)), activity)
+ } else {//不裁剪
+ callback?.invoke(inputFile)
+ }
+ } catch (e: Exception) {
+ error?.invoke(e)
+ }
+ }
+ }
+
+ }
+
+ private fun uriToFile(activity: Activity, uri: Uri): File {
+
+ // 首先使用系统提供的CursorLoader进行file获取
+ val context = activity.application
+ val projection = arrayOf(MediaStore.Images.Media.DATA)
+ var path: String
+ try {
+ CursorLoader(context, uri, projection, null, null, null)
+ .loadInBackground().apply {
+ getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
+ val index = getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
+ moveToFirst()
+ path = getString(index)
+ close()
+
+ }
+ return File(path)
+ } catch (e: Exception) {
+ // 当没获取到。再使用别的方式进行获取
+ val scheme = uri.scheme
+ path = uri.path ?: throw RuntimeException("Could not find path in this uri:[$uri]")
+ when (scheme) {
+ "file" -> {
+ val cr = context.contentResolver
+ val buff = StringBuffer()
+ buff.append("(").append(MediaStore.Images.ImageColumns.DATA).append("=").append("'$path'").append(")")
+ cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, arrayOf(MediaStore.Images.ImageColumns._ID,
+ MediaStore.Images.ImageColumns.DATA), buff.toString(), null, null).apply {
+ this ?: throw RuntimeException("cursor is null")
+ var dataIdx: Int
+ while (!this.isAfterLast) {
+ dataIdx = this.getColumnIndex(MediaStore.Images.ImageColumns.DATA)
+ path = this.getString(dataIdx)
+ this.moveToNext()
+ }
+ close()
+ }
+
+ return File(path)
+ }
+ "content" -> {
+
+ context.contentResolver.query(uri, arrayOf(MediaStore.Images.Media.DATA), null, null, null).apply {
+ this ?: throw RuntimeException("cursor is null")
+ if (this.moveToFirst()) {
+ val columnIndex = this.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
+ path = this.getString(columnIndex)
+ }
+ close()
+ }
+ return File(path)
+
+ }
+ else -> {
+ throw IllegalArgumentException("Could not find file by this uri:$uri")
+ }
+ }
+
+ }
+ }
+
+ /**
+ * 拍照获取
+ */
+ fun takePhoto(activity: Activity) {
+ val imgFile = if (isCrop) {
+ File(generateFilePath(activity))
+ } else {
+ mFilePath ?: File(generateFilePath(activity))
+ }
+
+ val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ Uri.fromFile(imgFile)
+ } else {
+ //兼容android7.0 使用共享文件的形式
+ val contentValues = ContentValues(1)
+ contentValues.put(MediaStore.Images.Media.DATA, imgFile.absolutePath)
+ activity.application.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ }
+
+ val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ takeFileInternal(imgFile, intent, activity)
+ } else {
+ mainHandler.post { takeFileInternal(imgFile, intent, activity) }
+ }
+ }
+
+ /**
+ * 音频录制
+ */
+ fun takeAudio(activity: Activity) {
+ isCrop = false
+ val imgFile = mFilePath ?: File(generateFilePath(activity, 1))
+ val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ Uri.fromFile(imgFile)
+ } else {
+ //兼容android7.0 使用共享文件的形式
+ val contentValues = ContentValues(1)
+ contentValues.put(MediaStore.Audio.Media.DATA, imgFile.absolutePath)
+ activity.application.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues)
+ }
+
+ val intent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ takeFileInternal(imgFile, intent, activity, 1)
+ } else {
+ mainHandler.post { takeFileInternal(imgFile, intent, activity, 1) }
+ }
+
+ }
+
+ /**
+ * 视频录制
+ */
+ fun takeVideo(activity: Activity) {
+ isCrop = false
+ val imgFile = mFilePath ?: File(generateFilePath(activity, 2))
+ val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ Uri.fromFile(imgFile)
+ } else {
+ //兼容android7.0 使用共享文件的形式
+ val contentValues = ContentValues(1)
+ contentValues.put(MediaStore.Video.Media.DATA, imgFile.absolutePath)
+ activity.application.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
+ }
+
+ val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
+ putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
+ // 默认录制时间10秒 部分手机该设置无效
+// putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10000)
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ takeFileInternal(imgFile, intent, activity, 2)
+ } else {
+ mainHandler.post { takeFileInternal(imgFile, intent, activity, 2) }
+ }
+
+ }
+
+ /**
+ * 拍照或选择
+ */
+ fun getImage(activity: Activity) {
+
+ val imgFile = if (isCrop) {
+ File(generateFilePath(activity))
+ } else {
+ mFilePath ?: File(generateFilePath(activity))
+ }
+
+ val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ Uri.fromFile(imgFile)
+ } else {
+ //兼容android7.0 使用共享文件的形式
+ val contentValues = ContentValues(1)
+ contentValues.put(MediaStore.Images.Media.DATA, imgFile.absolutePath)
+ activity.application.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ }
+
+
+ val cameraIntents = ArrayList()
+ val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ val packageManager = activity.packageManager
+ val camList = packageManager.queryIntentActivities(captureIntent, 0)
+ for (res in camList) {
+ val packageName = res.activityInfo.packageName
+ val intent = Intent(captureIntent)
+ intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
+ intent.setPackage(packageName)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
+ cameraIntents.add(intent)
+ }
+ val intent = Intent.createChooser(createPickMore(), "请选择").also {
+ it.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
+
+ }
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ takeFileInternal(imgFile, intent, activity)
+ } else {
+ mainHandler.post { takeFileInternal(imgFile, intent, activity) }
+ }
+ }
+
+ /**
+ * 音频录制或选择
+ */
+ fun getAudio(activity: Activity) {
+ isCrop = false
+ val imgFile = mFilePath ?: File(generateFilePath(activity))
+
+ val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ Uri.fromFile(imgFile)
+ } else {
+ //兼容android7.0 使用共享文件的形式
+ val contentValues = ContentValues(1)
+ contentValues.put(MediaStore.Audio.Media.DATA, imgFile.absolutePath)
+ activity.application.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues)
+ }
+ val cameraIntents = ArrayList()
+ val captureIntent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
+ val packageManager = activity.packageManager
+ val camList = packageManager.queryIntentActivities(captureIntent, 0)
+ for (res in camList) {
+ val packageName = res.activityInfo.packageName
+ val intent = Intent(captureIntent)
+ intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
+ intent.setPackage(packageName)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
+ cameraIntents.add(intent)
+ }
+ val intent = Intent.createChooser(createPickMore("audio/*"), "请选择").also {
+ it.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
+
+ }
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ takeFileInternal(imgFile, intent, activity, 1)
+ } else {
+ mainHandler.post { takeFileInternal(imgFile, intent, activity, 1) }
+ }
+ }
+
+ /**
+ * 视频拍摄或选择
+ */
+ fun getVideo(activity: Activity) {
+ isCrop = false
+ val imgFile = mFilePath ?: File(generateFilePath(activity, 2))
+
+ val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ Uri.fromFile(imgFile)
+ } else {
+ //兼容android7.0 使用共享文件的形式
+ val contentValues = ContentValues(1)
+ contentValues.put(MediaStore.Video.Media.DATA, imgFile.absolutePath)
+ activity.application.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
+ }
+ val cameraIntents = ArrayList()
+ val captureIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
+ // 某些手机此设置是不生效的,需要自行封装解决
+// captureIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10000)
+ val packageManager = activity.packageManager
+ val camList = packageManager.queryIntentActivities(captureIntent, 0)
+ for (res in camList) {
+ val packageName = res.activityInfo.packageName
+ val intent = Intent(captureIntent)
+ intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
+ intent.setPackage(packageName)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
+ cameraIntents.add(intent)
+ }
+ val intent = Intent.createChooser(createPickMore("video/*"), "请选择").also {
+ it.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
+
+ }
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ takeFileInternal(imgFile, intent, activity, 2)
+ } else {
+ mainHandler.post { takeFileInternal(imgFile, intent, activity, 2) }
+ }
+ }
+
+ /**
+ * 向系统发出指令
+ */
+ private fun takeFileInternal(takePhotoPath: File, intent: Intent, activity: Activity, type: Int = 0) {
+ val fragment = PhotoFragment.findOrCreate(activity)
+ fragment.start(intent, PhotoFragment.REQ_TAKE_FILE) { requestCode: Int, data: Intent? ->
+ if (requestCode == PhotoFragment.REQ_TAKE_FILE) {
+ if (data?.data != null) {
+ mFilePath = when (type) {
+ 0 -> {
+ uriToFile(activity, data.data!!)
+ }
+ else -> uriToFile(activity, data.data!!, type)
+ }
+
+ if (isCrop) {
+ zoomPhoto(takePhotoPath, mFilePath
+ ?: File(generateFilePath(activity)), activity)
+ } else {
+ callback?.invoke(mFilePath!!)
+ mFilePath = null
+ }
+ return@start
+ }
+ if (isCrop) {
+ zoomPhoto(takePhotoPath, mFilePath
+ ?: File(generateFilePath(activity)), activity)
+ } else {
+ callback?.invoke(takePhotoPath)
+ }
+ }
+ }
+ }
+
+ private fun uriToFile(activity: Activity, data: Uri, type: Int): File {
+ val cursor = activity.managedQuery(data, arrayOf(if (type == 1) MediaStore.Audio.Media.DATA else MediaStore.Video.Media.DATA), null,
+ null, null)
+ val path = if (cursor == null) {
+ data.path
+ } else {
+ val index = cursor.getColumnIndexOrThrow(if (type == 1) MediaStore.Audio.Media.DATA else MediaStore.Video.Media.DATA)
+ cursor.moveToFirst()
+ cursor.getString(index)
+ }
+ // 手动关掉报错如下
+// Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
+// cursor.close()
+ return File(path!!)
+ }
+
+ /***
+ * 图片裁剪
+ */
+ private fun zoomPhoto(inputFile: File?, outputFile: File, activity: Activity) {
+ try {
+ val intent = Intent("com.android.camera.action.CROP")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ intent.setDataAndType(getImageContentUri(activity, inputFile), "image/*")
+ } else {
+ intent.setDataAndType(Uri.fromFile(inputFile), "image/*")
+ }
+ intent.putExtra("crop", "true")
+
+ // 是否返回uri
+ intent.putExtra("return-data", false)
+ intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val imgFile = File("${Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES)}/${outputFile.name}")
+ // 通过 MediaStore API 插入file 为了拿到系统裁剪要保存到的uri(因为App没有权限不能访问公共存储空间,需要通过 MediaStore API来操作)
+ val values = ContentValues()
+ values.put(MediaStore.Images.Media.DATA, imgFile.getAbsolutePath());
+ values.put(MediaStore.Images.Media.DISPLAY_NAME, outputFile.name);
+ values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
+ val uri = activity.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile))
+ zoomPhotoInternal(outputFile, intent, activity)
+ }else {
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile))
+ zoomPhotoInternal(outputFile, intent, activity)
+ }
+ } catch (e: Exception) {
+ error?.invoke(e)
+ }
+
+ }
+
+ private fun zoomPhotoInternal(outputFile: File, intent: Intent, activity: Activity) {
+ PhotoFragment.findOrCreate(activity).start(intent, PhotoFragment.REQ_ZOOM_PHOTO) { requestCode: Int, data: Intent? ->
+ if (requestCode == PhotoFragment.REQ_ZOOM_PHOTO) {
+ data ?: return@start
+ callback?.invoke(outputFile)
+ }
+ }
+ }
+
+ /**构建文件多选Intent*/
+ private fun createPickMore(fileType: String = "image/*"): Intent {
+ val pictureChooseIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
+ type = fileType
+ }
+ pictureChooseIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
+ /**临时授权app访问URI代表的文件所有权*/
+ pictureChooseIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ return pictureChooseIntent
+ }
+ /**
+ * 产生图片的路径,带文件夹和文件名,文件名为当前毫秒数
+ */
+ private fun generateFilePath(activity: Activity, fileType: Int = 0): String {
+ val file = when (fileType) {
+ // 音频路径
+ 1 -> "${SystemConstant.SoundFolder}" + File.separator + System.currentTimeMillis().toString() + ".m4a"
+ // 视频路径
+ 2 -> "${SystemConstant.CameraFolder}" + File.separator + System.currentTimeMillis().toString() + ".mp4"
+ // 图片路径
+ else -> "${SystemConstant.CameraFolder}" + File.separator + System.currentTimeMillis().toString() + ".jpg"
+ }
+ File(file).parentFile.mkdirs()
+ return file
+ }
+ /**
+ * 获取SD下的应用目录
+ */
+ private fun getExternalStoragePath(activity: Activity): String {
+ val sb = "${activity.getExternalFilesDir(null)}/tmp"
+ return sb
+ }
+ /**
+ * 安卓7.0裁剪根据文件路径获取uri
+ */
+ private fun getImageContentUri(context: Context, imageFile: File?): Uri? {
+ val filePath = imageFile?.absolutePath
+ val cursor = context.contentResolver.query(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ arrayOf(MediaStore.Images.Media._ID),
+ MediaStore.Images.Media.DATA + "=? ",
+ arrayOf(filePath), null)
+ cursor.use { _ ->
+ return if (cursor != null && cursor.moveToFirst()) {
+ val id = cursor.getInt(cursor
+ .getColumnIndex(MediaStore.MediaColumns._ID))
+ val baseUri = Uri.parse("content://media/external/images/media")
+ Uri.withAppendedPath(baseUri, "" + id)
+ } else {
+ imageFile?.let {
+ if (it.exists()) {
+ val values = ContentValues()
+ values.put(MediaStore.Images.Media.DATA, filePath)
+ context.contentResolver.insert(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
+ } else {
+ null
+ }
+ }
+ }
+ }
+ }
+ /**
+ * 用于获取图片的Fragment
+ */
+ class PhotoFragment : Fragment() {
+ /**
+ * Fragment处理照片后返回接口
+ */
+ private var callback: ((requestCode: Int, intent: Intent?) -> Unit)? = null
+ /**
+ * 开启系统相册
+ * 裁剪图片、打开相册选择单张图片、拍照
+ */
+ fun start(intent: Intent, requestCode: Int, callback: ((requestCode: Int, intent: Intent?) -> Unit)) {
+ this.callback = callback
+ startActivityForResult(intent, requestCode)
+ }
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (resultCode == Activity.RESULT_OK) {
+ callback?.invoke(requestCode, data)
+ }
+ }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ retainInstance = true
+ }
+ companion object {
+ const val REQ_TAKE_FILE = 10001
+ const val REQ_SELECT_FILE = 10002
+ const val REQ_ZOOM_PHOTO = 10003
+ private const val TAG = "EasyPhoto:PhotoFragment"
+ @JvmStatic
+ fun findOrCreate(activity: Activity): PhotoFragment {
+ var fragment: PhotoFragment? = activity.fragmentManager.findFragmentByTag(TAG) as PhotoFragment?
+ if (fragment == null) {
+ fragment = PhotoFragment()
+ activity.fragmentManager.beginTransaction()
+ .add(fragment, TAG)
+ .commitAllowingStateLoss()
+ activity.fragmentManager.executePendingTransactions()
+ }
+ return fragment
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/utils/SystemConstant.kt b/app/src/main/java/com/navinfo/volvo/utils/SystemConstant.kt
new file mode 100644
index 0000000..0cc1857
--- /dev/null
+++ b/app/src/main/java/com/navinfo/volvo/utils/SystemConstant.kt
@@ -0,0 +1,10 @@
+package com.navinfo.volvo.utils
+
+class SystemConstant {
+ companion object {
+ lateinit var ROOT_PATH: String
+ lateinit var CameraFolder: String
+ lateinit var SoundFolder: String
+ lateinit var LogFolder: String
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_add_24dp.xml b/app/src/main/res/drawable/ic_add_24dp.xml
new file mode 100644
index 0000000..89633bb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_delete_24.xml b/app/src/main/res/drawable/ic_baseline_delete_24.xml
new file mode 100644
index 0000000..de011dd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_delete_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_person_24.xml b/app/src/main/res/drawable/ic_baseline_person_24.xml
new file mode 100644
index 0000000..98730cd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_person_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_volume_down_24.xml b/app/src/main/res/drawable/ic_baseline_volume_down_24.xml
new file mode 100644
index 0000000..f3ae1ba
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_volume_down_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml b/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml
new file mode 100644
index 0000000..16a576c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_volume_up_24.xml b/app/src/main/res/drawable/ic_baseline_volume_up_24.xml
new file mode 100644
index 0000000..2551246
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_volume_up_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 9012e8f..cd25770 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -12,6 +12,7 @@
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
+ app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@@ -29,4 +30,26 @@
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_message.xml b/app/src/main/res/layout/activity_message.xml
new file mode 100644
index 0000000..540fa58
--- /dev/null
+++ b/app/src/main/res/layout/activity_message.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_message.xml b/app/src/main/res/layout/adapter_message.xml
index aa728db..0137030 100644
--- a/app/src/main/res/layout/adapter_message.xml
+++ b/app/src/main/res/layout/adapter_message.xml
@@ -9,7 +9,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
- android:src="@mipmap/ic_launcher"
+ android:src="@mipmap/volvo_logo_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/layout/content_message.xml b/app/src/main/res/layout/content_message.xml
new file mode 100644
index 0000000..66d305d
--- /dev/null
+++ b/app/src/main/res/layout/content_message.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_camera.xml b/app/src/main/res/layout/fragment_camera.xml
index e935007..d839372 100644
--- a/app/src/main/res/layout/fragment_camera.xml
+++ b/app/src/main/res/layout/fragment_camera.xml
@@ -6,8 +6,8 @@
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_obtain_message.xml b/app/src/main/res/layout/fragment_obtain_message.xml
index cf35e14..0a482c1 100644
--- a/app/src/main/res/layout/fragment_obtain_message.xml
+++ b/app/src/main/res/layout/fragment_obtain_message.xml
@@ -27,7 +27,7 @@
app:counterEnabled="true"
app:counterMaxLength="10"
app:errorEnabled="true"
- android:hint="问候信息"
+ android:hint="请输入问候信息"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -40,13 +40,19 @@
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
+
+
+ android:text="问候附件"
+ app:layout_constraintTop_toBottomOf="@id/tt_title">
-
-
-
-
+
+
+
+
+
+ android:gravity="center"
+ android:visibility="gone"
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ android:gravity="center"
+ android:visibility="gone"
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
-
+ android:layout_height="wrap_content">
+
+
-
+
@@ -252,6 +360,7 @@
app:layout_constraintBottom_toBottomOf="parent">
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml
index 1ef3117..9274e30 100644
--- a/app/src/main/res/menu/bottom_nav_menu.xml
+++ b/app/src/main/res/menu/bottom_nav_menu.xml
@@ -5,16 +5,22 @@
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home" />
-
-
+
+
+
+
+
+
+
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..6806c11
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml
new file mode 100644
index 0000000..22d7f00
--- /dev/null
+++ b/app/src/main/res/values-land/dimens.xml
@@ -0,0 +1,3 @@
+
+ 48dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index aada80e..4dfdfbe 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,6 +1,6 @@
-
+
+
+
+
+
+
\ No newline at end of file