feat: 初始化项目结构和基本功能

- 创建项目根目录和主要子模块
- 添加基本的 Activity 和布局文件
- 实现简单的导航和电话拨打功能
- 添加相机和图像处理相关代码
- 创建网络请求和数据加密工具类
- 设置 AndroidManifest 文件和权限
This commit is contained in:
songzhiling
2025-04-11 11:52:07 +08:00
parent 74ce8fc526
commit 91305ab9d1
386 changed files with 36517 additions and 34 deletions

View File

@ -0,0 +1,113 @@
package com.za.common
import android.app.Application
import com.amap.api.location.AMapLocation
import com.tencent.mmkv.MMKV
import com.za.bean.DriverInfo
import com.za.bean.VehicleInfo
import com.za.bean.db.order.OrderInfo
import com.za.room.RoomHelper
object GlobalData {
lateinit var application : Application
var activityCount : Int = 0
var token : String? = null
get() {
return MMKV.defaultMMKV().decodeString("TOKEN", null)
}
set(value) {
MMKV.defaultMMKV().encode("TOKEN", value)
field = value
}
//记录上次登录的手机号
var lastLoginPhone : String? = null
get() {
return MMKV.defaultMMKV().decodeString("lastLoginPhone", null)
}
set(value) {
MMKV.defaultMMKV().encode("lastLoginPhone", value)
field = value
}
var aesKey : String? = null
get() {
return MMKV.defaultMMKV().decodeString("AES_KEY", null)
}
set(value) {
MMKV.defaultMMKV().encode("AES_KEY", value)
field = value
}
var driverInfo : DriverInfo? = null
get() {
return MMKV.defaultMMKV().decodeParcelable("driverInfo", DriverInfo::class.java)
}
set(value) {
MMKV.defaultMMKV().encode("driverInfo", value)
field = value
}
var vehicleInfo : VehicleInfo? = null
get() {
return MMKV.defaultMMKV().decodeParcelable("vehicleInfo", VehicleInfo::class.java)
}
set(value) {
MMKV.defaultMMKV().encode("vehicleInfo", value)
field = value
}
var currentOrder : OrderInfo? = null
get() {
return MMKV.defaultMMKV().decodeParcelable("currentOrder", OrderInfo::class.java)
}
set(value) {
MMKV.defaultMMKV().encode("currentOrder", value)
if (RoomHelper.db?.orderDao()?.getCurrentOrder() == null && value != null) {
RoomHelper.db?.orderDao()?.insertOrder(value)
} else if (value != null) {
RoomHelper.db?.orderDao()?.update(value)
}
field = value
}
var currentLocation : AMapLocation? = null
get() {
return MMKV.defaultMMKV().decodeParcelable("currentLocation", AMapLocation::class.java)
}
set(value) {
value?.time = System.currentTimeMillis()
MMKV.defaultMMKV().encode("currentLocation", value)
field = value
}
var loginTime : Long? = null
get() {
return MMKV.defaultMMKV().decodeLong("loginTime", System.currentTimeMillis())
}
set(value) {
MMKV.defaultMMKV().encode("loginTime", value ?: System.currentTimeMillis())
field = value
}
fun clearUserCache() {
token = null
aesKey = null
driverInfo = null
vehicleInfo = null
currentLocation = null
loginTime = null
}
fun clearAllOrderCache() {
currentOrder = null
RoomHelper.clearAll()
}
fun clearOrderCache(taskId : Int) {
RoomHelper.clearOrderFromTaskCode(taskId = taskId)
}
}

View File

@ -0,0 +1,35 @@
package com.za.common
import android.app.Application
import com.tencent.bugly.Bugly
import com.tencent.mmkv.MMKV
import com.tencent.mmkv.MMKVLogLevel
import com.za.base.AppConfig
import com.za.common.log.LogUtil
import com.za.room.RoomHelper
import com.za.service.location.ZdLocationManager
object ZDManager {
lateinit var application : Application
fun init(application : Application) {
this.application = application
thirdSdkInit()
}
private fun thirdSdkInit() {
GlobalData.application = application
MMKV.initialize(application, MMKVLogLevel.LevelInfo)
Bugly.init(application, "6972a6b56d", true)
AppConfig.crm1()
LogUtil.init(application)
RoomHelper.init(application)
ZdLocationManager.init(application)
// 初始化讯飞SDK
//科大讯飞初始化
// SpeechUtility.createUtility(application,
// SpeechConstant.APPID + "=6fd4aabe," + SpeechConstant.FORCE_LOGIN + "=true")
// SpeechManager.init(application)
}
}

View File

