To compile the project you need JDK 11 and the standard Android toolchain.
The project uses a standard MVVM design with repository pattern. If you're unfamiliar with that pattern please have a look at this guide to get started.
The UI part is very simple, it only contains a single Activity (MainActivity
) with a 3-tabs layout.
Packages are organized by screen (UI) and features (non UI) as follows:
ui
dashboard
:Fragment
andViewModel
of the "Dashboard" tabalerts
:Fragment
andViewModel
of the "Alerts" tabsettings
:Fragment
andViewModel
of the "Settings" tab
domain
: models for the UI (alerts)database
: models for the database (alerts) and DAOrepository
: single source of truth for data (alerts)di
: dependency injection stuffdata
: agents and service to collect data (cpu, mem, battery)
The core part of the app is data collection, that is handled by CollectionService
; to start/stop data collection just start and stop that service with context.startService()
and context.stopService()
. This is done in the DashboardFragment
when the user clicks the "Enable data collection" button. By default data collection happens also when the app is in background, if the user has opted out this feature then the CollectionService
is stopped by the MainActivity
when the app is no longer visible to the user (onStop
).
The data flow is as follows:
CollectorService
creates a separate thread and spawns all agents on that thread.- Each
Agent
collects the data (for example the cpu load) and reports it back toCollectorService
throughCollectorServiceCallback
CollectorService
is then responsible for creating notifications and storing the alert in the repository.
TODO
: add a diagram with the flow
To run in the background the CollectorService
calls startForeground()
and creates a visible notification. This dramatically reduces the chances of being killed by the OS.
To create a new agent just subclass the Agent
and implement its abstract methods:
override val name: String
get() = context.getString(R.string.mem_agent_name)
override fun aboveThMessage(value: Int): String =
context.getString(R.string.mem_above_th_message, value)
override fun belowThMessage(value: Int): String =
context.getString(R.string.mem_below_th_message)
Agent
is a generic data collection class that runs an infinite loop and collects data every X seconds.
If the new agent follows the same logic (collect data, wait for it, collect data, ...) then you should just implement getData()
and return the data; as an example you can see the MemoryAgent
which the simplest agent possible:
override suspend fun getData(): PerfValue {
val th = prefs.getInt(
context.getString(R.string.mem_alert_pref_key),
context.resources.getInteger(R.integer.mem_alert_default_th)
)
return parseProcMemInfo().toPerfValue(th)
}
If the new agent has a completely different logic then you should override enable()
and avoid calling super()
so that the collect loop is not created. Then, just call setData() directly to report back to CollectorService
; see BatteryAgent
as an example of such logic (that agent relies on Android intents that are sent out when battery level changes).
Please notice the value reported to CollectorService
is a PerfValue
object with two fields:
- the actual
value
expressed in percentage (e.g. "80") - a boolean to tell if
valueIsAboveTh
: this is needed because "above the threshold" actually means "in an error state" and the logic could be agent-specific (for exampleBatteryAgent
setsvalueIsAboveTh
totrue
when the value is below the threshold).
Finally, enable the agent by modifying CollectorService.spawnAgents()
and add the new agent to the agents
list.
Starting from Android O it's no longer possible to collect cpu usage because it's an "information leak". See this official thread. In the app I've implemented a few workarounds but they only work on older and rooted devices. In the market there are a few "cpu monitoring" tools, all of them either don't work or use the cpu frequency to assume the load. Since this approach is totally misleading I've decided not to implement it and just return 0 if no reliable way is possible.
Notifications are grouped by agent (to avoid flooding the user), which means that there won't be no more than 1 notification per agent, plus the persistent notification to keep collector service in foreground. When a new alert is raised it overwrites any previous notification from the same agent.
All alerts can be seen in the "Alerts" section by the way.
Here's a few ideas:
- create a layout optimized for tablets and landscape screens
- show current values (cpu, mem, battery) to the user somehow, i.e. in the persistent notification or in the dashboard
- add charts for collected data
- use NDK and JNI to collect data in a more performant way
- use Compose for layouts
- do more tests about power consumption to fine tune the infinite loop interval (currently set to 60'' in release and 5'' in debug)
- let the user choose the infinite loop interval to balance the trade off between accuracy and battery drain
- add a button to clear alerts
- improve the alerts section with sorting and filters
- let the user choose which agents should run individually
- add actions to start and stop the collection service from the notification
- start the service at boot (configurable)