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" /> + + + + +