@ -0,0 +1,65 @@
package com.za.common.log
import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.util.concurrent.TimeUnit
object LogRetrofitHelper {
private var retrofit: Retrofit? = null
private var apiService: LogService? = null
private val loggerInterceptor = HttpLoggingInterceptor {
try {
if (it.contains("image/*")) {
return@HttpLoggingInterceptor
}
if (it.contains("name=\"file\"; filename")) {
return@HttpLoggingInterceptor
}
Log.e(
"--network--",
URLDecoder.decode(it.replace(Regex("%(?![0-9a-fA-F]{2})"), ""), "utf-8")
)
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
}
}.setLevel(HttpLoggingInterceptor.Level.BODY)
fun getDefaultService(): LogService {
return if (apiService == null) {
apiService = getDefaultRetrofit().create(LogService::class.java)
apiService!!
} else {
apiService!!
}
}
private fun getDefaultRetrofit(): Retrofit {
return if (retrofit == null) {
retrofit = Retrofit.Builder().baseUrl("https://api2.sino-assist.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.client(getOkHttpClient())
.build()
retrofit!!
} else {
retrofit!!
}
}
private fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggerInterceptor)
.build()
}
}

View File

@ -0,0 +1,27 @@
package com.za.common.log
import io.reactivex.rxjava3.core.Observable
import okhttp3.MultipartBody
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Query
interface LogService {
//日志上传
@Multipart
@POST("/oss/minio/upload")
fun uploadLog(
@Part file: MultipartBody.Part?,
@Query("fileName") fileName: String?,
@Query("bucketName") bucketName: String?
): Observable<LogBaseResponse<String>>
}
data class LogBaseResponse<T : Any>(
val data: T?,
val msg: String?,
val success: Boolean?,
val code: Int?,
val total: Int?,
)

View File

