From 1097c45d8d6aa4b74a50e8d9a99dedab73f2bbad Mon Sep 17 00:00:00 2001 From: cloudroam <cloudroam> Date: 星期五, 28 二月 2025 13:35:07 +0800 Subject: [PATCH] add: 优化首页自动同步历史短信,和实时刷新包裹列表 --- app/src/main/res/layout/activity_phone_login.xml | 39 +++-- app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt | 4 app/src/main/java/com/example/firstapp/model/ExpressGroup.kt | 1 app/src/main/res/navigation/mobile_navigation.xml | 16 ++ app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt | 45 ++++--- app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt | 3 app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt | 2 app/src/main/res/layout/fragment_reminder_settings.xml | 46 +++--- app/src/main/java/com/example/firstapp/activity/PhoneLoginActivity.kt | 1 app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt | 1 app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt | 47 +++++++ app/src/main/java/com/example/firstapp/MainActivity.kt | 99 +++++++++++---- app/src/main/java/com/example/firstapp/ui/reminder/ReminderSettingsFragment.kt | 9 + 13 files changed, 217 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/com/example/firstapp/MainActivity.kt b/app/src/main/java/com/example/firstapp/MainActivity.kt index 6abffe0..bbb32a4 100644 --- a/app/src/main/java/com/example/firstapp/MainActivity.kt +++ b/app/src/main/java/com/example/firstapp/MainActivity.kt @@ -27,10 +27,19 @@ import androidx.work.WorkManager import com.example.firstapp.activity.LoginActivity import com.example.firstapp.adapter.MyAdapter +import com.example.firstapp.core.Core +import com.example.firstapp.database.entity.Code +import com.example.firstapp.database.entity.Msg +import com.example.firstapp.entity.Rule import com.example.firstapp.ui.home.HomeViewModel import com.example.firstapp.utils.Log import com.example.firstapp.workers.KeywordUpdateWorker +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.Calendar +import java.util.Date +import java.util.Locale import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { @@ -42,16 +51,18 @@ private lateinit var adapter: MyAdapter private lateinit var homeViewModel: HomeViewModel - - // 权限请求代码 - private val permissionRequest = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (isGranted) { - // 权限授予后注册短信监听器 - registerSmsReceiver() - syncRecentSms() - } else { - // 权限拒绝,提示用户 - Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show() + private val multiplePermissionRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + when { + permissions.getOrDefault(Manifest.permission.RECEIVE_SMS, false) && + permissions.getOrDefault(Manifest.permission.READ_SMS, false) -> { + // 两个权限都获得授权 + registerSmsReceiver() + syncRecentSms() + } + else -> { + // 有权限被拒绝 + Toast.makeText(this, "需要短信读取和接收权限才能正常使用功能", Toast.LENGTH_SHORT).show() + } } } @@ -71,6 +82,7 @@ // val navView: BottomNavigationView = binding.navView val navView = binding.navView val navController = findNavController(R.id.nav_host_fragment_activity_main) + // Passing each menu ID as a set of Ids because each // menu should be considered as top level destinations. val appBarConfiguration = AppBarConfiguration( @@ -82,10 +94,13 @@ navView.setupWithNavController(navController) // 检查权限 - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) != android.content.pm.PackageManager.PERMISSION_GRANTED) { - // 请求权限 - permissionRequest.launch(Manifest.permission.READ_SMS) - syncRecentSms() + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) != android.content.pm.PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) != android.content.pm.PackageManager.PERMISSION_GRANTED) { + // 同时请求两个权限 + multiplePermissionRequest.launch(arrayOf( + Manifest.permission.RECEIVE_SMS, + Manifest.permission.READ_SMS + )) } else { // 权限已经授予,继续执行相关操作 registerSmsReceiver() @@ -121,7 +136,10 @@ // }, filter) } - + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_activity_main) + return navController.navigateUp() || super.onSupportNavigateUp() + } private fun registerSmsReceiver() { // 应用启动时执行 registerSmsReceiver() // 创建 SmsReceiver 实例 @@ -179,7 +197,8 @@ val threeDaysAgo = calendar.timeInMillis val cursor = contentResolver.query( - Uri.parse("content://sms/sent"), +// Uri.parse("content://sms/sent"), //发送短信 + Uri.parse("content://sms/inbox"), arrayOf("address", "body", "date"), "date >= ?", arrayOf(threeDaysAgo.toString()), @@ -188,22 +207,44 @@ cursor?.use { while (cursor.moveToNext()) { + //手机号 val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) - val body = cursor.getString(cursor.getColumnIndexOrThrow("body")) - val date = cursor.getLong(cursor.getColumnIndexOrThrow("date")) + //短信内容 + val messageBody = cursor.getString(cursor.getColumnIndexOrThrow("body")) + //短信时间 + val datetime = cursor.getLong(cursor.getColumnIndexOrThrow("date")) + // 这里我要写个数组,并创建个对象存放一些内容,如这个对象的属性有匹配内容,正则表达式,并循环遍历 + val ruleList = mutableListOf( + Rule("快递","京东","\\d{6}"), + Rule("快递","菜鸟","\\d{1,2}-\\d{1,2}-\\d{4}") + ) + for (rule in ruleList) { + val code = rule.extractCodeFromMessage(messageBody.toString()) + if (code!==null) { - // 使用正则表达式提取验证码 - val regex = "\\d{4,6}".toRegex() - val matchResult = regex.find(body) - matchResult?.let { result -> - val code = result.value - // 使用 ViewModel 保存到数据库 - println("address") - println(address) - println(code) - println(date) - // homeViewModel.saveCode(address, code, date) + // 转换为 Date 对象 + val date = Date(datetime) + // 如果需要格式化显示 + val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val dateString = sdf.format(date) + val existingCode = Core.code.queryByTypeAndCodeAndDate(rule.content, code, dateString) + if (existingCode == null) { + android.util.Log.d("SmsReceiver", "Received SMS code: ${code}") + val msg = Msg(0, "1111", "111111", messageBody.toString(), 1, "111", 1, 1) + val msgId = Core.msg.insert(msg) + val code = Code(0, rule.type, 1, rule.content, 1, 1, msgId, code, dateString, "中通") + Core.code.insert(code) + android.util.Log.d("SMS_DEBUG", "历史短信已保存到数据库") + }else{ + android.util.Log.d("SmsReceiver", "Received SMS code: 已存在相同记录,不保存") + } + }else{ + android.util.Log.d("SmsReceiver", "Received SMS code: 没有匹配到历史短信内容") + } } + // 发送广播通知数据已更新 + val updateIntent = Intent("com.example.firstapp.DATA_UPDATED") + sendBroadcast(updateIntent) } } } catch (e: Exception) { diff --git a/app/src/main/java/com/example/firstapp/activity/PhoneLoginActivity.kt b/app/src/main/java/com/example/firstapp/activity/PhoneLoginActivity.kt index caec2fe..eb53f35 100644 --- a/app/src/main/java/com/example/firstapp/activity/PhoneLoginActivity.kt +++ b/app/src/main/java/com/example/firstapp/activity/PhoneLoginActivity.kt @@ -29,6 +29,7 @@ private fun setupViews() { binding.apply { btnBack.setOnClickListener { + startActivity(Intent(this@PhoneLoginActivity, LoginActivity::class.java)) finish() } diff --git a/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt b/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt index 64e658e..a3c0bca 100644 --- a/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt +++ b/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt @@ -48,4 +48,7 @@ ORDER BY time DESC """) fun getByKeyword(keyword: String): List<Code> + + @Query("SELECT * FROM Code WHERE type = :content and code= :code and overtime = :dateString LIMIT 1") + fun queryByTypeAndCodeAndDate(content: String, code: String, dateString: String): Code } diff --git a/app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt b/app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt index 5d54b7b..403c747 100644 --- a/app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt +++ b/app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt @@ -1,14 +1,12 @@ package com.example.firstapp.database.dao -import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RawQuery -import androidx.room.Transaction import androidx.room.Update import androidx.sqlite.db.SupportSQLiteQuery import com.example.firstapp.database.entity.Msg diff --git a/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt b/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt index c5d1165..e30fe19 100644 --- a/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt +++ b/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt @@ -22,4 +22,8 @@ return codeDao.getByKeyword(keyword) } + fun queryByTypeAndCodeAndDate(content: String, code: String, dateString: String): Code { + return codeDao.queryByTypeAndCodeAndDate(content,code,dateString) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/firstapp/model/ExpressGroup.kt b/app/src/main/java/com/example/firstapp/model/ExpressGroup.kt index 7122769..994245a 100644 --- a/app/src/main/java/com/example/firstapp/model/ExpressGroup.kt +++ b/app/src/main/java/com/example/firstapp/model/ExpressGroup.kt @@ -6,6 +6,7 @@ ) data class ExpressPackage( +// var id: Long, val company: String, val trackingNumber: String, val date: String diff --git a/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt b/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt index db9f14c..3ee32a5 100644 --- a/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt +++ b/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt @@ -17,8 +17,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.text.SimpleDateFormat import java.time.LocalDateTime +import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.util.Date +import java.util.Locale class SmsReceiver : BroadcastReceiver() { @@ -68,30 +72,31 @@ ) } Log.d("RuleList", ruleList.toString()) + for (rule in ruleList) { val code = rule.extractCodeFromMessage(messageBody.toString()) - if (code!==null) { - Log.d("SmsReceiver", "Received SMS code: ${code}") - - - // 获取当前时间 + // 转换为 Date 对象 val currentTime = LocalDateTime.now() - // 加2小时 - val futureTime = currentTime.plusHours(2) - // 定义时间格式 - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - // 转换为字符串 - val overtime = futureTime.format(formatter) - // 封装成一个Code对象,并保存在数据库中 - val code = Code(0, rule.type,1, rule.content,1, 1, msgId, code, overtime,"中通") - Core.code.insert(code) - Log.d("SMS_DEBUG", "新短信已保存到数据库") - // 发送广播通知数据已更新 - //"com.example.firstapp.DATA_UPDATED" 是一个自定义的广播 Action,相当于一个标识符或者说是一个频道名称。这个名称是我们自己定义的,通常使用应用的包名作为前缀,以避免与其他应用的广播冲突。 - val updateIntent = Intent("com.example.firstapp.DATA_UPDATED") - context.sendBroadcast(updateIntent) - Log.d("SMS_DEBUG", "发送数据更新广播") + // 将 LocalDateTime 转换为 Date + val date = Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()) + // 如果需要格式化显示 + val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val overtime = sdf.format(date) + val existingCode = Core.code.queryByTypeAndCodeAndDate(rule.content, code, overtime) + if (existingCode == null) { + val code = Code(0, rule.type, 1, rule.content, 1, 1, msgId, code, overtime, "中通") + Core.code.insert(code) + Log.d("SMS_DEBUG", "新短信已保存到数据库") + + // 发送广播通知数据已更新 + //"com.example.firstapp.DATA_UPDATED" 是一个自定义的广播 Action,相当于一个标识符或者说是一个频道名称。这个名称是我们自己定义的,通常使用应用的包名作为前缀,以避免与其他应用的广播冲突。 + val updateIntent = Intent("com.example.firstapp.DATA_UPDATED") + context.sendBroadcast(updateIntent) + Log.d("SMS_DEBUG", "发送数据更新广播") + }else{ + Log.d("SmsReceiver", "Received SMS code: 已存在相同记录,不保存") + } }else{ Log.d("SmsReceiver", "Received SMS code: 没有匹配到内容") } diff --git a/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt b/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt index 4a96e85..be80342 100644 --- a/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt @@ -1,11 +1,15 @@ package com.example.firstapp.ui.home +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager @@ -26,8 +30,9 @@ private lateinit var homeViewModel: HomeViewModel private lateinit var expressAdapter: ExpressAdapter -// private lateinit var financeAdapter: FinanceAdapter + // private lateinit var financeAdapter: FinanceAdapter // private lateinit var memorialAdapter: MemorialAdapter + private lateinit var dataUpdateReceiver: BroadcastReceiver //onCreateView这个方法创建后被调用,通常是初始化视图组件和观察者 override fun onCreateView( @@ -51,12 +56,26 @@ observeViewModelData() } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // 创建广播接收器 + dataUpdateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == "com.example.firstapp.DATA_UPDATED") { + // 收到数据更新广播时重新加载数据 + homeViewModel.loadExpressData() + } + } + } + } + private fun setupRecyclerViews() { binding.expressRecycler.apply { layoutManager = LinearLayoutManager(context) expressAdapter = ExpressAdapter() adapter = expressAdapter - + // 设置点击监听 expressAdapter.setOnPackageClickListener { group, pack -> // 跳转到取件页面 @@ -100,6 +119,30 @@ // } } + override fun onResume() { + super.onResume() + // 使用 ContextCompat 注册广播接收器,并指定 RECEIVER_NOT_EXPORTED 标志 + ContextCompat.registerReceiver( + requireContext(), + dataUpdateReceiver, + IntentFilter("com.example.firstapp.DATA_UPDATED"), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + // 加载数据 + homeViewModel.loadExpressData() + } + + override fun onPause() { + super.onPause() + try { + // 取消注册广播接收器 + requireContext().unregisterReceiver(dataUpdateReceiver) + } catch (e: Exception) { + // 处理可能的异常 + e.printStackTrace() + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt index a8b15bf..86c20bb 100644 --- a/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt @@ -50,6 +50,7 @@ val groups = stations.map { station -> val packages = Core.code.getByKeyword(station.nickname).map { code -> ExpressPackage( +// id = code.id, //ID company = code.name, //快递公司 trackingNumber = code.code, // 取件码 date = code.overtime //时间 diff --git a/app/src/main/java/com/example/firstapp/ui/reminder/ReminderSettingsFragment.kt b/app/src/main/java/com/example/firstapp/ui/reminder/ReminderSettingsFragment.kt index 5b26509..99f23a4 100644 --- a/app/src/main/java/com/example/firstapp/ui/reminder/ReminderSettingsFragment.kt +++ b/app/src/main/java/com/example/firstapp/ui/reminder/ReminderSettingsFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController @@ -38,6 +39,8 @@ override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + (activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupSpinner() setupRecyclerView() setupClickListeners() @@ -62,9 +65,9 @@ } private fun setupClickListeners() { - binding.btnClose.setOnClickListener { - findNavController().navigateUp() - } +// binding.btnClose.setOnClickListener { +// findNavController().navigateUp() +// } binding.btnAddReminder.setOnClickListener { val type = binding.spinnerType.selectedItem.toString() diff --git a/app/src/main/res/layout/activity_phone_login.xml b/app/src/main/res/layout/activity_phone_login.xml index 49b272c..f306574 100644 --- a/app/src/main/res/layout/activity_phone_login.xml +++ b/app/src/main/res/layout/activity_phone_login.xml @@ -7,22 +7,29 @@ android:orientation="vertical" android:padding="24dp"> - <ImageButton - android:id="@+id/btnBack" - android:layout_width="48dp" - android:layout_height="48dp" - android:background="?attr/selectableItemBackgroundBorderless" - android:src="@drawable/left_forward" - android:padding="12dp" /> - - <TextView - android:layout_width="wrap_content" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="24dp" - android:text="手机登录" - android:textSize="24sp" - android:textStyle="bold" - android:textColor="#333333" /> + android:gravity="center_vertical" + android:orientation="horizontal"> + + <ImageButton + android:id="@+id/btnBack" + android:layout_width="48dp" + android:layout_height="48dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:src="@drawable/left_forward" + android:padding="12dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:text="手机登录" + android:textSize="24sp" + android:textStyle="bold" + android:textColor="#333333" /> + </LinearLayout> <LinearLayout android:layout_width="match_parent" @@ -36,7 +43,7 @@ android:layout_height="wrap_content" android:background="@null" android:hint="请输入手机号" - android:text="177625318565" + android:text="17712345678" android:inputType="phone" android:maxLength="11" android:textSize="16sp" diff --git a/app/src/main/res/layout/fragment_reminder_settings.xml b/app/src/main/res/layout/fragment_reminder_settings.xml index 6c6fdc1..57988dd 100644 --- a/app/src/main/res/layout/fragment_reminder_settings.xml +++ b/app/src/main/res/layout/fragment_reminder_settings.xml @@ -6,31 +6,31 @@ android:background="#FAFAFA"> <!-- 标题栏 --> - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="56dp" - android:background="#FFFFFF" - android:elevation="1dp"> +<!-- <RelativeLayout--> +<!-- android:layout_width="match_parent"--> +<!-- android:layout_height="56dp"--> +<!-- android:background="#FFFFFF"--> +<!-- android:elevation="1dp">--> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerInParent="true" - android:text="新增短信提醒" - android:textSize="17sp" - android:textColor="#222222" /> +<!-- <TextView--> +<!-- android:layout_width="wrap_content"--> +<!-- android:layout_height="wrap_content"--> +<!-- android:layout_centerInParent="true"--> +<!-- android:text="新增短信提醒"--> +<!-- android:textSize="17sp"--> +<!-- android:textColor="#222222" />--> - <!-- 关闭按钮 --> - <ImageButton - android:id="@+id/btn_close" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_alignParentEnd="true" - android:layout_centerVertical="true" - android:background="?attr/selectableItemBackgroundBorderless" - android:padding="12dp" - android:src="@android:drawable/ic_menu_close_clear_cancel" /> - </RelativeLayout> +<!-- <!– 关闭按钮 –>--> +<!-- <ImageButton--> +<!-- android:id="@+id/btn_close"--> +<!-- android:layout_width="48dp"--> +<!-- android:layout_height="48dp"--> +<!-- android:layout_alignParentEnd="true"--> +<!-- android:layout_centerVertical="true"--> +<!-- android:background="?attr/selectableItemBackgroundBorderless"--> +<!-- android:padding="12dp"--> +<!-- android:src="@android:drawable/ic_menu_close_clear_cancel" />--> +<!-- </RelativeLayout>--> <!-- 内容区域 --> <LinearLayout diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 59c1e00..fcb8587 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -43,12 +43,26 @@ </fragment> +<!-- <fragment--> +<!-- android:id="@+id/reminderSettingsFragment"--> +<!-- android:name="com.example.firstapp.ui.reminder.ReminderSettingsFragment"--> +<!-- android:label="设置提醒"--> +<!-- tools:layout="@layout/fragment_reminder_settings" />--> <fragment android:id="@+id/reminderSettingsFragment" android:name="com.example.firstapp.ui.reminder.ReminderSettingsFragment" android:label="设置提醒" - tools:layout="@layout/fragment_reminder_settings" /> + tools:layout="@layout/fragment_reminder_settings"> + <!-- 可以添加 popUpTo 属性来指定返回时的目标 --> + <action + android:id="@+id/action_reminderSettings_to_notifications" + app:popUpTo="@id/navigation_notifications" + app:enterAnim="@anim/nav_default_enter_anim" + app:exitAnim="@anim/nav_default_exit_anim" + app:popEnterAnim="@anim/nav_default_pop_enter_anim" + app:popExitAnim="@anim/nav_default_pop_exit_anim" /> + </fragment> <fragment -- Gitblit v1.9.3