diff --git a/app/src/main/java/com/maary/liveinpeace/ConnectionListAdapter.kt b/app/src/main/java/com/maary/liveinpeace/ConnectionListAdapter.kt index 6da7255..b512a0c 100644 --- a/app/src/main/java/com/maary/liveinpeace/ConnectionListAdapter.kt +++ b/app/src/main/java/com/maary/liveinpeace/ConnectionListAdapter.kt @@ -1,58 +1,88 @@ package com.maary.liveinpeace +import android.annotation.SuppressLint import android.content.Context +import android.content.res.ColorStateList import android.media.AudioDeviceInfo +import android.media.Image import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.maary.liveinpeace.database.Connection import java.util.concurrent.TimeUnit +import kotlin.coroutines.coroutineContext class ConnectionListAdapter : ListAdapter(ConnectionsComparator()){ + private var currentItemLayout = R.layout.item_connection + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConnectionViewHolder { - return ConnectionViewHolder.create(parent) + return ConnectionViewHolder.create(parent, currentItemLayout) } override fun onBindViewHolder(holder: ConnectionViewHolder, position: Int) { val current = getItem(position) - holder.bind( current.name, current.type, current.duration) + holder.bind( current.name, current.type, current.duration, current.disconnectedTime) } class ConnectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val connectionIconView: ImageView = itemView.findViewById(R.id.device_icon) private val connectionDeviceNameView: TextView = itemView.findViewById(R.id.device_name) private val connectionDurationView: TextView = itemView.findViewById(R.id.device_connection_time) + private val connectionIndicatorView: ImageView = itemView.findViewById(R.id.connection_time_prefix) - fun bind(deviceName: String?, type: Int?,duration: Long?) { - if (type in listOf( - AudioDeviceInfo.TYPE_BLE_BROADCAST, - AudioDeviceInfo.TYPE_BLE_HEADSET, - AudioDeviceInfo.TYPE_BLE_SPEAKER, - AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, - AudioDeviceInfo.TYPE_BLUETOOTH_SCO - )){ - connectionIconView.setImageResource(R.drawable.ic_bluetooth_round) - } else { - connectionIconView.setImageResource(R.drawable.ic_headphone_round) - } + fun bind(deviceName: String?, type: Int?, duration: Long?, disconnectedTime: Long?) { + connectionIconView.setImageResource( + chooseDeviceDrawable( + type = type, + drawableHeadphone = R.drawable.ic_headphone_round, + drawableBLE = R.drawable.ic_bluetooth_round)) connectionDeviceNameView.text = deviceName connectionDurationView.text = duration?.let { formatMilliseconds(itemView.context, it) } + + if (disconnectedTime == null){ + // Get the desired tint color from resources + val tintColor = itemView.context.getColor(android.R.color.holo_green_light) + + // Create a ColorStateList with the desired tint color + val colorStateList = ColorStateList.valueOf(tintColor) + connectionIndicatorView.imageTintList = colorStateList + connectionIconView.setImageResource( + chooseDeviceDrawable( + type = type, + drawableHeadphone = R.drawable.ic_headphone_round_alt, + drawableBLE = R.drawable.ic_bluetooth_round_alt)) + } } companion object { - fun create(parent: ViewGroup): ConnectionViewHolder { + fun create(parent: ViewGroup, layoutId: Int): ConnectionViewHolder { val view: View = LayoutInflater.from(parent.context) - .inflate(R.layout.item_connection, parent, false) + .inflate(layoutId, parent, false) return ConnectionViewHolder(view) } } + private fun chooseDeviceDrawable(type: Int?, drawableHeadphone: Int, drawableBLE: Int): Int{ + return if (type in listOf( + AudioDeviceInfo.TYPE_BLE_BROADCAST, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_BLE_SPEAKER, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLUETOOTH_SCO + )){ + (drawableBLE) + } else { + (drawableHeadphone) + } + } + private fun formatMilliseconds(context: Context, milliseconds: Long): String { val hours = TimeUnit.MILLISECONDS.toHours(milliseconds) val minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60 @@ -64,6 +94,13 @@ class ConnectionListAdapter : ListAdapter(){ override fun areItemsTheSame(oldItem: Connection, newItem: Connection): Boolean { return oldItem == newItem diff --git a/app/src/main/java/com/maary/liveinpeace/DeviceMapChangeListener.kt b/app/src/main/java/com/maary/liveinpeace/DeviceMapChangeListener.kt new file mode 100644 index 0000000..0148f1b --- /dev/null +++ b/app/src/main/java/com/maary/liveinpeace/DeviceMapChangeListener.kt @@ -0,0 +1,7 @@ +package com.maary.liveinpeace + +import com.maary.liveinpeace.database.Connection + +interface DeviceMapChangeListener { + fun onDeviceMapChanged(deviceMap: Map) +} diff --git a/app/src/main/java/com/maary/liveinpeace/HistoryActivity.kt b/app/src/main/java/com/maary/liveinpeace/HistoryActivity.kt index 9976181..16fbf6d 100644 --- a/app/src/main/java/com/maary/liveinpeace/HistoryActivity.kt +++ b/app/src/main/java/com/maary/liveinpeace/HistoryActivity.kt @@ -1,21 +1,55 @@ package com.maary.liveinpeace import android.os.Bundle +import android.util.Log +import android.view.View import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.LinearLayoutManager +import com.maary.liveinpeace.database.Connection import com.maary.liveinpeace.databinding.ActivityHistoryBinding +import com.maary.liveinpeace.service.ForegroundService import java.time.LocalDate -class HistoryActivity : AppCompatActivity() { +class HistoryActivity : AppCompatActivity(), DeviceMapChangeListener { private lateinit var binding: ActivityHistoryBinding private val connectionViewModel: ConnectionViewModel by viewModels { ConnectionViewModelFactory((application as ConnectionsApplication).repository) } + private val currentAdapter = ConnectionListAdapter() + + override fun onResume() { + super.onResume() + ForegroundService.addDeviceMapChangeListener(this) + } + + override fun onPause() { + super.onPause() + ForegroundService.removeDeviceMapChangeListener(this) + } + + private fun currentConnectionsDuration(currentList: MutableList) : MutableList{ + val now = System.currentTimeMillis() + + for ( (index, connection) in currentList.withIndex()){ + val connectedTime = connection.connectedTime + val duration = now - connectedTime!! + currentList[index] = Connection( + name = connection.name, + type = connection.type, + connectedTime = connection.connectedTime, + disconnectedTime = null, + duration = duration, + date = connection.date + ) + } + + return currentList + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -24,6 +58,7 @@ class HistoryActivity : AppCompatActivity() { setContentView(binding.root) val connectionAdapter = ConnectionListAdapter() + binding.historyList.isNestedScrollingEnabled = false binding.historyList.adapter = connectionAdapter binding.historyList.layoutManager = LinearLayoutManager(this) @@ -33,6 +68,12 @@ class HistoryActivity : AppCompatActivity() { finish() } + binding.currentList.isNestedScrollingEnabled = false + binding.currentList.adapter = currentAdapter + binding.currentList.layoutManager = LinearLayoutManager(this) + + updateCurrentAdapter() + connectionViewModel.getAllConnectionsOnDate(LocalDate.now().toString()).observe(this) { connections -> connections.let { connectionAdapter.submitList(it) } } @@ -43,15 +84,40 @@ class HistoryActivity : AppCompatActivity() { connectionViewModel.getAllConnectionsOnDate(LocalDate.now().toString()).observe(this) { connections -> connections.let { connectionAdapter.submitList(it) } } + updateCurrentAdapter() } if (checkedId == R.id.button_summary) { connectionViewModel.getSummaryOnDate(LocalDate.now().toString()).observe(this) { connections -> connections.let { connectionAdapter.submitList(it) } } + updateCurrentAdapter() } } } + + private fun updateCurrentAdapter(){ + currentAdapter.submitList(currentConnectionsDuration(ForegroundService.getConnections())) + if (currentAdapter.itemCount == 0){ + Log.v("MUTE_", "GG") + binding.titleCurrent.visibility = View.GONE + }else{ + binding.titleCurrent.visibility = View.VISIBLE + } + } + + override fun onDeviceMapChanged(deviceMap: Map) { + if (deviceMap.isEmpty()){ + Log.v("MUTE_", "GGA") + + binding.titleCurrent.visibility = View.GONE + }else{ + Log.v("MUTE_", "GGB") + + binding.titleCurrent.visibility = View.VISIBLE + } + currentAdapter.submitList(currentConnectionsDuration(deviceMap.values.toMutableList())) + } } \ No newline at end of file diff --git a/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt b/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt index e293f70..90e23da 100644 --- a/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt +++ b/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt @@ -38,6 +38,7 @@ import com.maary.liveinpeace.Constants.Companion.MODE_NUM import com.maary.liveinpeace.Constants.Companion.PREF_ICON import com.maary.liveinpeace.Constants.Companion.PREF_WATCHING_CONNECTING_TIME import com.maary.liveinpeace.Constants.Companion.SHARED_PREF +import com.maary.liveinpeace.DeviceMapChangeListener import com.maary.liveinpeace.HistoryActivity import com.maary.liveinpeace.R import com.maary.liveinpeace.database.Connection @@ -58,7 +59,7 @@ class ForegroundService: Service() { private lateinit var connectionDao: ConnectionDao private val deviceTimerMap: MutableMap = mutableMapOf() - private val deviceMap: MutableMap = mutableMapOf() +// private val deviceMap: MutableMap = mutableMapOf() private val volumeDrawableIds = intArrayOf( R.drawable.ic_volume_silent, @@ -81,6 +82,29 @@ class ForegroundService: Service() { fun isForegroundServiceRunning(): Boolean { return isForegroundServiceRunning } + + private val deviceMap: MutableMap = mutableMapOf() + + // 在伴生对象中定义一个静态方法,用于其他类访问deviceMap + fun getConnections(): MutableList { + return deviceMap.values.toMutableList() + } + + private val deviceMapChangeListeners: MutableList = mutableListOf() + + fun addDeviceMapChangeListener(listener: DeviceMapChangeListener) { + deviceMapChangeListeners.add(listener) + } + + fun removeDeviceMapChangeListener(listener: DeviceMapChangeListener) { + deviceMapChangeListeners.remove(listener) + } + } + + private fun notifyDeviceMapChange() { + deviceMapChangeListeners.forEach { listener -> + listener.onDeviceMapChanged(deviceMap) + } } private fun getVolumePercentage(context: Context): Int { @@ -126,6 +150,31 @@ class ForegroundService: Service() { } } + private fun saveDateWhenStop(){ + val disconnectedTime = System.currentTimeMillis() + + for ( (deviceName, connection) in deviceMap){ + + val connectedTime = connection.connectedTime + val connectionTime = disconnectedTime - connectedTime!! + + CoroutineScope(Dispatchers.IO).launch { + connectionDao.insert( + Connection( + name = connection.name, + type = connection.type, + connectedTime = connection.connectedTime, + disconnectedTime = disconnectedTime, + duration = connectionTime, + date = connection.date + ) + ) + } + deviceMap.remove(deviceName) + } + return + } + private val audioDeviceCallback = object : AudioDeviceCallback() { @SuppressLint("MissingPermission") override fun onAudioDevicesAdded(addedDevices: Array?) { @@ -158,6 +207,7 @@ class ForegroundService: Service() { duration = null, date = LocalDate.now().toString() ) + notifyDeviceMapChange() // 执行其他逻辑,比如将设备信息保存到数据库或日志中 } @@ -216,6 +266,7 @@ class ForegroundService: Service() { } deviceMap.remove(deviceName) + notifyDeviceMapChange() } val sharedPreferences = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE) @@ -254,12 +305,12 @@ class ForegroundService: Service() { } override fun onDestroy() { - super.onDestroy() - + saveDateWhenStop() // 取消注册音量变化广播接收器 unregisterReceiver(volumeChangeReceiver) audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) isForegroundServiceRunning = false + super.onDestroy() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { diff --git a/app/src/main/res/drawable/ic_bluetooth_alt.xml b/app/src/main/res/drawable/ic_bluetooth_alt.xml new file mode 100644 index 0000000..462ac0d --- /dev/null +++ b/app/src/main/res/drawable/ic_bluetooth_alt.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_bluetooth_round_alt.xml b/app/src/main/res/drawable/ic_bluetooth_round_alt.xml new file mode 100644 index 0000000..a47d618 --- /dev/null +++ b/app/src/main/res/drawable/ic_bluetooth_round_alt.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_headphone_alt.xml b/app/src/main/res/drawable/ic_headphone_alt.xml new file mode 100644 index 0000000..4f8e69a --- /dev/null +++ b/app/src/main/res/drawable/ic_headphone_alt.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_headphone_round_alt.xml b/app/src/main/res/drawable/ic_headphone_round_alt.xml new file mode 100644 index 0000000..513dec1 --- /dev/null +++ b/app/src/main/res/drawable/ic_headphone_round_alt.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_alt.xml b/app/src/main/res/drawable/shape_circle_alt.xml new file mode 100644 index 0000000..4cb26ee --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_alt.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml index 9326f7e..cc6eedf 100644 --- a/app/src/main/res/layout/activity_history.xml +++ b/app/src/main/res/layout/activity_history.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - - + android:orientation="vertical" + android:background="@android:color/transparent"> + + + + + + + + + diff --git a/app/src/main/res/layout/item_connection.xml b/app/src/main/res/layout/item_connection.xml index 99ef67d..e0ece23 100644 --- a/app/src/main/res/layout/item_connection.xml +++ b/app/src/main/res/layout/item_connection.xml @@ -26,18 +26,30 @@ android:textSize="18sp" android:textColor="?android:attr/textColorPrimary" android:layout_marginStart="16dp" - android:textStyle="bold" + tools:text="Headphone"/> + + diff --git a/app/src/main/res/layout/item_current_connections.xml b/app/src/main/res/layout/item_current_connections.xml new file mode 100644 index 0000000..f5c0e83 --- /dev/null +++ b/app/src/main/res/layout/item_current_connections.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3e3efc2..64b38ca 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -33,4 +33,8 @@ %02d 小时 %02d 分钟 启用提醒 禁用提醒 + 当前连接 + 历史连接 + 已连接 + 设备连接状态指示 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fac937a..63255ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,7 +17,7 @@ %1$s has been connected for over two hours. Alerts Notification channel for reminding about excessive device connection time. - History + Connections Timeline Summary Device icon @@ -25,6 +25,10 @@ %02d h %02d min Enable alert Disable alert + Current Connections + Connections Histroy + Already connected + Device connection state indicator Shhhhhhh… Perfectly Balanced, As All Things Should Be…