@ -0,0 +1,317 @@
package com.za.common.log
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.work.Configuration
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.blankj.utilcode.constant.MemoryConstants
import com.blankj.utilcode.util.AppUtils
import com.blankj.utilcode.util.DeviceUtils
import com.blankj.utilcode.util.FileUtils
import com.blankj.utilcode.util.TimeUtils
import com.za.common.GlobalData
import com.za.common.util.AppFileManager
import com.za.servicing.BuildConfig
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream
import java.io.BufferedWriter
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
object LogUtil {
private var context: Application? = null
private var logDestinationPath: String? = null
private var orderLogDirPath: String? = null
private var normalLogDirPath: String? = null
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val logBuffer = StringBuilder()
private val isWriting = AtomicBoolean(false)
fun init(context: Application) {
this.context = context
logDestinationPath = AppFileManager.getLogPath(context).also { path ->
createDirectoryIfNotExists(path)
orderLogDirPath = "$path${File.separator}order_log".also { createDirectoryIfNotExists(it) }
normalLogDirPath = "$path${File.separator}normal_log".also { createDirectoryIfNotExists(it) }
}
initializeWorkManager(context)
}
private fun createDirectoryIfNotExists(path: String) {
File(path).apply { if (!exists()) mkdir() }
}
private fun initializeWorkManager(context: Application) {
if (!WorkManager.isInitialized()) {
WorkManager.initialize(context, Configuration.Builder()
.setMinimumLoggingLevel(Log.INFO)
.build())
}
WorkManager.getInstance(context).apply {
cancelAllWorkByTag("logWorkRequest")
enqueue(PeriodicWorkRequest.Builder(LogTask::class.java, 20, TimeUnit.MINUTES)
.addTag("logWorkRequest")
.build())
}
}
fun print(tag: String, content: String) {
val time = getCurrentTime()
val logEntry = "$time---$tag---$content\n"
if (BuildConfig.DEBUG) {
Log.e("normal", "$tag---$content")
}
synchronized(logBuffer) {
logBuffer.append(logEntry)
}
if (logBuffer.length > 4096) {
coroutineScope.launch {
flushBuffer()
}
}
}
fun print(tag: String, throwable: Throwable) {
val content = StringWriter()
val printWriter = PrintWriter(content)
throwable.printStackTrace(printWriter)
print(tag, content.toString())
}
private suspend fun flushBuffer() = withContext(Dispatchers.IO) {
if (!isWriting.compareAndSet(false, true)) return@withContext
val logContent: String
synchronized(logBuffer) {
if (logBuffer.isEmpty()) {
isWriting.set(false)
return@withContext
}
logContent = logBuffer.toString()
logBuffer.clear()
}
try {
val fileName = "normal_log.txt"
val logFile = File("$normalLogDirPath${File.separator}$fileName")
logFile.parentFile?.mkdirs()
if (!logFile.exists()) {
logFile.createNewFile()
addLogHead(logFile, getCurrentTime())
}
BufferedWriter(FileWriter(logFile, true)).use { writer ->
writer.write(logContent)
writer.flush()
}
if (logFile.length() >= 8 * MemoryConstants.MB) {
rotateLogFile(logFile)
}
} catch (e: IOException) {
Log.e("LogUtil", "Error in flushBuffer: ${e.message}")
} catch (e: Exception) {
Log.e("LogUtil", "Error in flushBuffer: ${e.message}")
} finally {
isWriting.set(false)
}
}
private fun rotateLogFile(file: File) {
if (!file.exists()) return
val newFileName = buildString {
append(AppUtils.getAppVersionCode())
append("_")
append(GlobalData.vehicleInfo?.vehicleName ?: "unknown")
append("_")
append(GlobalData.driverInfo?.userName ?: "unknown")
append("_")
append(TimeUtils.getNowString())
append(".txt")
}
val newFile = File("$normalLogDirPath${File.separator}$newFileName")
try {
if (file.renameTo(newFile)) {
compressAndUploadLog(newFile)
// 创建新的日志文件
file.createNewFile()
addLogHead(file, getCurrentTime())
} else {
print("LogUtil", "Failed to rename log file")
}
} catch (e: Exception) {
print("LogUtil", "Error during log rotation: ${e.message}")
}
}
private fun compressAndUploadLog(logFile: File) = coroutineScope.launch {
try {
val compressedFile = File("${logFile.absolutePath}.7z")
compress(logFile, compressedFile.absolutePath)
upload(logFile, compressedFile)
} catch (e: Exception) {
print("LogUtil", e.toString())
}
}
private fun deleteLog(file: File) {
try {
FileUtils.delete(file.absolutePath)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun updateNormalLog() {
thread {
if (GlobalData.token.isNullOrBlank()) {
return@thread
}
val fileName = "normal_log.txt"
val file = File("$normalLogDirPath${File.separator}$fileName")
val reName = "${AppUtils.getAppVersionCode()}_${GlobalData.vehicleInfo?.vehicleName}_${GlobalData.driverInfo?.userName}_${TimeUtils.getNowString()}.txt"
val reNamePath = "$normalLogDirPath${File.separator}$reName"
file.renameTo(File(reNamePath))
normalLogDirPath?.let { it ->
File(it).listFiles()?.forEach {
if (it.length() / MemoryConstants.MB >= 10) {
deleteLog(it)
return@thread
}
if (it.exists() && !it.name.contains("normal_log")) {
if (it.name.contains("7z")) {
upload(null, desFile = it)
} else {
val zipNamePath = it.absolutePath + ".7z"
val zipFile = File(zipNamePath)
if (!zipFile.exists()) {
try {
zipFile.createNewFile()
} catch (e: IOException) {
e.printStackTrace()
}
}
compress(it, zipNamePath)
}
}
}
}
}
}
private fun compress(srcFile: File, desFilePath: String) {
try {
val out = XZCompressorOutputStream(FileOutputStream(desFilePath))
addToArchiveCompression(out, srcFile, File(desFilePath))
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun addToArchiveCompression(
sevenZOutputFile: XZCompressorOutputStream,
srcFile: File, desFile: File
) {
if (srcFile.isFile) {
var inputStream: FileInputStream? = null
try {
inputStream = FileInputStream(srcFile)
val b = ByteArray(2048)
var count: Int
while (inputStream.read(b).also { count = it } != -1) {
sevenZOutputFile.write(b, 0, count)
}
sevenZOutputFile.close()
inputStream.close()
upload(srcFile, desFile)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
sevenZOutputFile.close()
inputStream!!.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
private fun upload(srcFile: File?, desFile: File) {
val requestBody: RequestBody = desFile.asRequestBody("multipart/form-data".toMediaType())
val part = MultipartBody.Part.createFormData("file", desFile.name, requestBody)
val disposable = LogRetrofitHelper.getDefaultService()
.uploadLog(part, desFile.name, "rescue-app")
.subscribeOn(Schedulers.io())
.subscribe({ it ->
if (it.code == 200) {
deleteLog(desFile)
}
srcFile?.let {
deleteLog(it)
}
}, {
}, {})
}
private fun addLogHead(file: File, time: String) {
file.appendBytes("${time}---应用版本---${AppUtils.getAppVersionName()}".toByteArray())
file.appendBytes("\n".toByteArray())
file.appendBytes("$time---系统版本---Android${DeviceUtils.getSDKVersionName()} ${DeviceUtils.getSDKVersionCode()}".toByteArray())
file.appendBytes("\n".toByteArray())
file.appendBytes("$time---ROM---${DeviceUtils.getManufacturer()} ${DeviceUtils.getModel()}".toByteArray())
file.appendBytes("\n".toByteArray())
file.appendBytes("$time---build---${AppUtils.getAppVersionCode()}".toByteArray())
file.appendBytes("\n".toByteArray())
file.appendBytes("$time---APP名称---中道救援-司机端".toByteArray())
file.appendBytes("\n".toByteArray())
file.appendBytes("$time---车辆名称---${GlobalData.vehicleInfo?.vehicleName}".toByteArray())
file.appendBytes("\n".toByteArray())
file.appendBytes("$time---司机名称---${GlobalData.driverInfo?.userName ?: GlobalData.vehicleInfo?.userName}".toByteArray())
file.appendBytes("\n".toByteArray())
}
private fun getCurrentTime(): String {
return TimeUtils.millis2String(System.currentTimeMillis(), "yyyy/MM/dd HH:mm:ss.SSS")
}
class LogTask(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
override fun doWork(): Result {
updateNormalLog()
return Result.success()
}
}
}

View File

@ -0,0 +1,82 @@
package com.za.common.util
import android.content.Context
import android.os.Build
import android.os.Environment
import com.za.base.Const
import java.io.File
object AppFileManager {
//获取人脸大头照存储地址
fun getFaceDir(context: Context): File {
val path = context.cacheDir?.path + File.separator + "faceVerify"
val file = File(path)
if (!file.exists()) {
file.mkdir()
}
return file
}
/**
* 师傅签名位置保存
*/
fun getDriverSignDirPath(context : Context) : String {
val tempPath = context.cacheDir.toString() + File.separator + "driver_sign"
val tempFile = File(tempPath)
if (! tempFile.exists()) {
tempFile.mkdir()
}
return tempFile.absolutePath
}
/**
* 师傅签名位置保存
*/
fun getDriverSignPath(context : Context) : String {
return getDriverSignDirPath(context) + File.separator + Const.driverSighName
}
//人脸大头照存储地址
fun getFaceFilePath(context: Context): String {
return getFaceDir(context).absolutePath + File.separator + "avatar.jpg"
}
/**
* app日志路径
*/
fun getLogPath(context: Context?): String {
val path = "${context?.filesDir?.absolutePath}${File.separator}Log"
if (!File(path).exists()) {
File(path).mkdir()
}
return path
}
fun orderWaterMarkerPath(context: Context): String {
val path = context.cacheDir?.path + File.separator + "water_marker"
if (!File(path).exists()) {
File(path).mkdir()
}
return path
}
/**
* 拍摄的照片的保存位置
*/
fun getTakePictureParentPath(): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return Environment.DIRECTORY_PICTURES
} else {
val dstPath =
Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.DIRECTORY_PICTURES + File.separator + "中道救援"
val file = File(dstPath)
if (!file.exists()) {
file.mkdir()
}
return dstPath
}
}
}

View File

@ -0,0 +1,20 @@
package com.za.common.util;
import android.view.View;
public class ClickProxy implements View.OnClickListener {
private long lastClick = 0;
private final View.OnClickListener onClickListener;
public ClickProxy(View.OnClickListener onClickListener) {
this.onClickListener = onClickListener;
}
@Override
public void onClick(View v) {
if (System.currentTimeMillis() - lastClick >= 1000) {
onClickListener.onClick(v);
lastClick = System.currentTimeMillis();
}
}
}

View File

@ -0,0 +1,76 @@
package com.za.common.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.MediaDrm;
import android.os.Build;
import android.provider.Settings;
import com.blankj.utilcode.util.SPUtils;
import java.security.MessageDigest;
import java.util.UUID;
/**
* 设备唯一标识
* 顺序为Android_ID->DRM_ID->UUID
*/
public class DeviceUtil {
public static String getAndroidId(Context context) {
String deviceId = SPUtils.getInstance().getString("deviceId");
if (deviceId == null || deviceId.isEmpty()) {
@SuppressLint("HardwareIds") String temp = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (temp == null || temp.isEmpty() || temp.replace("0", "").isEmpty()) {
temp = getDrmId();
}
SPUtils.getInstance().put("deviceId", temp);
return temp;
}
return deviceId;
}
private static String getDrmId() {
String sRet = "";
UUID WIDEVINE_UUID = new UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L);
MediaDrm mediaDrm = null;
try {
mediaDrm = new MediaDrm(WIDEVINE_UUID);
byte[] widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(widevineId);
sRet = bytesToHex(md.digest()); //we convert byte[] to hex for our purposes
} catch (Exception e) {
//WIDEVINE is not available
} finally {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (null != mediaDrm) {
mediaDrm.close();
}
} else {
if (null != mediaDrm) {
mediaDrm.release();
}
}
}
if (sRet.isEmpty()) {
sRet = WIDEVINE_UUID.toString().replace("-", "");
}
return sRet;
}
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
}

View File

@ -0,0 +1,489 @@
package com.za.common.util
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.VectorDrawable
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.text.format.DateUtils
import android.view.View
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import com.amap.api.maps.model.BitmapDescriptor
import com.amap.api.maps.model.BitmapDescriptorFactory
import com.za.common.log.LogUtil
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 图片工具类
*
* @author
*/
object ImageUtil {
/**
* 设置水印图片在左上角
*
* @param context
* @param src
* @param watermark
* @param paddingLeft
* @param paddingTop
* @return
*/
fun createWaterMaskLeftTop(context : Context,
src : Bitmap?,
watermark : Bitmap,
paddingLeft : Int,
paddingTop : Int) : Bitmap? {
return createWaterMaskBitmap(src,
watermark,
paddingLeft,
dp2px(context, paddingTop.toFloat()))
}
private fun createWaterMaskBitmap(src : Bitmap?,
watermark : Bitmap,
paddingLeft : Int,
paddingTop : Int) : Bitmap? {
if (src == null) {
return null
}
val width = src.width
val height = src.height //创建一个bitmap
val newb = Bitmap.createBitmap(width,
height,
Bitmap.Config.ARGB_8888) // 创建一个新的和SRC长度宽度一样的位图 //将该图片作为画布
val canvas = Canvas(newb) //在画布 00坐标上开始绘制原始图片
canvas.drawBitmap(src, 0f, 0f, null) //在画布上绘制水印图片
canvas.drawBitmap(watermark, paddingLeft.toFloat(), paddingTop.toFloat(), null) // 保存
canvas.save() // 存储
canvas.restore()
return newb
}
/**
* 设置水印图片在右下角
*
* @param context
* @param src
* @param watermark
* @param paddingRight
* @param paddingBottom
* @return
*/
fun createWaterMaskRightBottom(context : Context?,
src : Bitmap,
watermark : Bitmap,
paddingRight : Int,
paddingBottom : Int) : Bitmap? {
return createWaterMaskBitmap(src,
watermark,
src.width - watermark.width - paddingRight,
src.height - watermark.height - paddingBottom)
}
/**
* 设置水印图片到右上角
*
* @param context
* @param src
* @param watermark
* @param paddingRight
* @param paddingTop
* @return
*/
fun createWaterMaskRightTop(context : Context?,
src : Bitmap,
watermark : Bitmap,
paddingRight : Int,
paddingTop : Int) : Bitmap? {
return createWaterMaskBitmap(src,
watermark,
src.width - watermark.width - paddingRight,
paddingTop)
}
/**
* 设置水印图片到左下角
*
* @param context
* @param src
* @param watermark
* @param paddingLeft
* @param paddingBottom
* @return
*/
fun createWaterMaskLeftBottom(context : Context?,
src : Bitmap,
watermark : Bitmap,
paddingLeft : Int,
paddingBottom : Int) : Bitmap? {
return createWaterMaskBitmap(src,
watermark,
paddingLeft,
src.height - watermark.height - paddingBottom)
}
/**
* 设置水印图片到中间
*
* @param src
* @param watermark
* @return
*/
fun createWaterMaskCenter(src : Bitmap, watermark : Bitmap) : Bitmap? {
return createWaterMaskBitmap(src,
watermark,
(src.width - watermark.width) / 2,
(src.height - watermark.height) / 2)
}
/**
* 给图片添加文字到左上角
*
* @param context
* @param bitmap
* @param text
* @return
*/
fun drawTextToLeftTop(context : Context,
bitmap : Bitmap,
text : String,
size : Int,
color : Int,
paddingLeft : Int,
paddingTop : Int) : Bitmap {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = color
paint.textSize = size.toFloat()
paint.setTypeface(Typeface.createFromAsset(context.assets, "fonts/song.ttf"))
paint.isFakeBoldText = true
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
return drawTextToBitmap(context,
bitmap,
text,
paint,
bounds,
paddingLeft,
paddingTop + bounds.height())
}
/**
* 绘制文字到右下角
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @return
*/
fun drawTextToRightBottom(context : Context,
bitmap : Bitmap,
text : String,
size : Int,
color : Int,
paddingRight : Int,
paddingBottom : Int) : Bitmap {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = color
paint.textSize = size.toFloat()
paint.setTypeface(Typeface.createFromAsset(context.assets, "fonts/song.ttf"))
paint.isFakeBoldText = true
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
return drawTextToBitmap(context,
bitmap,
text,
paint,
bounds,
bitmap.width - bounds.width() - paddingRight,
bitmap.height - paddingBottom)
}
/**
* 绘制文字到右上方
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @param paddingRight
* @param paddingTop
* @return
*/
fun drawTextToRightTop(context : Context,
bitmap : Bitmap,
text : String,
size : Int,
color : Int,
paddingRight : Int,
paddingTop : Int) : Bitmap {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = color
paint.textSize = size.toFloat()
paint.setTypeface(Typeface.createFromAsset(context.assets, "fonts/song.ttf"))
paint.isFakeBoldText = true
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
return drawTextToBitmap(context,
bitmap,
text,
paint,
bounds,
bitmap.width - bounds.width() - paddingRight,
paddingTop + bounds.height())
}
/**
* 绘制文字到左下方
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @param paddingLeft
* @param paddingBottom
* @return
*/
fun drawTextToLeftBottom(context : Context,
bitmap : Bitmap,
text : String,
size : Int,
color : Int,
paddingLeft : Int,
paddingBottom : Int) : Bitmap {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = color
paint.textSize = size.toFloat()
paint.setTypeface(Typeface.createFromAsset(context.assets, "fonts/song.ttf"))
paint.isFakeBoldText = true
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
return drawTextToBitmap(context,
bitmap,
text,
paint,
bounds,
paddingLeft,
bitmap.height - paddingBottom)
}
/**
* 绘制文字到中间
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @return
*/
fun drawTextToCenter(context : Context,
bitmap : Bitmap,
text : String,
size : Int,
color : Int) : Bitmap {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = color
paint.textSize = size.toFloat()
paint.setTypeface(Typeface.createFromAsset(context.assets, "fonts/song.ttf"))
paint.isFakeBoldText = true
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
return drawTextToBitmap(context,
bitmap,
text,
paint,
bounds,
(bitmap.width - bounds.width()) / 2,
(bitmap.height + bounds.height()) / 2)
}
//图片上绘制文字
private fun drawTextToBitmap(context : Context,
myBitmap : Bitmap,
text : String,
paint : Paint,
bounds : Rect,
paddingLeft : Int,
paddingTop : Int) : Bitmap {
val bitmapConfig = myBitmap.config
paint.isDither = true // 获取跟清晰的图像采样
paint.isFilterBitmap = true // 过滤一些
val bitmap = myBitmap.copy(bitmapConfig !!, true)
val canvas = Canvas(bitmap)
canvas.drawText(text, paddingLeft.toFloat(), paddingTop.toFloat(), paint)
return bitmap
}
/**
* 缩放图片
*
* @param src
* @param w
* @param h
* @return
*/
fun scaleWithWH(src : Bitmap?, w : Double, h : Double) : Bitmap? {
if (w == 0.0 || h == 0.0 || src == null) {
return src
} else { // 记录src的宽高
val width = src.width
val height = src.height // 创建一个matrix容器
val matrix = Matrix() // 计算缩放比例
val scaleWidth = (w / width).toFloat()
val scaleHeight = (h / height).toFloat() // 开始缩放
matrix.postScale(scaleWidth, scaleHeight) // 创建缩放后的图片
return Bitmap.createBitmap(src, 0, 0, width, height, matrix, true)
}
}
/**
* dip转pix
*
* @param context
* @param dp
* @return
*/
private fun dp2px(context : Context, dp : Float) : Int {
val scale = context.resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
// view转bitmap
fun createBitmapFromView(view : View) : Bitmap? { //是ImageView直接获取
if (view is ImageView) {
val drawable = view.drawable
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
}
view.clearFocus()
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
canvas.setBitmap(null)
return bitmap
}
// 保存图片到相册
fun saveImage(context : Context, bitmap : Bitmap?) {
if (bitmap == null) {
return
}
val isSaveSuccess = if (Build.VERSION.SDK_INT < 29) {
saveImageToGallery(context, bitmap)
} else {
saveImageToGallery1(context, bitmap)
}
if (isSaveSuccess) {
LogUtil.print("照片保存到相册成功", "success")
} else {
LogUtil.print("照片保存到相册失败", "failed")
}
}
/**
* android 10 以下版本
*/
private fun saveImageToGallery(context : Context, image : Bitmap) : Boolean { // 首先保存图片
val storePath =
Environment.getExternalStorageDirectory().absolutePath + File.separator + "中道救援"
val appDir = File(storePath)
if (! appDir.exists()) {
appDir.mkdir()
}
val fileName = System.currentTimeMillis().toString() + ".jpg"
val file = File(appDir, fileName)
try {
val fos = FileOutputStream(file) // 通过io流的方式来压缩保存图片
val isSuccess = image.compress(Bitmap.CompressFormat.JPEG, 60, fos)
fos.flush()
fos.close()
// 保存图片后发送广播通知更新数据库
val uri = Uri.fromFile(file)
context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))
return isSuccess
} catch (e : IOException) {
e.printStackTrace()
}
return false
}
/**
* android 10 以上版本
*/
private fun saveImageToGallery1(context : Context, image : Bitmap) : Boolean {
val mImageTime = System.currentTimeMillis()
val imageDate =
SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()).format(Date(mImageTime))
val SCREENSHOT_FILE_NAME_TEMPLATE = "zd_%s.png" //图片名称,以"zd"+时间戳命名
val mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate)
val values = ContentValues()
values.put(MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + File.separator + "中道救援") //Environment.DIRECTORY_SCREENSHOTS:截图,图库中显示的文件夹名
values.put(MediaStore.MediaColumns.DISPLAY_NAME, mImageFileName)
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
values.put(MediaStore.MediaColumns.DATE_ADDED, mImageTime / 1000)
values.put(MediaStore.MediaColumns.DATE_MODIFIED, mImageTime / 1000)
values.put(MediaStore.MediaColumns.DATE_EXPIRES,
(mImageTime + DateUtils.DAY_IN_MILLIS) / 1000)
values.put(MediaStore.MediaColumns.IS_PENDING, 1)
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
try { // First, write the actual data for our screenshot
resolver.openOutputStream(uri !!).use { out ->
if (! image.compress(Bitmap.CompressFormat.PNG, 100, out !!)) {
return false
}
} // Everything went well above, publish it!
values.clear()
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
values.putNull(MediaStore.MediaColumns.DATE_EXPIRES)
resolver.update(uri, values, null, null)
} catch (e : IOException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
resolver.delete(uri !!, null)
}
return false
}
return true
}
fun vectorToBitmap(context : Context, vectorResId : Int) : BitmapDescriptor {
val vectorDrawable = ContextCompat.getDrawable(context, vectorResId) as VectorDrawable
val bitmap = createBitmap(vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight)
val canvas = Canvas(bitmap)
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
vectorDrawable.draw(canvas)
return BitmapDescriptorFactory.fromBitmap(bitmap)
}
}

View File

@ -0,0 +1,101 @@
package com.za.common.util
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import com.amap.api.maps.model.LatLng
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.ln
import kotlin.math.sin
import kotlin.math.sqrt
object MapUtil {
const val PN_GAODE_MAP: String = "com.autonavi.minimap" // 高德地图包名
const val PN_BAIDU_MAP: String = "com.baidu.BaiduMap" // 百度地图包名
const val PN_TENCENT_MAP: String = "com.tencent.map" // 百度地图包名
/**
* 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换
* 即谷歌、高德 转 百度
*
* @param latLng
* @returns
*/
fun gcj02ToBD09(latLng: LatLng): LatLng {
val xPI = 3.141592653589793 * 3000.0 / 180.0
val z = sqrt(latLng.longitude * latLng.longitude + latLng.latitude * latLng.latitude) + 0.00002 * sin(latLng.latitude * xPI)
val theta = atan2(latLng.latitude, latLng.longitude) + 0.000003 * cos(latLng.longitude * xPI)
val bdLat = z * sin(theta) + 0.006
val bdLng = z * cos(theta) + 0.0065
return LatLng(bdLat, bdLng)
}
/**
* 检查应用是否安装
*
* @return
*/
fun isGdMapInstalled(context: Context): Boolean {
return isInstallPackage(context, PN_GAODE_MAP)
}
fun isBaiduMapInstalled(context: Context): Boolean {
return isInstallPackage(context, PN_BAIDU_MAP)
}
fun isTencentInstalled(context: Context): Boolean {
return isInstallPackage(context, PN_TENCENT_MAP)
}
private fun isInstallPackage(context: Context, packageName: String): Boolean {
try {
val info = context.packageManager
.getApplicationInfo(packageName,
PackageManager.GET_UNINSTALLED_PACKAGES)
return true
} catch (e: PackageManager.NameNotFoundException) {
return false
}
}
//开启高德导航
fun startNavigationGd(context: Context, lat: Double?, lng: Double?, address: String?) {
val stringBuffer = "androidamap://route?sourceApplication=" + "amap" +
"&dlat=" + lat +
"&dlon=" + lng +
"&dname=" + address +
"&dev=" + 0 +
"&t=" + 0
val intent = Intent("android.intent.action.VIEW", Uri.parse(stringBuffer))
intent.setPackage("com.autonavi.minimap")
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
//开启高德导航
fun startNavigationBd(context: Context, lat: Double?, lng: Double?, address: String?) {
val i1 = Intent("android.intent.action.VIEW")
i1.setData(Uri.parse("baidumap://map/navi?query=${address}&mode=driving&location=${lat},${lng}&coord_type=gcj02&src=com.za.servicing"))
context.startActivity(i1)
}
//开启高德导航
fun startNavigationTencent(context: Context, lat: Double?, lng: Double?, address: String?) {
val stringBuffer = "androidamap://route?sourceApplication=" + "amap" +
"&dlat=" + lat +
"&dlon=" + lng +
"&dname=" + address +
"&dev=" + 0 +
"&t=" + 0
val intent = Intent("android.intent.action.VIEW", Uri.parse(stringBuffer))
intent.setPackage("com.autonavi.minimap")
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}

View File

@ -0,0 +1,5 @@
package com.za.common.util
object NotificationUtil {
const val CHANNEL_ID = "1001"
}

View File

@ -0,0 +1,92 @@
package com.za.common.util
import android.graphics.Bitmap
import android.graphics.Color
import android.text.TextUtils
import androidx.annotation.ColorInt
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.qrcode.QRCodeWriter
import java.util.Hashtable
/**
* @ClassName: QRCodeUtil
* @Description: 二维码工具类
*/
object QRCodeUtil {
/**
* 创建二维码位图 (支持自定义配置和自定义样式)
*
* @param content 字符串内容
* @param width 位图宽度,要求>=0(单位:px)
* @param height 位图高度,要求>=0(单位:px)
* @param character_set 字符集/字符转码格式 (支持格式:[CharacterSetECI])。传null时,zxing源码默认使用 "ISO-8859-1"
* @param error_correction 容错级别 (支持级别:[ErrorCorrectionLevel])。传null时,zxing源码默认使用 "L"
* @param margin 空白边距 (可修改,要求:整型且>=0), 传null时,zxing源码默认使用"4"。
* @param color_black 黑色色块的自定义颜色值
* @param color_white 白色色块的自定义颜色值
* @return
*/
/**
* 创建二维码位图
*
* @param content 字符串内容(支持中文)
* @param width 位图宽度(单位:px)
* @param height 位图高度(单位:px)
* @return
*/
@JvmOverloads
fun createQRCodeBitmap(content: String?, width: Int, height: Int,
characterSet: String? = "UTF-8", errorCorrection: String? = "H", margin: String? = "2",
@ColorInt colorBlack: Int = Color.BLACK, @ColorInt colorWhite: Int = Color.WHITE): Bitmap? {
/** 1.参数合法性判断 */
if (TextUtils.isEmpty(content)) { // 字符串内容判空
return null
}
if (width < 0 || height < 0) { // 宽和高都需要>=0
return null
}
try {
/** 2.设置二维码相关配置,生成BitMatrix(位矩阵)对象 */
val hints = Hashtable<EncodeHintType, String?>()
if (!TextUtils.isEmpty(characterSet)) {
hints[EncodeHintType.CHARACTER_SET] = characterSet // 字符转码格式设置
}
if (!TextUtils.isEmpty(errorCorrection)) {
hints[EncodeHintType.ERROR_CORRECTION] = errorCorrection // 容错级别设置
}
if (!TextUtils.isEmpty(margin)) {
hints[EncodeHintType.MARGIN] = margin // 空白边距设置
}
val bitMatrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
/** 3.创建像素数组,并根据BitMatrix(位矩阵)对象为数组元素赋颜色值 */
val pixels = IntArray(width * height)
for (y in 0 until height) {
for (x in 0 until width) {
if (bitMatrix[x, y]) {
pixels[y * width + x] = colorBlack // 黑色色块像素设置
} else {
pixels[y * width + x] = colorWhite // 白色色块像素设置
}
}
}
/** 4.创建Bitmap对象,根据像素数组设置Bitmap每个像素点的颜色值,之后返回Bitmap对象 */
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
return bitmap
} catch (e: WriterException) {
e.printStackTrace()
}
return null
}
}

View File

@ -0,0 +1,53 @@
package com.za.common.util
import android.content.Context
import android.media.MediaPlayer
import com.za.servicing.R
object ServicingSpeechManager {
private var mediaPlayer: MediaPlayer? = null
// 提醒客户签字
fun playOrderCustomSign(mContext: Context) {
mediaPlayer = MediaPlayer.create(mContext, R.raw.custom_sign)
mediaPlayer?.start()
}
// 提醒接车人签字
fun playOrderAcceptSign(mContext: Context) {
mediaPlayer = MediaPlayer.create(mContext, R.raw.accept_sign)
mediaPlayer?.start()
}
// 车主签字
fun playCarOwnerSign(mContext: Context) {
mediaPlayer = MediaPlayer.create(mContext, R.raw.car_onwer_sign)
mediaPlayer?.start()
}
// 提醒五星好评
fun playOrderGoodService(mContext: Context) {
mediaPlayer = MediaPlayer.create(mContext, R.raw.good_star)
mediaPlayer?.start()
}
//提醒客户不需要再上传纸质工单
fun playNoUploadEleOrderWork(mContext: Context) {
mediaPlayer = MediaPlayer.create(mContext, R.raw.no_upload_ele_order_work)
mediaPlayer?.start()
}
// 发车前提示
fun playStartTip(mContext: Context) {
mediaPlayer = MediaPlayer.create(mContext, R.raw.start_tip)
mediaPlayer?.start()
}
// 验车前提示
fun playCheckTip(mContext: Context) {
mediaPlayer = MediaPlayer.create(mContext, R.raw.check_tip)
mediaPlayer?.start()
}
}

View File

@ -0,0 +1,33 @@
package com.za.common.util
import android.util.Base64
import java.nio.charset.StandardCharsets
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
/**
* @author DoggieX
* @create 2021/3/22 16:17
* @mail coldpuppy@163.com
*/
object Tools {
/**
* @param text 要签名的文本
* @param secretKey 阿里云MQ secretKey
* @return 加密后的字符串
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class)
fun macSignature(text: String, secretKey: String): String {
val charset = StandardCharsets.UTF_8
val algorithm = "HmacSHA1"
val mac = Mac.getInstance(algorithm)
mac.init(SecretKeySpec(secretKey.toByteArray(charset), algorithm))
val bytes = mac.doFinal(text.toByteArray(charset))
// android的base64编码注意换行符情况, 使用NO_WRAP
return String(Base64.encode(bytes, Base64.NO_WRAP), charset)
}
}