Compare commits

...
60 Commits
Author SHA1 Message Date
Kamil Gurgul f7881c7a15 GENERAL - fix crash on Android 4.4 2019-07-07 10:53:22 +02:00
Kamil Gurgul 22050d6e8a GENERAL - fix dark theme for Q 2019-07-01 06:52:12 +02:00
Kamil Gurgul a27691171c GENERAL - remove exif from images 2019-06-30 20:34:07 +02:00
Kamil Gurgul 3cc4314ffd GENERAL - fix unit tests 2019-06-30 19:13:41 +02:00
Kamil Gurgul 4744f9a7d0 GENERAL - try to remove travis cache 2019-06-30 18:58:06 +02:00
Kamil Gurgul bad64d8f20 Merge branch 'feature/dark_them' 2019-06-29 20:42:09 +02:00
Richy HBMandKamil Gurgul 3d198c1f4e Add a tile to view average CPU load (#27)
* Get average CPU each second when looking at tile

* Change icon depending on load
2019-06-22 16:41:30 +02:00
Kamil Gurgul ebb90a95b0 DARK-THEME - fix missing layouts, update dagger 2019-06-17 20:55:54 +02:00
Kamil Gurgul 2df1e1bfc8 Dark theme - libs update, add initializers, remove old ram cleaner 2019-06-17 20:34:10 +02:00
Kamil Gurgul 8013cae879 Dark theme - fix text colors, update material lib 2019-06-04 21:47:48 +02:00
Kamil Gurgul 6560215e26 DARK-THEME - change light theme colors 2019-05-26 15:05:59 +02:00
Kamil Gurgul 91535cd653 DARK-THEME - fix application list colors 2019-05-25 19:33:35 +02:00
Kamil Gurgul 5e1add510d Dark theme - fix dialog, nav header and temperature item colors 2019-05-22 21:28:49 +02:00
Kamil Gurgul 60aeb29590 Base for dark them - in progress 2019-05-19 22:07:13 +02:00
Kamil Gurgul a59bc4891a GENERAL - fix menu icon 2019-03-29 18:15:02 +01:00
Kamil Gurgul 5826eea632 GENERAL - start style cleanup 2019-03-17 19:11:26 +01:00
Kamil Gurgul aee9826097 GENERAL - removed delayed refreshing state 2019-03-17 18:50:06 +01:00
Kamil Gurgul 03c51a51c7 GENERAL - small UI animation improvement 2019-03-15 21:23:35 +01:00
Kamil Gurgul bb5665e2d8 GENERAL - add splash 2019-03-15 20:12:39 +01:00
Kamil Gurgul c2738e5540 GENERAL - fix glide app icon loading 2019-03-15 18:46:39 +01:00
Kamil Gurgul 4f962063f6 GENERAL - fix opencsv proguard 2019-03-15 17:16:54 +01:00
Kamil Gurgul a03a40e396 GENERAL - fix uninstall intent 2019-03-15 17:08:56 +01:00
Jeff HuangandKamil Gurgul cb3c7c4f45 Update zh-rTW translation. (#21) 2019-02-28 06:34:45 +01:00
Kamil Gurgul c5c421572b #15 - add export option 2019-02-27 10:48:30 +01:00
Jeff HuangandKamil Gurgul 94517190b1 Update zh-rTW translation. (#20) 2019-02-27 06:22:39 +01:00
Kamil Gurgul a244fe2a41 GENERAL - update libs, bump version to 4.3.0 2019-02-26 06:45:50 +01:00
Kamil Gurgul 85e6b05be4 #18 - add encrypted storage status 2019-02-26 06:41:12 +01:00
Jeff HuangandKamil Gurgul 3f759ad47f Sync and update zh-rTW translations. (#17) 2019-01-15 18:46:42 +01:00
Kamil Gurgul 946be85f00 Update libs - ready for new release 2018-12-08 14:23:29 +01:00
Kamil Gurgul 22da53f39d Update libs 2018-10-27 17:07:35 +02:00
lazyStudntandKamil Gurgul d7ff7bf371 Updated zh-rTW (Chinese (Taiwan)) translation (#11)
Commit made via Stringlate
2018-10-27 16:40:09 +02:00
FliegendeWurstandKamil Gurgul c9b9353846 Add german translation (#12) 2018-10-27 16:39:55 +02:00
PoussinouandKamil Gurgul 694952c458 Update README.md (#14) 2018-10-17 16:57:26 +02:00
Kamil Gurgul 627dcfc014 Bring back gradle-all. F-droid release 2018-09-27 16:42:38 +02:00
Kamil Gurgul 5a46bbe73a Update gradle 2018-09-27 07:07:59 +02:00
Kamil Gurgul 035bab3d82 Update gradle 2018-09-27 06:45:46 +02:00
Kamil Gurgul 2922e70505 Bump version 2018-09-26 17:20:29 +02:00
p-boandKamil Gurgul efe39d77dc Added Czech localization (#8)
* Added Czech localization
* Removed non translatable strings.
2018-09-26 16:23:11 +02:00
Kamil Gurgul 3ff723fd45 Update libs 2018-09-26 16:20:02 +02:00
Kamil Gurgul 319c7d66eb Try to fix travis 2018-09-18 23:03:26 +02:00
Kamil Gurgul c7d15a02b7 Remove fake google config 2018-09-18 22:37:06 +02:00
Kamil Gurgul 27854c071c Remove Google services - force release 2018-09-18 22:35:06 +02:00
Kamil Gurgul a7afc31fd4 Libs update, small refactoring 2018-09-15 11:40:50 +02:00
Kamil Gurgul a7ef7b622c Change lifecycle owner for fragments 2018-08-25 11:50:03 +02:00
Kamil Gurgul a73271b49c Migration to AndroidX 2018-08-25 11:26:06 +02:00
Kamil Gurgul cccb0f7698 Add copy option for info tab 2018-08-15 14:09:48 +02:00
Kamil Gurgul 32c1cd634c Remove temperature item binding 2018-08-14 17:19:11 +02:00
Kamil Gurgul 26d40e9563 Update nav library 2018-08-13 21:00:09 +02:00
Kamil Gurgul d902298b98 Changed to dark mode, tablet redesign 2018-08-13 17:53:53 +02:00
Kamil Gurgul c46ecfbbc8 Merge remote-tracking branch 'origin/feature/new_navigation' into feature/new_navigation
# Conflicts:
#	build.gradle
#	buildSrc/src/main/java/Dependecies.kt
2018-08-08 14:10:25 +02:00
Kamil Gurgul 8e0c680eca Versions update and refactoring 2018-08-08 14:09:29 +02:00
Kamil Gurgul 5d5b02178c Update libs 2018-06-09 17:51:56 +02:00
Kamil Gurgul 8ee511d0a5 Add new android navigation lib 2018-05-16 22:11:50 +02:00
Kamil Gurgul 43f0f2f7ab Libs update, small refactoring 2018-05-09 13:23:55 +02:00
Kamil Gurgul 9ead029ec3 Remove AutoClearedValue wrapper and change ObservableList to new
ListLiveData
2018-05-02 21:33:28 +02:00
Kamil Gurgul 05a6014c6b Remove databinding warnings 2018-04-29 11:56:50 +02:00
Kamil Gurgul 2b7055ecc6 Fix tests 2018-04-29 11:46:17 +02:00
Kamil Gurgul 96b0d1c2e9 Update libs 2018-04-29 11:22:00 +02:00
Kamil Gurgul 116af9c59b Merge remote-tracking branch 'origin/master'
# Conflicts:
#	build.gradle
2018-04-29 11:13:49 +02:00
Kamil Gurgul 65645e7275 Remove AutClearedValue wrapper from bindings. Preparations to drop it
completely.
2018-04-29 11:13:17 +02:00
167 changed files with 3497 additions and 2318 deletions
+2 -10
View File
@@ -5,23 +5,15 @@ android:
- tools
- platform-tools
- tools
- build-tools-27.0.3
- build-tools-28.0.2
- android-19
- android-27
- android-28
- sys-img-armeabi-v7a-android-19
licenses:
- 'android-sdk-preview-license-.+'
- 'android-sdk-license-.+'
- 'google-gdk-license-.+'
before_cache:
-rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
-rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
-$HOME/.gradle/caches/
-$HOME/.gradle/wrapper/
before_script:
- echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a
- emulator -avd test -no-audio -no-window &
+8 -5
View File
@@ -1,14 +1,17 @@
<img src="info/icon_glow.png" width="100" height="100" />
[![Build Status](https://travis-ci.org/kamgurgul/cpu-info.svg?branch=master)](https://travis-ci.org/kamgurgul/cpu-info)<br/>
CPU Info
CPU Info [![Build Status](https://travis-ci.org/kamgurgul/cpu-info.svg?branch=master)](https://travis-ci.org/kamgurgul/cpu-info)<br/>
========
CPU Info provides information about Android device hardware and software.
Most of the code is written in Kotlin but some old widgets are still in
Java.
Current version can be found on Google Play:<br />
[![Get it on Google Play](info/google-play-badge.png)](https://play.google.com/store/apps/details?id=com.kgurgul.cpuinfo)
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.kgurgul.cpuinfo/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.kgurgul.cpuinfo)
Used libraries
==============
@@ -53,4 +56,4 @@ License
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.
+41 -41
View File
@@ -1,27 +1,25 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'org.jetbrains.kotlin.android.extensions'
apply plugin: 'io.fabric'
apply from: "$rootDir/reports/jacoco.gradle"
apply from: "$rootDir/versioning.gradle"
Properties signingProperties = getReleaseSigningConfig()
android {
compileSdkVersion Versions.compiledSdk
buildToolsVersion Versions.buildTools
compileSdkVersion compiledSdk
buildToolsVersion buildTools
defaultConfig {
applicationId "com.kgurgul.cpuinfo"
minSdkVersion Versions.minSdk
targetSdkVersion Versions.targetSdk
minSdkVersion project.minSdk
targetSdkVersion project.targetSdk
versionCode generatedVersionCode
versionName generatedVersionName
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
@@ -66,59 +64,63 @@ androidExtensions {
}
kapt {
generateStubs = true
mapDiagnosticLocations = true
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "com.android.support:appcompat-v7:${Versions.androidSupport}"
implementation "com.android.support:design:${Versions.androidSupport}"
implementation "com.android.support:preference-v14:${Versions.androidSupport}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$project.kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
implementation "org.jetbrains.anko:anko-coroutines:${Versions.anko}"
// Firebase
releaseImplementation 'com.crashlytics.sdk.android:crashlytics:2.9.1'
implementation "com.google.firebase:firebase-core:${Versions.firebase}"
implementation "androidx.appcompat:appcompat:$project.appCompat"
implementation "androidx.core:core:$project.core"
implementation "com.google.android.material:material:$project.material"
implementation "androidx.preference:preference:$project.preference"
// Bus
implementation 'org.greenrobot:eventbus:3.1.1'
implementation "org.greenrobot:eventbus:$project.eventBus"
// Rx
implementation 'io.reactivex.rxjava2:rxjava:2.1.4'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation "io.reactivex.rxjava2:rxjava:$project.rxJava"
implementation "io.reactivex.rxjava2:rxandroid:$project.rxAndroid"
// GSON
implementation 'com.google.code.gson:gson:2.8.0'
implementation "com.google.code.gson:gson:$project.gson"
// Dagger
implementation "com.google.dagger:dagger:${Versions.dagger}"
implementation "com.google.dagger:dagger-android-support:${Versions.dagger}"
kapt "com.google.dagger:dagger-compiler:${Versions.dagger}"
kapt "com.google.dagger:dagger-android-processor:${Versions.dagger}"
implementation "com.google.dagger:dagger:$project.dagger"
implementation "com.google.dagger:dagger-android-support:$project.dagger"
kapt "com.google.dagger:dagger-compiler:$project.dagger"
kapt "com.google.dagger:dagger-android-processor:$project.dagger"
// Timber
implementation 'com.jakewharton.timber:timber:4.5.1'
implementation "com.jakewharton.timber:timber:$project.timber"
// Google arch
implementation "android.arch.lifecycle:extensions:${Versions.archLifecycle}"
kapt "android.arch.lifecycle:compiler:${Versions.archLifecycle}"
implementation "androidx.lifecycle:lifecycle-extensions:$project.archLifecycle"
kapt "androidx.lifecycle:lifecycle-compiler:$project.archLifecycle"
// Navigation
implementation "android.arch.navigation:navigation-fragment-ktx:$project.navigation"
implementation "android.arch.navigation:navigation-ui-ktx:$project.navigation"
// Glide
implementation "com.github.bumptech.glide:glide:${Versions.glide}"
kapt "com.github.bumptech.glide:compiler:${Versions.glide}"
implementation "com.github.bumptech.glide:glide:$project.glide"
kapt "com.github.bumptech.glide:compiler:$project.glide"
testImplementation 'junit:junit:4.12'
testImplementation "org.mockito:mockito-core:2.11.0"
testImplementation "com.nhaarman:mockito-kotlin-kt1.1:1.5.0"
testImplementation "android.arch.core:core-testing:${Versions.archLifecycle}"
// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$project.coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$project.coroutines"
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation "com.android.support.test.espresso:espresso-core:${Versions.espresso}"
androidTestImplementation "com.android.support.test.espresso:espresso-contrib:${Versions.espresso}"
testImplementation "junit:junit:$project.jUnit"
testImplementation "org.mockito:mockito-core:$project.mockito"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$project.mockitoKotlin"
testImplementation "androidx.arch.core:core-testing:$project.coreTesting"
testImplementation "org.robolectric:robolectric:$project.robolectric"
androidTestImplementation "androidx.test:runner:$project.testRunner"
androidTestImplementation "androidx.test:rules:$project.testRunner"
androidTestImplementation "androidx.test.espresso:espresso-core:$project.espresso"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$project.espresso"
}
def getReleaseSigningConfig() {
@@ -147,5 +149,3 @@ def getDebugSigningConfig() {
properties.setProperty('keyPass', 'android')
return properties
}
apply plugin: 'com.google.gms.google-services'
-76
View File
@@ -1,76 +0,0 @@
{
"project_info": {
"project_number": "11111111111",
"firebase_url": "https://cpu-info.firebaseio.com",
"project_id": "cpu-info",
"storage_bucket": "cpu-info.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:111111111111:android:1111111111111111",
"android_client_info": {
"package_name": "com.kgurgul.cpuinfo"
}
},
"oauth_client": [
{
"client_id": "111111111111-abcdjfkdfjhgkdhfkdfhjgdjkh22232w.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "laksjdkldjfskljfklJklsJSdlKjfsdfjklsdjfl"
}
],
"services": {
"analytics_service": {
"status": 2,
"analytics_property": {
"tracking_id": "UA-11111111-1"
}
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:111111111111:android:1111111111111111",
"android_client_info": {
"package_name": "com.kgurgul.cpuinfo.debug"
}
},
"oauth_client": [
{
"client_id": "111111111111-abcdjfkdfjhgkdhfkdfhjgdjkh22232w.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "laksjdkldjfskljfklJklsJSdlKjfsdfjklsdjfl"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}
Binary file not shown.
+27 -6
View File
@@ -43,9 +43,30 @@
public *;
}
# Crashlytics
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable
-keep public class * extends java.lang.Exception
-keep class com.crashlytics.** { *; }
-dontwarn com.crashlytics.**
# Coroutines
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}
# OpenCSV
-dontwarn com.opencsv.**
-dontwarn org.apache.commons.beanutils.**
-dontwarn org.apache.commons.collections.**
# Remove logs
-assumenosideeffects class timber.log.Timber* {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static *** e(...);
}
# VerifyError in Android 4
# https://github.com/material-components/material-components-android/issues/397
-keep class com.google.android.material.tabs.TabLayout$Tab {
*;
}
@@ -16,10 +16,10 @@
package com.kgurgul.cpuinfo.core
import android.support.test.espresso.matcher.BoundedMatcher
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
@@ -54,14 +54,14 @@ object CustomMatchers {
*/
fun atPosition(position: Int, itemMatcher: Matcher<View>): Matcher<View> {
checkNotNull(itemMatcher)
return object : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {
return object : BoundedMatcher<View, androidx.recyclerview.widget.RecyclerView>(androidx.recyclerview.widget.RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
override fun matchesSafely(view: androidx.recyclerview.widget.RecyclerView): Boolean {
val viewHolder = view.findViewHolderForAdapterPosition(position) ?: return false
return itemMatcher.matches(viewHolder.itemView)
}
@@ -16,11 +16,11 @@
package com.kgurgul.cpuinfo.core
import android.support.test.espresso.NoMatchingViewException
import android.support.test.espresso.ViewAssertion
import android.support.test.espresso.matcher.ViewMatchers.assertThat
import android.support.v7.widget.RecyclerView
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import org.hamcrest.Matcher
/**
@@ -49,8 +49,8 @@ class RecyclerViewItemCountAssertion : ViewAssertion {
throw noViewFoundException
}
val recyclerView = view as RecyclerView
val recyclerView = view as androidx.recyclerview.widget.RecyclerView
val adapter = recyclerView.adapter
assertThat(adapter.itemCount, matcher)
assertThat(adapter?.itemCount, matcher)
}
}
@@ -19,7 +19,7 @@
package com.kgurgul.cpuinfo.core
import android.app.Activity
import android.support.test.rule.ActivityTestRule
import androidx.test.rule.ActivityTestRule
import org.hamcrest.CoreMatchers
/**
@@ -16,14 +16,14 @@
package com.kgurgul.cpuinfo.screens
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.action.ViewActions.click
import android.support.test.espresso.action.ViewActions.scrollTo
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition
import android.support.test.espresso.matcher.ViewMatchers.*
import android.support.v7.widget.RecyclerView
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
import androidx.test.espresso.matcher.ViewMatchers.*
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.core.CustomMatchers.atPosition
import com.kgurgul.cpuinfo.core.CustomMatchers.withToolbarTitle
@@ -65,7 +65,7 @@ class HardwareScreen {
*/
fun hasTextOnPosition(text: String, position: Int) {
onView(allOf(isDisplayed(), recyclerView))
.perform(scrollToPosition<RecyclerView.ViewHolder>(position))
.perform(scrollToPosition<androidx.recyclerview.widget.RecyclerView.ViewHolder>(position))
.check(matches(atPosition(position, hasDescendant(withText(text)))))
}
@@ -16,9 +16,9 @@
package com.kgurgul.cpuinfo.testsuits.information
import android.support.test.filters.LargeTest
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.core.getString
import com.kgurgul.cpuinfo.features.HostActivity
+14 -2
View File
@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<application
android:name=".CpuInfoApp"
@@ -16,12 +17,14 @@
android:resizeableActivity="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/AppThemeBase.Material"
android:theme="@style/AppThemeBase"
tools:ignore="AllowBackup,GoogleAppIndexingWarning"
tools:targetApi="n">
<activity
android:name="com.kgurgul.cpuinfo.features.HostActivity"
android:label="@string/app_name">
android:label="@string/app_name"
android:theme="@style/AppThemeBase.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -45,6 +48,15 @@
android:resource="@xml/ram_widget_provider" />
</receiver>
<service android:name=".features.cputile.CPUTile"
android:icon="@drawable/ic_cpu_low"
android:label="@string/app_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
</application>
</manifest>
@@ -18,16 +18,12 @@ package com.kgurgul.cpuinfo
import android.app.Activity
import android.app.Application
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Build
import android.app.Service
import com.kgurgul.cpuinfo.appinitializers.AppInitializers
import com.kgurgul.cpuinfo.di.AppInjector
import com.kgurgul.cpuinfo.features.ramwidget.RamUsageWidgetProvider
import com.kgurgul.cpuinfo.utils.isDebugBuild
import com.kgurgul.cpuinfo.utils.runOnApiBelow
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import timber.log.Timber
import dagger.android.HasServiceInjector
import javax.inject.Inject
/**
@@ -35,41 +31,24 @@ import javax.inject.Inject
*
* @author kgurgul
*/
class CpuInfoApp : Application(), HasActivityInjector {
companion object {
lateinit var instance: CpuInfoApp
}
class CpuInfoApp : Application(), HasActivityInjector, HasServiceInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
@Inject
lateinit var dispatchingServiceInjector: DispatchingAndroidInjector<Service>
@Inject
lateinit var initializers: AppInitializers
override fun onCreate() {
super.onCreate()
instance = this
if (isDebugBuild()) {
Timber.plant(Timber.DebugTree())
}
AppInjector.init(this)
tryToUpdateRamWidget()
initializers.init(this)
}
/**
* Try to create refresh service for ram widget. Currently it will work only on API below 26
*/
private fun tryToUpdateRamWidget() {
runOnApiBelow(Build.VERSION_CODES.O, {
Timber.d("updateRamWidget()")
val intent = Intent(this, RamUsageWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = intArrayOf(R.xml.ram_widget_provider)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
})
}
override fun activityInjector() = dispatchingAndroidInjector
override fun activityInjector(): DispatchingAndroidInjector<Activity>
= dispatchingAndroidInjector
override fun serviceInjector() = dispatchingServiceInjector
}
@@ -0,0 +1,7 @@
package com.kgurgul.cpuinfo.appinitializers
import android.app.Application
interface AppInitializer {
fun init(application: Application)
}
@@ -0,0 +1,14 @@
package com.kgurgul.cpuinfo.appinitializers
import android.app.Application
import javax.inject.Inject
class AppInitializers @Inject constructor(
private val initializers: Set<@JvmSuppressWildcards AppInitializer>
) {
fun init(application: Application) {
initializers.forEach {
it.init(application)
}
}
}
@@ -0,0 +1,31 @@
package com.kgurgul.cpuinfo.appinitializers
import android.app.Application
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Build
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.features.ramwidget.RamUsageWidgetProvider
import com.kgurgul.cpuinfo.utils.runOnApiBelow
import timber.log.Timber
import javax.inject.Inject
/**
* Custom ram widget initialization. It will try to create refresh service for ram widget.
* Currently it will work only on API below 26.
*
* @author kgurgul
*/
class RamWidgetInitializer @Inject constructor() : AppInitializer {
override fun init(application: Application) {
runOnApiBelow(Build.VERSION_CODES.O) {
Timber.d("updateRamWidget()")
val intent = Intent(application, RamUsageWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = intArrayOf(R.xml.ram_widget_provider)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
application.sendBroadcast(intent)
}
}
}
@@ -0,0 +1,25 @@
package com.kgurgul.cpuinfo.appinitializers
import android.app.Application
import io.reactivex.exceptions.UndeliverableException
import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber
import javax.inject.Inject
/**
* Ignore [UndeliverableException] and [InterruptedException] exceptions. it can be caused by
* error emitted to the disposed observer or because some blocking code was interrupted by a
* dispose call.
*/
class RxInitializer @Inject constructor() : AppInitializer {
override fun init(application: Application) {
RxJavaPlugins.setErrorHandler { e ->
if (e is UndeliverableException || e is InterruptedException) {
Timber.e(e)
return@setErrorHandler
}
throw RuntimeException(e)
}
}
}
@@ -0,0 +1,22 @@
package com.kgurgul.cpuinfo.appinitializers
import android.app.Application
import android.content.SharedPreferences
import com.kgurgul.cpuinfo.utils.ThemeHelper
import javax.inject.Inject
/**
* Apply theme on app start
*
* @author kgurgul
*/
class ThemeInitializer @Inject constructor(
private val sharedPreferences: SharedPreferences
) : AppInitializer {
override fun init(application: Application) {
sharedPreferences.getString(ThemeHelper.KEY_THEME, ThemeHelper.DEFAULT_MODE)?.let {
ThemeHelper.applyTheme(it)
}
}
}
@@ -0,0 +1,15 @@
package com.kgurgul.cpuinfo.appinitializers
import android.app.Application
import com.kgurgul.cpuinfo.utils.isDebugBuild
import timber.log.Timber
import javax.inject.Inject
class TimberInitializer @Inject constructor() : AppInitializer {
override fun init(application: Application) {
if (isDebugBuild()) {
Timber.plant(Timber.DebugTree())
}
}
}
@@ -1,123 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.common.list
import android.databinding.ListChangeRegistry
import android.databinding.ObservableList
import java.util.*
/**
* Copy from [android.databinding.ObservableArrayList] which makes adapters happy with animations.
* It adds [replace] method.
*
* @author kgurgul
*/
class AdapterArrayList<T> : ArrayList<T>(), ObservableList<T> {
@Transient
private var mListeners: ListChangeRegistry? = null
override fun addOnListChangedCallback(
listener: ObservableList.OnListChangedCallback<out ObservableList<T>>?) {
if (mListeners == null) {
mListeners = ListChangeRegistry()
}
mListeners!!.add(listener)
}
override fun removeOnListChangedCallback(
listener: ObservableList.OnListChangedCallback<out ObservableList<T>>?) {
mListeners?.remove(listener)
}
override fun add(element: T): Boolean {
super.add(element)
notifyAdd(size - 1, 1)
return true
}
override fun add(index: Int, element: T) {
super.add(index, element)
notifyAdd(index, 1)
}
override fun addAll(elements: Collection<T>): Boolean {
val oldSize = size
val added = super.addAll(elements)
if (added) {
notifyAdd(oldSize, size - oldSize)
}
return added
}
override fun addAll(index: Int, elements: Collection<T>): Boolean {
val added = super.addAll(index, elements)
if (added) {
notifyAdd(index, elements.size)
}
return added
}
override fun clear() {
val oldSize = size
super.clear()
if (oldSize != 0) {
notifyRemove(0, oldSize)
}
}
override fun removeAt(index: Int): T {
val value = super.removeAt(index)
notifyRemove(index, 1)
return value
}
override fun remove(element: T): Boolean {
val index = indexOf(element)
return if (index >= 0) {
removeAt(index)
true
} else {
false
}
}
override fun set(index: Int, element: T): T {
val value = super.set(index, element)
mListeners?.notifyChanged(this, index, 1)
return value
}
override fun removeRange(fromIndex: Int, toIndex: Int) {
super.removeRange(fromIndex, toIndex)
notifyRemove(fromIndex, toIndex - fromIndex)
}
fun replace(elements: Collection<T>) {
super.clear()
super.addAll(elements)
mListeners?.notifyChanged(this)
}
private fun notifyAdd(start: Int, count: Int) {
mListeners?.notifyInserted(this, start, count)
}
private fun notifyRemove(start: Int, count: Int) {
mListeners?.notifyRemoved(this, start, count)
}
}
@@ -1,28 +0,0 @@
package com.kgurgul.cpuinfo.common.list
import android.databinding.ObservableList
import android.support.v7.widget.RecyclerView
import java.lang.ref.WeakReference
/**
* To avoid problems with Android LifecycleOwner and LifecycleObserver
* (https://medium.com/@BladeCoder/architecture-components-pitfalls-part-1-9300dd969808)
* we have to notify adapter about onStart and onStop manually.
*
* @author kgurgul
*/
@Suppress("LeakingThis")
abstract class ObservableListAdapter<T, VH : RecyclerView.ViewHolder>(
private val observableList: ObservableList<T>) : RecyclerView.Adapter<VH>() {
private val simpleListChangeNotifier =
SimpleListChangeNotifier<T>(WeakReference(this))
fun registerListChangeNotifier() {
observableList.addOnListChangedCallback(simpleListChangeNotifier)
}
fun unregisterListChangeNotifier() {
observableList.removeOnListChangedCallback(simpleListChangeNotifier)
}
}
@@ -1,49 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.common.list
import android.databinding.ObservableList
import android.support.v7.widget.RecyclerView
import java.lang.ref.WeakReference
/**
* In case of any change on the list, this notifier will notify adapter about specific change
*
* @author kgurgul
*/
class SimpleListChangeNotifier<T>(val adapter: WeakReference<RecyclerView.Adapter<*>>) :
ObservableList.OnListChangedCallback<ObservableList<T>>() {
override fun onChanged(sender: ObservableList<T>) {
adapter.get()?.notifyDataSetChanged()
}
override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int, itemCount: Int) {
adapter.get()?.notifyItemRangeRemoved(positionStart, itemCount)
}
override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int, itemCount: Int) {
adapter.get()?.notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int, itemCount: Int) {
adapter.get()?.notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int, toPosition: Int, itemCount: Int) {
adapter.get()?.notifyDataSetChanged()
}
}
@@ -24,16 +24,10 @@ import javax.inject.Scope
* Dagger scope for Activities
*/
@Scope
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope
/**
* Dagger scope for Fragments
*/
@Scope
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScope
@@ -19,9 +19,9 @@ package com.kgurgul.cpuinfo.di
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import android.support.v4.app.FragmentManager
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.kgurgul.cpuinfo.CpuInfoApp
import com.kgurgul.cpuinfo.di.components.DaggerAppComponent
import dagger.android.AndroidInjection
@@ -37,8 +37,8 @@ import dagger.android.support.HasSupportFragmentInjector
object AppInjector {
fun init(cpuInfoApp: CpuInfoApp) {
DaggerAppComponent.builder().application(cpuInfoApp).build().inject(cpuInfoApp)
cpuInfoApp
.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
cpuInfoApp.registerActivityLifecycleCallbacks(
object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
handleActivity(activity)
}
@@ -16,8 +16,8 @@
package com.kgurgul.cpuinfo.di
import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import javax.inject.Inject
/**
@@ -18,7 +18,7 @@ package com.kgurgul.cpuinfo.di.components
import android.app.Application
import com.kgurgul.cpuinfo.CpuInfoApp
import com.kgurgul.cpuinfo.di.modules.ActivityModule
import com.kgurgul.cpuinfo.di.modules.ActivityBuildersModule
import com.kgurgul.cpuinfo.di.modules.AppModule
import dagger.BindsInstance
import dagger.Component
@@ -31,7 +31,7 @@ import javax.inject.Singleton
* @author kgurgul
*/
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityModule::class])
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBuildersModule::class])
interface AppComponent {
@Component.Builder
@@ -27,7 +27,7 @@ import dagger.android.ContributesAndroidInjector
* @author kgurgul
*/
@Module
abstract class ActivityModule {
abstract class ActivityBuildersModule {
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
@@ -18,15 +18,15 @@ package com.kgurgul.cpuinfo.di.modules
import android.app.ActivityManager
import android.app.Application
import android.app.admin.DevicePolicyManager
import android.content.ContentResolver
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.hardware.SensorManager
import android.support.v7.preference.PreferenceManager
import android.view.WindowManager
import com.google.firebase.analytics.FirebaseAnalytics
import androidx.preference.PreferenceManager
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@@ -36,7 +36,7 @@ import javax.inject.Singleton
*
* @author kgurgul
*/
@Module
@Module(includes = [AppModuleBinds::class])
class AppModule {
@Provides
@@ -49,6 +49,11 @@ class AppModule {
fun provideActivityManager(app: Application): ActivityManager =
app.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
@Provides
@Singleton
fun provideDevicePolicyManager(app: Application): DevicePolicyManager =
app.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
@Provides
@Singleton
fun providePackageManager(app: Application): PackageManager =
@@ -73,9 +78,4 @@ class AppModule {
@Singleton
fun provideSharedPreferences(app: Application): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(app)
@Provides
@Singleton
fun provideFirebaseAnalytics(app: Application): FirebaseAnalytics =
FirebaseAnalytics.getInstance(app)
}
@@ -0,0 +1,26 @@
package com.kgurgul.cpuinfo.di.modules
import com.kgurgul.cpuinfo.appinitializers.*
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
@Module
abstract class AppModuleBinds {
@Binds
@IntoSet
abstract fun provideRxInitializer(bind: RxInitializer): AppInitializer
@Binds
@IntoSet
abstract fun provideTimberInitializer(bind: TimberInitializer): AppInitializer
@Binds
@IntoSet
abstract fun provideRamWidgetInitializer(bind: RamWidgetInitializer): AppInitializer
@Binds
@IntoSet
abstract fun provideThemeInitializer(bind: ThemeInitializer): AppInitializer
}
@@ -30,7 +30,6 @@ import com.kgurgul.cpuinfo.features.information.sensors.SensorsInfoFragment
import com.kgurgul.cpuinfo.features.information.storage.StorageInfoFragment
import com.kgurgul.cpuinfo.features.processes.ProcessesFragment
import com.kgurgul.cpuinfo.features.temperature.TemperatureFragment
import com.kgurgul.cpuinfo.features.temperature.TemperatureModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
@@ -91,6 +90,6 @@ abstract class FragmentBuildersModule {
abstract fun contributeProcessesFragment(): ProcessesFragment
@FragmentScope
@ContributesAndroidInjector(modules = [TemperatureModule::class])
@ContributesAndroidInjector()
abstract fun contributeTemperatureFragment(): TemperatureFragment
}
@@ -17,22 +17,23 @@
package com.kgurgul.cpuinfo.features
import android.annotation.SuppressLint
import android.databinding.DataBindingUtil
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.Fragment
import android.support.v4.view.GravityCompat
import android.support.v7.app.ActionBarDrawerToggle
import android.support.v7.app.AppCompatActivity
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.findNavController
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.databinding.ActivityHostLayoutBinding
import com.kgurgul.cpuinfo.utils.NavigationUtils
import com.kgurgul.cpuinfo.utils.isTablet
import com.kgurgul.cpuinfo.utils.runOnApiAbove
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import timber.log.Timber
import javax.inject.Inject
/**
@@ -42,122 +43,42 @@ import javax.inject.Inject
*/
class HostActivity : AppCompatActivity(), HasSupportFragmentInjector {
companion object {
private const val CURRENT_PAGE_ID_KEY = "CURRENT_PAGE_ID_KEY"
private const val CURRENT_PAGE_NAME_KEY = "CURRENT_PAGE_NAME_KEY"
private const val FRAGMENT_OPEN_DELAY_MILLIS = 500L
}
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var navigationController: NavigationController
private lateinit var navController: NavController
private lateinit var binding: ActivityHostLayoutBinding
private var currentItemId = R.id.hardware
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppThemeBase)
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_host_layout)
setSupportActionBar(binding.toolbar)
binding.navigationView.setNavigationItemSelectedListener { menuItem ->
handleMenuNavigation(menuItem)
navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, destination, _ ->
setToolbarTitleAndElevation(destination.label.toString())
}
runOnApiAbove(Build.VERSION_CODES.M, {
setSupportActionBar(binding.toolbar)
NavigationUtils.setupNavigationView(binding.navigationView, navController)
runOnApiAbove(Build.VERSION_CODES.M) {
// Processes cannot be listed above M
val menu = binding.navigationView.menu
menu.findItem(R.id.processes).isVisible = false
})
val actionBarDrawerToggle = getDrawerToggle()
binding.drawerLayout.addDrawerListener(actionBarDrawerToggle)
actionBarDrawerToggle.syncState()
if (savedInstanceState == null) {
navigationController.navigateToInfo()
} else {
currentItemId = savedInstanceState.getInt(CURRENT_PAGE_ID_KEY)
setToolbarTitleAndElevation(savedInstanceState.getString(CURRENT_PAGE_NAME_KEY))
}
if (!isTablet()) {
val actionBarDrawerToggle = getDrawerToggle()
binding.drawerLayout?.addDrawerListener(actionBarDrawerToggle)
actionBarDrawerToggle.syncState()
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.apply {
putInt(CURRENT_PAGE_ID_KEY, currentItemId)
putString(CURRENT_PAGE_NAME_KEY, binding.toolbar.title.toString())
}
super.onSaveInstanceState(outState)
}
/**
* Handle all navigation callbacks from NavigationDrawer. It also block possibility to open the
* same fragment twice
*/
private fun handleMenuNavigation(menuItem: MenuItem): Boolean {
binding.drawerLayout.closeDrawers()
if (currentItemId == menuItem.itemId) {
return true
}
currentItemId = menuItem.itemId
when (menuItem.itemId) {
R.id.hardware -> {
Handler().postDelayed({
navigationController.navigateToInfo()
setToolbarTitleAndElevation(getString(R.string.hardware))
}, FRAGMENT_OPEN_DELAY_MILLIS)
return true
}
R.id.applications -> {
Handler().postDelayed({
navigationController.navigateToApplications()
setToolbarTitleAndElevation(getString(R.string.applications))
}, FRAGMENT_OPEN_DELAY_MILLIS)
return true
}
R.id.processes -> {
Handler().postDelayed({
navigationController.navigateToProcesses()
setToolbarTitleAndElevation(getString(R.string.processes))
}, FRAGMENT_OPEN_DELAY_MILLIS)
return true
}
R.id.temp -> {
Handler().postDelayed({
navigationController.navigateToTemperature()
setToolbarTitleAndElevation(getString(R.string.temperature))
}, FRAGMENT_OPEN_DELAY_MILLIS)
return true
}
R.id.settings -> {
Handler().postDelayed({
navigationController.navigateToSettings()
setToolbarTitleAndElevation(getString(R.string.settings))
}, FRAGMENT_OPEN_DELAY_MILLIS)
return true
}
else -> {
Timber.e("Bad nav option")
return true
}
}
}
override fun onSupportNavigateUp() = navController.navigateUp()
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawers()
if (binding.drawerLayout?.isDrawerOpen(GravityCompat.START) == true) {
binding.drawerLayout?.closeDrawers()
return
}
super.onBackPressed()
val homeFragment = supportFragmentManager
.findFragmentByTag(NavigationController.FragmentTag.PARENT_FRAGMENT_TAG.tag)
if (homeFragment != null && homeFragment.isVisible) {
currentItemId = R.id.hardware
setToolbarTitleAndElevation(getString(R.string.hardware))
}
}
/**
@@ -166,13 +87,13 @@ class HostActivity : AppCompatActivity(), HasSupportFragmentInjector {
@SuppressLint("NewApi")
private fun setToolbarTitleAndElevation(title: String) {
binding.toolbar.title = title
runOnApiAbove(Build.VERSION_CODES.KITKAT_WATCH, {
if (currentItemId == R.id.hardware) {
runOnApiAbove(Build.VERSION_CODES.KITKAT_WATCH) {
if (navController.currentDestination?.id == R.id.hardware) {
binding.toolbar.elevation = 0f
} else {
binding.toolbar.elevation = resources.getDimension(R.dimen.elevation_height)
}
})
}
}
/**
@@ -183,6 +104,5 @@ class HostActivity : AppCompatActivity(), HasSupportFragmentInjector {
R.string.open_drawer, R.string.close_drawer)
}
override fun supportFragmentInjector(): AndroidInjector<Fragment>
= dispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> = dispatchingAndroidInjector
}
@@ -1,111 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.features
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentTransaction
import com.google.firebase.analytics.FirebaseAnalytics
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.analytics.AnalyticsEvents
import com.kgurgul.cpuinfo.features.applications.ApplicationsFragment
import com.kgurgul.cpuinfo.features.information.InfoContainerFragment
import com.kgurgul.cpuinfo.features.processes.ProcessesFragment
import com.kgurgul.cpuinfo.features.settings.SettingsFragment
import com.kgurgul.cpuinfo.features.temperature.TemperatureFragment
import javax.inject.Inject
/**
* NavigationController controls whole navigation in [HostActivity]
*
* @author kgurgul
*/
class NavigationController @Inject constructor(private val hostActivity: HostActivity,
private val firebaseAnalytics: FirebaseAnalytics) {
enum class FragmentTag(val tag: String) {
PARENT_FRAGMENT_TAG("PARENT_FRAGMENT_TAG"),
SINGLE_CHILD_FRAGMENT_TAG("SINGLE_CHILD_FRAGMENT_TAG")
}
private val fragmentManager = hostActivity.supportFragmentManager
private val containerId = R.id.fragment_container
fun navigateToInfo() {
firebaseAnalytics.setCurrentScreen(hostActivity, AnalyticsEvents.INFO_SCREEN, null)
val infoFragment = InfoContainerFragment()
addToSingleChildStack(infoFragment, FragmentTag.PARENT_FRAGMENT_TAG)
}
fun navigateToTemperature() {
firebaseAnalytics.setCurrentScreen(hostActivity, AnalyticsEvents.TEMPERATURE_SCREEN, null)
val temperatureFragment = TemperatureFragment()
addToSingleChildStack(temperatureFragment, FragmentTag.SINGLE_CHILD_FRAGMENT_TAG)
}
fun navigateToApplications() {
firebaseAnalytics.setCurrentScreen(hostActivity, AnalyticsEvents.APPLICATIONS_SCREEN, null)
val applicationsFragment = ApplicationsFragment()
addToSingleChildStack(applicationsFragment, FragmentTag.SINGLE_CHILD_FRAGMENT_TAG)
}
fun navigateToProcesses() {
firebaseAnalytics.setCurrentScreen(hostActivity, AnalyticsEvents.PROCESSES_SCREEN, null)
val processesFragment = ProcessesFragment()
addToSingleChildStack(processesFragment, FragmentTag.SINGLE_CHILD_FRAGMENT_TAG)
}
fun navigateToSettings() {
firebaseAnalytics.setCurrentScreen(hostActivity, AnalyticsEvents.SETTINGS_SCREEN, null)
val settingsFragment = SettingsFragment()
addToSingleChildStack(settingsFragment, FragmentTag.SINGLE_CHILD_FRAGMENT_TAG)
}
/**
* App navigation will currently build stack with [InfoContainerFragment] on the top and all
* others fragments as a single child.
*/
private fun addToSingleChildStack(fragment: Fragment, fragmentTag: FragmentTag) {
if (fragmentTag == FragmentTag.PARENT_FRAGMENT_TAG) {
if (fragmentManager.findFragmentByTag(FragmentTag.PARENT_FRAGMENT_TAG.tag) != null) {
val currentFragment =
fragmentManager.findFragmentByTag(FragmentTag.SINGLE_CHILD_FRAGMENT_TAG.tag)
if (currentFragment != null) {
fragmentManager.beginTransaction()
.remove(currentFragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commitNow()
}
fragmentManager.popBackStackImmediate()
} else {
fragmentManager.beginTransaction()
.replace(containerId, fragment, fragmentTag.tag)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commitAllowingStateLoss()
}
} else {
val fragmentTransaction = fragmentManager.beginTransaction()
val hasOnlyParentFragment = fragmentManager.findFragmentByTag(
FragmentTag.SINGLE_CHILD_FRAGMENT_TAG.tag) == null
fragmentTransaction.replace(containerId, fragment, fragmentTag.tag)
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
if (hasOnlyParentFragment) {
fragmentTransaction.addToBackStack(null)
}
fragmentTransaction.commitAllowingStateLoss()
}
}
}
@@ -17,26 +17,19 @@
package com.kgurgul.cpuinfo.features.applications
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.databinding.ObservableList
import android.os.Build
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.ObservableListAdapter
import com.kgurgul.cpuinfo.utils.Utils
import com.kgurgul.cpuinfo.utils.glide.GlideApp
import com.kgurgul.cpuinfo.utils.runOnApiBelow
import com.kgurgul.cpuinfo.widgets.swiperv.SwipeHorizontalMenuLayout
import java.io.File
/**
@@ -44,15 +37,10 @@ import java.io.File
*
* @author kgurgul
*/
class ApplicationsAdapter(private val context: Context,
private val appList: ObservableList<ExtendedAppInfo>,
private val appClickListener: ItemClickListener)
: ObservableListAdapter<ExtendedAppInfo,
ApplicationsAdapter.ApplicationViewHolder>(appList) {
private val packageManager: PackageManager = context.packageManager
private val storageLabel: String = context.getString(R.string.storage_used)
private val calculatingLabel: String = context.getString(R.string.calculating)
class ApplicationsAdapter(
private val appList: List<ExtendedAppInfo>,
private val appClickListener: ItemClickListener
) : RecyclerView.Adapter<ApplicationsAdapter.ApplicationViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ApplicationViewHolder {
val view = LayoutInflater.from(parent.context)
@@ -60,93 +48,75 @@ class ApplicationsAdapter(private val context: Context,
return ApplicationViewHolder(view)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ApplicationViewHolder, position: Int) {
val app = appList[position]
GlideApp.with(context)
.load(getApplicationInfo(app.sourceDir))
.transition(DrawableTransitionOptions.withCrossFade())
.fitCenter()
.into(holder.iconIv)
holder.nameTv.text = app.name
holder.packageTv.text = app.packageName
runOnApiBelow(Build.VERSION_CODES.O, {
val size =
if (app.appSize == 0L) calculatingLabel
else Utils.humanReadableByteCount(app.appSize)
holder.storageTv.text = "$storageLabel $size"
}, {
holder.storageTv.visibility = View.GONE
})
holder.mainContainer.setOnClickListener {
val pos = holder.adapterPosition
if (pos != RecyclerView.NO_POSITION) {
appClickListener.appOpenClicked(pos)
}
}
holder.settingsV.setOnClickListener {
val pos = holder.adapterPosition
if (pos != RecyclerView.NO_POSITION) {
appClickListener.appSettingsClicked(holder.adapterPosition)
}
}
holder.deleteView.setOnClickListener {
val pos = holder.adapterPosition
if (pos != RecyclerView.NO_POSITION) {
holder.sml.smoothCloseMenu()
appClickListener.appUninstallClicked(holder.adapterPosition)
}
}
// Native libs
if (app.nativeLibraryDir != null) {
val fileDir = File(app.nativeLibraryDir)
val list = fileDir.listFiles()
if (list != null && list.isNotEmpty()) {
holder.nativeButtonIV.visibility = View.VISIBLE
holder.nativeButtonIV.setOnClickListener {
appClickListener.appNativeLibsClicked(fileDir)
}
} else {
holder.nativeButtonIV.visibility = View.GONE
}
} else {
holder.nativeButtonIV.visibility = View.GONE
}
holder.bind(app, appClickListener)
}
override fun getItemCount(): Int =
appList.size
override fun getItemCount(): Int = appList.size
class ApplicationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val iconIv: ImageView = itemView.findViewById(R.id.app_icon)
val nameTv: TextView = itemView.findViewById(R.id.app_name)
val packageTv: TextView = itemView.findViewById(R.id.app_package)
val storageTv: TextView = itemView.findViewById(R.id.storage_usage)
val sml: SwipeHorizontalMenuLayout = itemView.findViewById(R.id.sml)
val mainContainer: View = itemView.findViewById(R.id.smContentView)
val settingsV: View = itemView.findViewById(R.id.settings)
val deleteView: View = itemView.findViewById(R.id.delete)
val nativeButtonIV: ImageView = itemView.findViewById(R.id.native_button)
}
private val storageLabel: String = itemView.context.getString(R.string.storage_used)
private val calculatingLabel: String = itemView.context.getString(R.string.calculating)
/**
* Generate [ApplicationInfo] from [PackageInfo]
*/
private fun getApplicationInfo(path: String): ApplicationInfo? {
val info = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES)
var appInfo: ApplicationInfo? = null
if (info != null) {
appInfo = info.applicationInfo
appInfo!!.sourceDir = path
appInfo.publicSourceDir = path
private val iconIv: ImageView = itemView.findViewById(R.id.app_icon)
private val nameTv: TextView = itemView.findViewById(R.id.app_name)
private val packageTv: TextView = itemView.findViewById(R.id.app_package)
private val storageTv: TextView = itemView.findViewById(R.id.storage_usage)
private val sml: SwipeHorizontalMenuLayout = itemView.findViewById(R.id.sml)
private val mainContainer: View = itemView.findViewById(R.id.smContentView)
private val settingsV: View = itemView.findViewById(R.id.settings)
private val deleteView: View = itemView.findViewById(R.id.delete)
private val nativeButtonIV: ImageView = itemView.findViewById(R.id.native_button)
@SuppressLint("SetTextI18n")
fun bind(app: ExtendedAppInfo, appClickListener: ItemClickListener) {
GlideApp.with(iconIv.context)
.load(app.appIconUri)
.transition(DrawableTransitionOptions.withCrossFade())
.fitCenter()
.into(iconIv)
nameTv.text = app.name
packageTv.text = app.packageName
runOnApiBelow(Build.VERSION_CODES.O, {
val size =
if (app.appSize == 0L) calculatingLabel
else Utils.humanReadableByteCount(app.appSize)
storageTv.text = "$storageLabel $size"
}, {
storageTv.visibility = View.GONE
})
mainContainer.setOnClickListener {
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION) {
appClickListener.appOpenClicked(pos)
}
}
settingsV.setOnClickListener {
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION) {
appClickListener.appSettingsClicked(adapterPosition)
}
}
deleteView.setOnClickListener {
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION) {
sml.smoothCloseMenu()
appClickListener.appUninstallClicked(adapterPosition)
}
}
nativeButtonIV.setOnClickListener {
app.nativeLibraryDir?.let { appClickListener.appNativeLibsClicked(it) }
}
if (app.hasNativeLibs) {
nativeButtonIV.visibility = View.VISIBLE
} else {
nativeButtonIV.visibility = View.GONE
}
}
return appInfo
}
/**
@@ -156,6 +126,6 @@ class ApplicationsAdapter(private val context: Context,
fun appOpenClicked(position: Int)
fun appSettingsClicked(position: Int)
fun appUninstallClicked(position: Int)
fun appNativeLibsClicked(nativeFile: File)
fun appNativeLibsClicked(nativeDir: String)
}
}
@@ -16,32 +16,31 @@
package com.kgurgul.cpuinfo.features.applications
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProvider
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.databinding.DataBindingUtil
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.support.design.widget.Snackbar
import android.support.v4.app.Fragment
import android.support.v7.app.AlertDialog
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SimpleItemAnimator
import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import com.kgurgul.cpuinfo.databinding.FragmentApplicationsBinding
import com.kgurgul.cpuinfo.di.Injectable
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.utils.AutoClearedValue
import com.kgurgul.cpuinfo.utils.DividerItemDecoration
import com.kgurgul.cpuinfo.utils.Utils
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import com.kgurgul.cpuinfo.utils.viewModelProvider
import com.kgurgul.cpuinfo.utils.wrappers.EventObserver
import com.kgurgul.cpuinfo.widgets.swiperv.SwipeMenuRecyclerView
import java.io.File
import javax.inject.Inject
@@ -57,8 +56,8 @@ class ApplicationsFragment : Fragment(), Injectable, ApplicationsAdapter.ItemCli
lateinit var viewModelInjectionFactory: ViewModelInjectionFactory<ApplicationsViewModel>
private lateinit var viewModel: ApplicationsViewModel
private lateinit var binding: AutoClearedValue<FragmentApplicationsBinding>
private lateinit var applicationsAdapter: AutoClearedValue<ApplicationsAdapter>
private lateinit var binding: FragmentApplicationsBinding
private lateinit var applicationsAdapter: ApplicationsAdapter
private val uninstallReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -69,53 +68,36 @@ class ApplicationsFragment : Fragment(), Injectable, ApplicationsAdapter.ItemCli
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(ApplicationsViewModel::class.java)
viewModel = viewModelProvider(viewModelInjectionFactory)
viewModel.refreshApplicationsList()
registerUninstallBroadcast()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = AutoClearedValue(this,
DataBindingUtil.inflate(inflater, R.layout.fragment_applications, container, false))
binding.get().viewModel = viewModel
binding.get().swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent,
R.color.colorPrimaryDark)
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_applications, container,
false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
binding.swipeRefreshLayout.setColorSchemeResources(R.color.accent,
R.color.primaryDark)
initObservables()
setupRecyclerView()
return binding.get().root
}
override fun onStart() {
super.onStart()
applicationsAdapter.get().registerListChangeNotifier()
}
override fun onStop() {
applicationsAdapter.get().unregisterListChangeNotifier()
super.onStop()
}
override fun onDestroyView() {
// Fix for buggy SwipeMenuRecyclerView which can leak. More investigation needed.
binding.get().recyclerView.adapter = null
super.onDestroyView()
return binding.root
}
/**
* Setup for [SwipeMenuRecyclerView]
*/
private fun setupRecyclerView() {
applicationsAdapter = AutoClearedValue(this,
ApplicationsAdapter(requireContext(), viewModel.applicationList, this))
applicationsAdapter = ApplicationsAdapter(viewModel.applicationList, this)
viewModel.applicationList.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(applicationsAdapter))
val rvLayoutManager = LinearLayoutManager(context)
binding.get().apply {
recyclerView.layoutManager = rvLayoutManager
recyclerView.adapter = applicationsAdapter.get()
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
(recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
binding.recyclerView.apply {
layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
adapter = applicationsAdapter
addItemDecoration(DividerItemDecoration(requireContext()))
}
}
@@ -123,7 +105,7 @@ class ApplicationsFragment : Fragment(), Injectable, ApplicationsAdapter.ItemCli
* Register all fields from [ApplicationsViewModel] which should be observed
*/
private fun initObservables() {
viewModel.shouldStartStorageService.observe(this, Observer {
viewModel.shouldStartStorageServiceEvent.observe(viewLifecycleOwner, EventObserver {
StorageUsageService.startService(requireContext(), viewModel.applicationList)
})
}
@@ -159,7 +141,7 @@ class ApplicationsFragment : Fragment(), Injectable, ApplicationsAdapter.ItemCli
val appInfo = viewModel.applicationList[position]
// Block self opening
if (appInfo.packageName == requireContext().packageName) {
Snackbar.make(binding.get().mainContainer, getString(R.string.cpu_open),
Snackbar.make(binding.mainContainer, getString(R.string.cpu_open),
Snackbar.LENGTH_SHORT).show()
return
}
@@ -169,11 +151,11 @@ class ApplicationsFragment : Fragment(), Injectable, ApplicationsAdapter.ItemCli
try {
startActivity(intent)
} catch (e: Exception) {
Snackbar.make(binding.get().mainContainer, getString(R.string.app_open),
Snackbar.make(binding.mainContainer, getString(R.string.app_open),
Snackbar.LENGTH_SHORT).show()
}
} else {
Snackbar.make(binding.get().mainContainer, getString(R.string.app_open),
Snackbar.make(binding.mainContainer, getString(R.string.app_open),
Snackbar.LENGTH_SHORT).show()
}
}
@@ -194,7 +176,7 @@ class ApplicationsFragment : Fragment(), Injectable, ApplicationsAdapter.ItemCli
override fun appUninstallClicked(position: Int) {
val appInfo = viewModel.applicationList[position]
if (appInfo.packageName == requireContext().packageName) {
Snackbar.make(binding.get().mainContainer, getString(R.string.cpu_uninstall),
Snackbar.make(binding.mainContainer, getString(R.string.cpu_uninstall),
Snackbar.LENGTH_SHORT).show()
return
}
@@ -207,21 +189,23 @@ class ApplicationsFragment : Fragment(), Injectable, ApplicationsAdapter.ItemCli
/**
* Open dialog with native lib list and open google if user taps on it
*/
override fun appNativeLibsClicked(nativeFile: File) {
showNativeListDialog(nativeFile)
override fun appNativeLibsClicked(nativeDir: String) {
showNativeListDialog(nativeDir)
}
/**
* Create dialog with native libraries list
*/
private fun showNativeListDialog(nativeLibsDir: File) {
val builder = AlertDialog.Builder(requireContext(), R.style.CustomAppCompatAlertDialogStyle)
@SuppressLint("InflateParams")
private fun showNativeListDialog(nativeLibsDir: String) {
val builder = MaterialAlertDialogBuilder(requireContext())
val inflater = LayoutInflater.from(context)
val dialogLayout = inflater.inflate(R.layout.dialog_native_libs, null)
val libs = nativeLibsDir.listFiles().map { it.name }
val nativeDirFile = File(nativeLibsDir)
val libs = nativeDirFile.listFiles().map { it.name }
val listView: ListView = dialogLayout.findViewById(R.id.dialog_lv)
val arrayAdapter = ArrayAdapter<String>(context, R.layout.item_native_libs,
val arrayAdapter = ArrayAdapter<String>(requireContext(), R.layout.item_native_libs,
R.id.native_name_tv, libs)
listView.adapter = arrayAdapter
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
@@ -16,27 +16,28 @@
package com.kgurgul.cpuinfo.features.applications
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.databinding.ObservableBoolean
import android.net.Uri
import android.os.Build
import android.support.annotation.VisibleForTesting
import com.kgurgul.cpuinfo.common.Prefs
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import com.kgurgul.cpuinfo.utils.SingleLiveEvent
import com.kgurgul.cpuinfo.utils.runOnApiBelow
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.kgurgul.cpuinfo.utils.wrappers.Event
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.jetbrains.anko.coroutines.experimental.asReference
import org.jetbrains.anko.coroutines.experimental.bg
import timber.log.Timber
import java.io.File
import javax.inject.Inject
/**
@@ -45,18 +46,21 @@ import javax.inject.Inject
* @author kgurgul
*/
class ApplicationsViewModel @Inject constructor(
private val dispatchersProvider: DispatchersProvider,
private val prefs: Prefs,
private val packageManager: PackageManager) : ViewModel() {
private val packageManager: PackageManager) : ScopedViewModel(dispatchersProvider) {
companion object {
private const val SORTING_APPS_KEY = "SORTING_APPS_KEY"
}
val isLoading = ObservableBoolean(false)
val applicationList = AdapterArrayList<ExtendedAppInfo>()
val shouldStartStorageService = SingleLiveEvent<Void>()
val isLoading = NonNullMutableLiveData(false)
val applicationList = ListLiveData<ExtendedAppInfo>()
private val _shouldStartStorageServiceEvent = MutableLiveData<Event<Unit>>()
val shouldStartStorageServiceEvent: LiveData<Event<Unit>>
get() = _shouldStartStorageServiceEvent
private val ref = asReference()
private var isSortingAsc = prefs.get(SORTING_APPS_KEY, true)
private var refreshingDisposable: Disposable? = null
@@ -66,17 +70,17 @@ class ApplicationsViewModel @Inject constructor(
@Synchronized
fun refreshApplicationsList() {
if (refreshingDisposable == null || !isLoading.get()) {
if (refreshingDisposable == null || !isLoading.value) {
refreshingDisposable = getApplicationsListSingle()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe({ _ -> isLoading.set(true) })
.doFinally({ isLoading.set(false) })
.doOnSubscribe { isLoading.value = true }
.doFinally { isLoading.value = false }
.subscribe({ appList ->
applicationList.replace(appList)
runOnApiBelow(Build.VERSION_CODES.O, {
shouldStartStorageService.call()
})
runOnApiBelow(Build.VERSION_CODES.O) {
_shouldStartStorageServiceEvent.value = Event(Unit)
}
}, Timber::e)
}
}
@@ -86,36 +90,37 @@ class ApplicationsViewModel @Inject constructor(
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
internal fun getApplicationsListSingle(): Single<List<ExtendedAppInfo>> {
return Single.fromCallable({
val extendedAppList = ArrayList<ExtendedAppInfo>()
var appsList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
return Single.fromCallable {
val appsList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
.filter { (it.flags and ApplicationInfo.FLAG_SYSTEM) == 0 }
appsList = if (isSortingAsc) {
appsList.sortedBy {
it.loadLabel(packageManager).toString().toUpperCase()
}
} else {
appsList.sortedByDescending {
it.loadLabel(packageManager).toString().toUpperCase()
}
}
appsList.map {
extendedAppList.add(
.map {
val hasNativeLibs = if (it.nativeLibraryDir != null) {
val fileDir = File(it.nativeLibraryDir)
val list = fileDir.listFiles()
list != null && list.isNotEmpty()
} else false
ExtendedAppInfo(it.loadLabel(packageManager).toString(), it.packageName,
it.sourceDir, it.nativeLibraryDir))
it.sourceDir, it.nativeLibraryDir, hasNativeLibs,
getAppIconUri(it.packageName))
}
return@fromCallable if (isSortingAsc) {
appsList.sortedBy { it.name.toUpperCase() }
} else {
appsList.sortedByDescending { it.name.toUpperCase() }
}
extendedAppList
})
}
}
/**
* Change apps list sorting type from ascending to descending or or vice versa
*/
fun changeAppsSorting() {
async(UI) {
val result = bg { getAppSortedList(!isSortingAsc) }
val sortedAppList = result.await()
ref().applicationList.replace(sortedAppList)
launch {
val sortedAppList = withContext(dispatchersProvider.ioDispatcher) {
getAppSortedList(!isSortingAsc)
}
applicationList.replace(sortedAppList)
}
}
@@ -126,27 +131,27 @@ class ApplicationsViewModel @Inject constructor(
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
internal fun getAppSortedList(sortingAsc: Boolean): List<ExtendedAppInfo> {
val appListCopy = ArrayList<ExtendedAppInfo>(applicationList)
isSortingAsc = sortingAsc
prefs.insert(SORTING_APPS_KEY, sortingAsc)
if (sortingAsc) {
appListCopy.sortBy { it.name.toUpperCase() }
return if (sortingAsc) {
applicationList.sortedBy { it.name.toUpperCase() }
} else {
appListCopy.sortByDescending { it.name.toUpperCase() }
applicationList.sortedByDescending { it.name.toUpperCase() }
}
return appListCopy
}
/**
* Update package size whit specific package name using coroutine
*/
@Suppress("unused")
@Subscribe
fun onUpdatePackageSizeEvent(event: StorageUsageService.UpdatePackageSizeEvent) {
async(UI) {
val result = bg { getUpdatedApp(event) }
val newAppPair = result.await()
launch {
val newAppPair = withContext(dispatchersProvider.ioDispatcher) {
getUpdatedApp(event)
}
if (newAppPair.first != -1) {
ref().applicationList[newAppPair.first] = newAppPair.second!!
applicationList[newAppPair.first] = newAppPair.second!!
}
}
}
@@ -165,6 +170,24 @@ class ApplicationsViewModel @Inject constructor(
return Pair(-1, null)
}
private fun getAppIconUri(packageName: String): Uri {
return Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(packageName)
.path(getResourceId(packageName).toString())
.build()
}
private fun getResourceId(packageName: String): Int {
val packageInfo: PackageInfo
try {
packageInfo = packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
return 0
}
return packageInfo.applicationInfo.icon
}
override fun onCleared() {
super.onCleared()
refreshingDisposable?.dispose()
@@ -16,7 +16,7 @@
package com.kgurgul.cpuinfo.features.applications
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@@ -25,10 +25,12 @@ import kotlinx.android.parcel.Parcelize
*
* @author kgurgul
*/
@SuppressLint("ParcelCreator")
@Parcelize
data class ExtendedAppInfo(val name: String,
val packageName: String,
val sourceDir: String,
val nativeLibraryDir: String?,
var appSize: Long = 0) : Parcelable
data class ExtendedAppInfo(
val name: String,
val packageName: String,
val sourceDir: String,
val nativeLibraryDir: String?,
val hasNativeLibs: Boolean,
val appIconUri: Uri,
var appSize: Long = 0) : Parcelable
@@ -49,9 +49,8 @@ class StorageUsageService : IntentService("StorageUsageService") {
}
override fun onHandleIntent(intent: Intent) {
val packagesList = intent.extras.getParcelableArrayList<ExtendedAppInfo>(PACKAGES_LIST_TAG)
packagesList.forEach {
val packagesList = intent.extras?.getParcelableArrayList<ExtendedAppInfo>(PACKAGES_LIST_TAG)
packagesList?.forEach {
getPackageSize(it.packageName, packageManager)
}
}
@@ -0,0 +1,115 @@
package com.kgurgul.cpuinfo.features.cputile
import android.os.Build
import android.os.Handler
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import android.graphics.drawable.Icon
import com.kgurgul.cpuinfo.R
import java.io.RandomAccessFile
@RequiresApi(api = Build.VERSION_CODES.N)
class CPUTile : TileService() {
enum class CPULoad {
Low,
Medium,
High
}
private val handler = Handler()
private var runnable = Runnable {}
private val minMaxAvg : Pair<Long, Long> by lazy {
val cpuCount = Runtime.getRuntime().availableProcessors()
val minFreq = mutableListOf<Long>()
val maxFreq = mutableListOf<Long>()
for (i in 0 until cpuCount) {
val minMax = tryToGetMinMaxFreq(i)
minFreq.add(minMax.first)
maxFreq.add(minMax.second)
}
Pair(minFreq.sum() / cpuCount, maxFreq.sum() / cpuCount)
}
private val icons by lazy { mapOf<CPULoad, Icon>(
CPULoad.Low to Icon.createWithResource(this, R.drawable.ic_cpu_low),
CPULoad.Medium to Icon.createWithResource(this, R.drawable.ic_cpu_med),
CPULoad.High to Icon.createWithResource(this, R.drawable.ic_cpu_high)
)}
private val defaultIcon by lazy { Icon.createWithResource(this, R.drawable.ic_cpu_high) }
override fun onTileAdded() {
super.onTileAdded()
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
override fun onStartListening() {
super.onStartListening()
runnable = Runnable {
val load = averageCPUFreq()
qsTile.label = "Avg ${load}MHz"
qsTile.icon = getLoadIcon(load)
qsTile.updateTile()
handler.postDelayed(runnable, 1000)
}
handler.postDelayed (runnable, 1)
}
override fun onStopListening() {
super.onStopListening()
qsTile.updateTile()
handler.removeCallbacks(runnable)
}
private fun getLoadIcon(avgLoad: Long) : Icon {
val freqDiff = minMaxAvg.second - minMaxAvg.first
val freqThirds = freqDiff / 3
val loadEnum = when {
avgLoad >= minMaxAvg.second - freqThirds -> CPULoad.High
minMaxAvg.second - freqThirds > avgLoad && avgLoad >= minMaxAvg.first + freqThirds -> CPULoad.Medium
else -> CPULoad.Low
}
return icons.getOrDefault(loadEnum, defaultIcon)
}
private fun averageCPUFreq() : Long {
val cpuCount = Runtime.getRuntime().availableProcessors()
var sumFreq = 0L
for (i in 0 until cpuCount) {
sumFreq += tryToGetCurrentFreq(i)
}
return (sumFreq / cpuCount)
}
private fun tryToGetCurrentFreq(coreNumber: Int): Long {
val filePath = "/sys/devices/system/cpu/cpu$coreNumber/cpufreq/scaling_cur_freq"
try {
val reader = RandomAccessFile(filePath, "r")
val value = reader.readLine().toLong() / 1000
reader.close()
return value
} catch (ignored: Exception) {
}
return 0
}
private fun tryToGetMinMaxFreq(coreNumber: Int): Pair<Long, Long> {
val minPath = "/sys/devices/system/cpu/cpu$coreNumber/cpufreq/cpuinfo_min_freq"
val maxPath = "/sys/devices/system/cpu/cpu$coreNumber/cpufreq/cpuinfo_max_freq"
try {
var reader = RandomAccessFile(minPath, "r")
val minMhz = reader.readLine().toLong() / 1000
reader.close()
reader = RandomAccessFile(maxPath, "r")
val maxMhz = reader.readLine().toLong() / 1000
reader.close()
return Pair(minMhz, maxMhz)
} catch (e: Exception) {
}
return Pair(0, 0)
}
}
@@ -16,17 +16,16 @@
package com.kgurgul.cpuinfo.features.information
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.databinding.FragmentInfoBinding
import com.kgurgul.cpuinfo.di.Injectable
import com.kgurgul.cpuinfo.features.information.base.ViewPagerAdapter
import com.kgurgul.cpuinfo.utils.AutoClearedValue
/**
* Fragment which is base for all hardware and software information fragments
@@ -35,19 +34,18 @@ import com.kgurgul.cpuinfo.utils.AutoClearedValue
*/
class InfoContainerFragment : Fragment(), Injectable {
private lateinit var binding: AutoClearedValue<FragmentInfoBinding>
private lateinit var binding: FragmentInfoBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = AutoClearedValue(this,
DataBindingUtil.inflate(inflater, R.layout.fragment_info, container, false))
return binding.get().root
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_info, container, false)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val viewPagerAdapter = ViewPagerAdapter(requireContext(), childFragmentManager)
binding.get().viewPager.adapter = viewPagerAdapter
binding.get().tabs.setupWithViewPager(binding.get().viewPager)
binding.viewPager.adapter = viewPagerAdapter
binding.tabs.setupWithViewPager(binding.viewPager)
}
}
@@ -16,12 +16,18 @@
package com.kgurgul.cpuinfo.features.information.android
import android.arch.lifecycle.ViewModelProvider
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.features.information.base.InfoItemsAdapter
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import javax.inject.Inject
/**
@@ -39,24 +45,50 @@ class AndroidInfoFragment : BaseRvFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(AndroidInfoViewModel::class.java)
infoItemsAdapter = InfoItemsAdapter(requireContext(), viewModel.dataObservableList,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT)
setHasOptionsMenu(true)
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onStart() {
super.onStart()
infoItemsAdapter.registerListChangeNotifier()
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
runOnApiAbove(18) {
inflater.inflate(R.menu.info_menu, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onStop() {
infoItemsAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_export_to_csv -> {
createSafFile(MIME_TEXT_PLAIN, DUMP_FILENAME, RC_CREATE_FILE)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun setupRecyclerViewAdapter() {
infoItemsAdapter = InfoItemsAdapter(viewModel.listLiveData,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT, onClickListener = this)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(infoItemsAdapter))
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
recyclerView.adapter = infoItemsAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_CREATE_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
viewModel.saveListToFile(uri)
}
}
}
}
}
companion object {
private const val RC_CREATE_FILE = 100
private const val DUMP_FILENAME = "android_info.txt"
}
}
@@ -17,18 +17,24 @@
package com.kgurgul.cpuinfo.features.information.android
import android.annotation.SuppressLint
import android.arch.lifecycle.ViewModel
import android.app.admin.DevicePolicyManager
import android.content.ContentResolver
import android.content.res.Resources
import android.net.Uri
import android.os.Build
import android.provider.Settings
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.opencsv.CSVWriter
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.BufferedReader
import java.io.File
import java.io.FileWriter
import java.io.InputStreamReader
import java.security.Security
import java.util.*
import javax.inject.Inject
/**
@@ -39,24 +45,47 @@ import javax.inject.Inject
*/
class AndroidInfoViewModel @Inject constructor(
private val resources: Resources,
private val contentResolver: ContentResolver) : ViewModel() {
private val contentResolver: ContentResolver,
private val devicePolicyManager: DevicePolicyManager,
private val dispatchersProvider: DispatchersProvider
) : ScopedViewModel(dispatchersProvider) {
val dataObservableList = AdapterArrayList<Pair<String, String>>()
val listLiveData = ListLiveData<Pair<String, String>>()
init {
getData()
}
/**
* Invoked when user wants to export whole list to the CSV file
*/
fun saveListToFile(uri: Uri) {
launch(context = dispatchersProvider.ioDispatcher) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
CSVWriter(FileWriter(it.fileDescriptor)).use { csvWriter ->
listLiveData.forEach { pair ->
csvWriter.writeNext(pair.toList().toTypedArray())
}
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
/**
* Get all data connected with Android OS
*/
private fun getData() {
if (dataObservableList.isNotEmpty()) {
if (listLiveData.isNotEmpty()) {
return
}
getBuildData()
getAndroidIdData()
getRootData()
getDeviceEncryptionStatus()
getSecurityData()
}
@@ -64,18 +93,18 @@ class AndroidInfoViewModel @Inject constructor(
* Retrieve data from static Build class and system property "java.vm.version"
*/
private fun getBuildData() {
dataObservableList.add(Pair(resources.getString(R.string.version), Build.VERSION.RELEASE))
dataObservableList.add(Pair("SDK", Build.VERSION.SDK_INT.toString()))
dataObservableList.add(Pair(resources.getString(R.string.codename), Build.VERSION.CODENAME))
dataObservableList.add(Pair("Bootloader", Build.BOOTLOADER))
dataObservableList.add(Pair(resources.getString(R.string.brand), Build.BRAND))
dataObservableList.add(Pair(resources.getString(R.string.model), Build.MODEL))
dataObservableList.add(Pair(resources.getString(R.string.manufacturer), Build.MANUFACTURER))
dataObservableList.add(Pair(resources.getString(R.string.board), Build.BOARD))
dataObservableList.add(Pair("VM", getVmVersion()))
dataObservableList.add(Pair("Kernel", System.getProperty("os.version") ?: ""))
listLiveData.add(Pair(resources.getString(R.string.version), Build.VERSION.RELEASE))
listLiveData.add(Pair("SDK", Build.VERSION.SDK_INT.toString()))
listLiveData.add(Pair(resources.getString(R.string.codename), Build.VERSION.CODENAME))
listLiveData.add(Pair("Bootloader", Build.BOOTLOADER))
listLiveData.add(Pair(resources.getString(R.string.brand), Build.BRAND))
listLiveData.add(Pair(resources.getString(R.string.model), Build.MODEL))
listLiveData.add(Pair(resources.getString(R.string.manufacturer), Build.MANUFACTURER))
listLiveData.add(Pair(resources.getString(R.string.board), Build.BOARD))
listLiveData.add(Pair("VM", getVmVersion()))
listLiveData.add(Pair("Kernel", System.getProperty("os.version") ?: ""))
@Suppress("DEPRECATION")
dataObservableList.add(Pair(resources.getString(R.string.serial), Build.SERIAL))
listLiveData.add(Pair(resources.getString(R.string.serial), Build.SERIAL))
}
/**
@@ -85,7 +114,7 @@ class AndroidInfoViewModel @Inject constructor(
private fun getAndroidIdData() {
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
if (androidId != null) {
dataObservableList.add(Pair("Android ID", androidId))
listLiveData.add(Pair("Android ID", androidId))
}
}
@@ -95,7 +124,27 @@ class AndroidInfoViewModel @Inject constructor(
private fun getRootData() {
val isRootedStr = if (isDeviceRooted()) resources.getString(R.string.yes) else
resources.getString(R.string.no)
dataObservableList.add(Pair(resources.getString(R.string.rooted), isRootedStr))
listLiveData.add(Pair(resources.getString(R.string.rooted), isRootedStr))
}
/**
* Add information about device encrypted storage status
*/
private fun getDeviceEncryptionStatus() {
try {
val status = devicePolicyManager.storageEncryptionStatus
val statusText = when (status) {
DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED -> ENCRYPTION_STATUS_UNSUPPORTED
DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE -> ENCRYPTION_STATUS_INACTIVE
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVATING -> ENCRYPTION_STATUS_ACTIVATING
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE -> ENCRYPTION_STATUS_ACTIVE
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER ->
ENCRYPTION_STATUS_ACTIVE_PER_USER
else -> resources.getString(R.string.unknown)
}
listLiveData.add(Pair(resources.getString(R.string.encrypted_storage), statusText))
} catch (ignored: Exception) {
}
}
/**
@@ -148,21 +197,18 @@ class AndroidInfoViewModel @Inject constructor(
* Get information about security providers
*/
private fun getSecurityData() {
val securityProviders = getSecurityProviders()
val securityProviders = Security.getProviders().map { Pair(it.name, it.version.toString()) }
if (!securityProviders.isEmpty()) {
dataObservableList.add(Pair(resources.getString(R.string.security_providers), ""))
dataObservableList.addAll(securityProviders)
listLiveData.add(Pair(resources.getString(R.string.security_providers), ""))
listLiveData.addAll(securityProviders)
}
}
private fun getSecurityProviders(): ArrayList<Pair<String, String>> {
val functionsList = ArrayList<Pair<String, String>>()
val providersList = Security.getProviders()
providersList.forEach {
functionsList.add(Pair(it.name, it.version.toString()))
}
return functionsList
companion object {
private const val ENCRYPTION_STATUS_UNSUPPORTED = "UNSUPPORTED"
private const val ENCRYPTION_STATUS_INACTIVE = "INACTIVE"
private const val ENCRYPTION_STATUS_ACTIVATING = "ACTIVATING"
private const val ENCRYPTION_STATUS_ACTIVE = "ACTIVE"
private const val ENCRYPTION_STATUS_ACTIVE_PER_USER = "ACTIVE_PER_USER"
}
}
@@ -16,29 +16,36 @@
package com.kgurgul.cpuinfo.features.information.base
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.SimpleItemAnimator
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.di.Injectable
/**
* Simple base for all info fragments which displays data on [RecyclerView]
*
* @author kgurgul
*/
abstract class BaseRvFragment : Fragment(), Injectable {
abstract class BaseRvFragment : Fragment(), Injectable, InfoItemsAdapter.OnClickListener {
protected lateinit var mainContainer: View
protected lateinit var recyclerView: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_rv, container, false)
mainContainer = view.findViewById(R.id.main_container)
recyclerView = view.findViewById(R.id.recycler_view)
setupRecyclerView()
setupRecyclerViewAdapter()
@@ -60,4 +67,13 @@ abstract class BaseRvFragment : Fragment(), Injectable {
* Setup adapter for [RecyclerView]. Use this method to set adapter and refreshing logic.
*/
abstract fun setupRecyclerViewAdapter()
override fun onItemLongPressed(item: Pair<String, String>) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE)
as ClipboardManager
val clip = ClipData.newPlainText(requireContext().getString(R.string.app_name),
item.second)
clipboard.primaryClip = clip
Snackbar.make(mainContainer, R.string.text_copied, Snackbar.LENGTH_SHORT).show()
}
}
@@ -16,16 +16,14 @@
package com.kgurgul.cpuinfo.features.information.base
import android.content.Context
import android.databinding.ObservableList
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.ObservableListAdapter
/**
* Adapter for all info items inside [BaseRvFragment]. It should support two types of layouts:
@@ -34,11 +32,11 @@ import com.kgurgul.cpuinfo.common.list.ObservableListAdapter
*
* @author kgurgul
*/
class InfoItemsAdapter(private val context: Context,
private val itemsObservableList: ObservableList<Pair<String, String>>,
private val layoutType: LayoutType = InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT) :
ObservableListAdapter<Pair<String, String>,
InfoItemsAdapter.SingleItemViewHolder>(itemsObservableList) {
class InfoItemsAdapter(
private val itemsObservableList: List<Pair<String, String>>,
private val layoutType: LayoutType = InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT,
private val onClickListener: OnClickListener? = null)
: RecyclerView.Adapter<InfoItemsAdapter.SingleItemViewHolder>() {
enum class LayoutType {
HORIZONTAL_LAYOUT, VERTICAL_LAYOUT
@@ -47,28 +45,57 @@ class InfoItemsAdapter(private val context: Context,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SingleItemViewHolder {
val layout = if (layoutType == LayoutType.HORIZONTAL_LAYOUT)
R.layout.item_value else R.layout.item_value_vertical
val v = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return SingleItemViewHolder(v)
return SingleItemViewHolder(LayoutInflater.from(parent.context).inflate(layout, parent,
false), onClickListener)
}
override fun onBindViewHolder(holder: SingleItemViewHolder, position: Int) {
val item = itemsObservableList[position]
holder.titleTv.text = item.first
holder.valueTv.text = item.second
if (item.second.isEmpty()) {
holder.titleTv.setTextColor(ContextCompat.getColor(context, R.color.colorAccent))
holder.valueTv.visibility = View.GONE
} else {
holder.titleTv.setTextColor(ContextCompat.getColor(context, R.color.textColorSecondary))
holder.valueTv.visibility = View.VISIBLE
}
holder.bind(itemsObservableList[position])
}
override fun getItemCount(): Int = itemsObservableList.size
class SingleItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTv: TextView = itemView.findViewById(R.id.title_tv)
val valueTv: TextView = itemView.findViewById(R.id.value_tv)
class SingleItemViewHolder(
itemView: View,
onClickListener: OnClickListener?) : RecyclerView.ViewHolder(itemView) {
private val titleTv: TextView = itemView.findViewById(R.id.title_tv)
private val valueTv: TextView = itemView.findViewById(R.id.value_tv)
private var item: Pair<String, String>? = null
init {
itemView.findViewById<LinearLayout>(R.id.item_container).setOnLongClickListener { _ ->
item?.let {
if (it.second.isNotEmpty()) {
onClickListener?.onItemLongPressed(it)
return@setOnLongClickListener true
}
}
false
}
}
fun bind(item: Pair<String, String>) {
this.item = item
titleTv.text = item.first
valueTv.text = item.second
if (item.second.isEmpty()) {
titleTv.setTextColor(ContextCompat.getColor(titleTv.context, R.color.accent))
valueTv.visibility = View.GONE
} else {
titleTv.setTextColor(ContextCompat.getColor(titleTv.context,
R.color.onSurface))
valueTv.visibility = View.VISIBLE
}
}
}
interface OnClickListener {
/**
* Invoked when user use long press on item
*/
fun onItemLongPressed(item: Pair<String, String>)
}
}
@@ -17,9 +17,8 @@
package com.kgurgul.cpuinfo.features.information.base
import android.content.Context
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentStatePagerAdapter
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.features.information.android.AndroidInfoFragment
import com.kgurgul.cpuinfo.features.information.cpu.CpuInfoFragment
@@ -49,7 +48,7 @@ class ViewPagerAdapter(val context: Context, manager: FragmentManager) :
const val INFO_PAGE_AMOUNT = 8
}
override fun getItem(position: Int): Fragment =
override fun getItem(position: Int): androidx.fragment.app.Fragment =
when (position) {
CPU_POS -> CpuInfoFragment()
GPU_POS -> GpuInfoFragment()
@@ -16,12 +16,18 @@
package com.kgurgul.cpuinfo.features.information.cpu
import android.arch.lifecycle.ViewModelProvider
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.features.information.base.InfoItemsAdapter
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import javax.inject.Inject
/**
@@ -39,26 +45,60 @@ class CpuInfoFragment : BaseRvFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(CpuInfoViewModel::class.java)
infoItemsAdapter = InfoItemsAdapter(requireContext(), viewModel.dataObservableList,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT)
setHasOptionsMenu(true)
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
runOnApiAbove(18) {
inflater.inflate(R.menu.info_menu, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_export_to_csv -> {
createSafFile(MIME_TEXT_PLAIN, DUMP_FILENAME, RC_CREATE_FILE)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onStart() {
super.onStart()
infoItemsAdapter.registerListChangeNotifier()
viewModel.startProvidingData()
}
override fun onStop() {
viewModel.stopProvidingData()
infoItemsAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun setupRecyclerViewAdapter() {
infoItemsAdapter = InfoItemsAdapter(viewModel.listLiveData,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT, onClickListener = this)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(infoItemsAdapter))
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
recyclerView.adapter = infoItemsAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_CREATE_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
viewModel.saveListToFile(uri)
}
}
}
}
}
companion object {
private const val RC_CREATE_FILE = 100
private const val DUMP_FILENAME = "cpu_info.txt"
}
}
@@ -16,27 +16,35 @@
package com.kgurgul.cpuinfo.features.information.cpu
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.net.Uri
import android.os.Build
import android.support.annotation.UiThread
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import androidx.annotation.UiThread
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.opencsv.CSVWriter
import io.reactivex.Flowable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.*
import java.util.ArrayList
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import javax.inject.Inject
import kotlin.collections.HashMap
/**
* ViewModel for CPU information
*
* @author kgurgul
*/
class CpuInfoViewModel @Inject constructor() : ViewModel() {
class CpuInfoViewModel @Inject constructor(
private val dispatchersProvider: DispatchersProvider,
private val contentResolver: ContentResolver
) : ScopedViewModel(dispatchersProvider) {
companion object {
private const val REFRESHING_INTERVAL = 1L
@@ -44,19 +52,19 @@ class CpuInfoViewModel @Inject constructor() : ViewModel() {
}
private val excludedTags = listOf("processor", "BogoMIPS")
private val minMaxFreqMap = HashMap<Int, Pair<Long, Long>>()
private val minMaxFreqMap = mutableMapOf<Int, Pair<Long, Long>>()
private var refreshingDisposable: Disposable? = null
val dataObservableList = AdapterArrayList<Pair<String, String>>()
val listLiveData = ListLiveData<Pair<String, String>>()
@Synchronized
fun startProvidingData() {
if (dataObservableList.isEmpty()) {
if (listLiveData.isEmpty()) {
val cpuInfoList = ArrayList<Pair<String, String>>()
cpuInfoList.add(getAbi())
cpuInfoList.addAll(getCoresInfo())
cpuInfoList.addAll(getCpuInfoFromFile())
dataObservableList.addAll(cpuInfoList)
listLiveData.addAll(cpuInfoList)
}
if (refreshingDisposable == null || refreshingDisposable?.isDisposed != false) {
@@ -72,13 +80,32 @@ class CpuInfoViewModel @Inject constructor() : ViewModel() {
}
/**
* Get current CPU frequency and refresh [dataObservableList]
* Invoked when user wants to export whole list to the CSV file
*/
fun saveListToFile(uri: Uri) {
launch(context = dispatchersProvider.ioDispatcher) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
CSVWriter(FileWriter(it.fileDescriptor)).use { csvWriter ->
listLiveData.forEach { pair ->
csvWriter.writeNext(pair.toList().toTypedArray())
}
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
/**
* Get current CPU frequency and refresh [listLiveData]
*/
@UiThread
private fun refreshFrequencies() {
Timber.i("refreshFrequencies()")
val freqList = getCoresInfo()
freqList.forEachIndexed { i, pair -> dataObservableList[i + FREQUENCY_OFFSET] = pair }
freqList.forEachIndexed { i, pair -> listLiveData[i + FREQUENCY_OFFSET] = pair }
}
/**
@@ -101,11 +128,9 @@ class CpuInfoViewModel @Inject constructor() : ViewModel() {
private fun getCoresInfo(): ArrayList<Pair<String, String>> {
val coresList = ArrayList<Pair<String, String>>()
val numbersOfCores = getNumberOfCores()
var i = 0
coresList.add(Pair("Cores", numbersOfCores.toString()))
while (i < numbersOfCores) {
for (i in 0 until numbersOfCores) {
coresList.add(Pair(" Core $i", tryToGetCurrentFreq(i)))
i++
}
return coresList
}
@@ -166,8 +191,7 @@ class CpuInfoViewModel @Inject constructor() : ViewModel() {
frequency += "\n(${minMaxPair.first}MHz - ${minMaxPair.second}MHz)"
}
}
} catch (e: Exception) {
// Timber.e(e)
} catch (ignored: Exception) {
}
return frequency
@@ -188,7 +212,7 @@ class CpuInfoViewModel @Inject constructor() : ViewModel() {
val maxMhz = reader.readLine().toLong() / 1000
reader.close()
val values = Pair(minMhz, maxMhz)
minMaxFreqMap.put(coreNumber, values)
minMaxFreqMap[coreNumber] = values
return values
} catch (e: Exception) {
Timber.e(e)
@@ -237,4 +261,4 @@ class CpuInfoViewModel @Inject constructor() : ViewModel() {
1
}
}
}
}
@@ -16,18 +16,18 @@
package com.kgurgul.cpuinfo.features.information.gpu
import android.arch.lifecycle.ViewModelProvider
import android.app.Activity
import android.content.Intent
import android.opengl.GLSurfaceView
import android.os.Bundle
import android.os.Handler
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.features.information.base.InfoItemsAdapter
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import javax.inject.Inject
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
@@ -70,12 +70,26 @@ class GpuInfoFragment : BaseRvFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(GpuInfoViewModel::class.java)
infoItemsAdapter = InfoItemsAdapter(requireContext(), viewModel.dataObservableList,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT)
setHasOptionsMenu(true)
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
runOnApiAbove(18) {
inflater.inflate(R.menu.info_menu, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_export_to_csv -> {
createSafFile(MIME_TEXT_PLAIN, DUMP_FILENAME, RC_CREATE_FILE)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val view = super.onCreateView(inflater, container, savedInstanceState)
@@ -92,11 +106,6 @@ class GpuInfoFragment : BaseRvFragment() {
return view
}
override fun onStart() {
super.onStart()
infoItemsAdapter.registerListChangeNotifier()
}
override fun onResume() {
super.onResume()
glSurfaceView?.onResume()
@@ -107,13 +116,30 @@ class GpuInfoFragment : BaseRvFragment() {
glSurfaceView?.onPause()
}
override fun onStop() {
infoItemsAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun setupRecyclerViewAdapter() {
infoItemsAdapter = InfoItemsAdapter(viewModel.listLiveData,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT, onClickListener = this)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(infoItemsAdapter))
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
recyclerView.adapter = infoItemsAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_CREATE_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
viewModel.saveListToFile(uri)
}
}
}
}
}
companion object {
private const val RC_CREATE_FILE = 100
private const val DUMP_FILENAME = "gpu_info.txt"
}
}
@@ -17,11 +17,18 @@
package com.kgurgul.cpuinfo.features.information.gpu
import android.app.ActivityManager
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.content.res.Resources
import android.net.Uri
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.Utils
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.opencsv.CSVWriter
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.FileWriter
import javax.inject.Inject
/**
@@ -29,10 +36,14 @@ import javax.inject.Inject
*
* @author kgurgul
*/
class GpuInfoViewModel @Inject constructor(private val activityManager: ActivityManager,
private val resources: Resources) : ViewModel() {
class GpuInfoViewModel @Inject constructor(
private val activityManager: ActivityManager,
private val resources: Resources,
private val dispatchersProvider: DispatchersProvider,
private val contentResolver: ContentResolver
) : ScopedViewModel(dispatchersProvider) {
val dataObservableList = AdapterArrayList<Pair<String, String>>()
val listLiveData = ListLiveData<Pair<String, String>>()
init {
getGpuData()
@@ -42,23 +53,22 @@ class GpuInfoViewModel @Inject constructor(private val activityManager: Activity
* Get all GPU information
*/
private fun getGpuData() {
if (dataObservableList.isEmpty()) {
if (listLiveData.isEmpty()) {
val configurationInfo = activityManager.deviceConfigurationInfo
val version = configurationInfo.glEsVersion
if (!version.isNullOrEmpty()) {
// Add GLES version on the first position because this ViewModel doesn't contains
// synchronization with "addGlInfo" method
dataObservableList.add(0, Pair(resources.getString(R.string.gles_version),
listLiveData.add(0, Pair(resources.getString(R.string.gles_version),
version))
}
}
}
/**
* Check if additional GPU info was already added into [dataObservableList]
* Check if additional GPU info was already added into [listLiveData]
*/
fun isGlInfoStored(): Boolean =
dataObservableList.size > 1
fun isGlInfoStored(): Boolean = listLiveData.size > 1
/**
* Add additional GPU info from OpenGL if it wasn't added previously
@@ -66,7 +76,7 @@ class GpuInfoViewModel @Inject constructor(private val activityManager: Activity
* @param gpuInfoMap map of additional data like version, render etc.
*/
fun addGlInfo(gpuInfoMap: Map<GlInfoType, String?>) {
if (dataObservableList.size <= 1) {
if (listLiveData.size <= 1) {
val gpuInfoPairs = ArrayList<Pair<String, String>>()
Utils.addPairIfExists(gpuInfoPairs, resources.getString(R.string.vendor),
gpuInfoMap[GlInfoType.GL_VENDOR])
@@ -76,7 +86,26 @@ class GpuInfoViewModel @Inject constructor(private val activityManager: Activity
gpuInfoMap[GlInfoType.GL_RENDERER])
Utils.addPairIfExists(gpuInfoPairs, resources.getString(R.string.extensions),
gpuInfoMap[GlInfoType.GL_EXTENSIONS])
dataObservableList.addAll(gpuInfoPairs)
listLiveData.addAll(gpuInfoPairs)
}
}
/**
* Invoked when user wants to export whole list to the CSV file
*/
fun saveListToFile(uri: Uri) {
launch(context = dispatchersProvider.ioDispatcher) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
CSVWriter(FileWriter(it.fileDescriptor)).use { csvWriter ->
listLiveData.forEach { pair ->
csvWriter.writeNext(pair.toList().toTypedArray())
}
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
@@ -27,7 +27,7 @@ import javax.inject.Singleton
/**
* Return battery status from ACTION_BATTERY_CHANGED broadcast receiver and capacity from
* com.android.internal.os.PowerProfile
* [com.android.internal.os.PowerProfile]
*
* @author kgurgul
*/
@@ -48,7 +48,6 @@ class BatteryStatusProvider @Inject constructor(private val app: Application) {
@SuppressLint("PrivateApi")
fun getBatteryCapacity(): Double {
var capacity = -1.0
try {
val powerProfile = Class.forName("com.android.internal.os.PowerProfile")
.getConstructor(Context::class.java).newInstance(app)
@@ -59,7 +58,6 @@ class BatteryStatusProvider @Inject constructor(private val app: Application) {
} catch (e: Exception) {
Timber.e(e)
}
return capacity
}
}
@@ -16,16 +16,21 @@
package com.kgurgul.cpuinfo.features.information.hardware
import android.arch.lifecycle.ViewModelProvider
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.features.information.base.InfoItemsAdapter
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import javax.inject.Inject
/**
@@ -49,24 +54,31 @@ class HardwareInfoFragment : BaseRvFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(HardwareInfoViewModel::class.java)
infoItemsAdapter = InfoItemsAdapter(requireContext(), viewModel.dataObservableList,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT)
setHasOptionsMenu(true)
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onStart() {
super.onStart()
infoItemsAdapter.registerListChangeNotifier()
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
runOnApiAbove(18) {
inflater.inflate(R.menu.info_menu, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_export_to_csv -> {
createSafFile(MIME_TEXT_PLAIN, DUMP_FILENAME, RC_CREATE_FILE)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter()
intentFilter.addAction("android.intent.action.ACTION_POWER_CONNECTED")
intentFilter.addAction("android.intent.action.ACTION_POWER_DISCONNECTED")
requireActivity().registerReceiver(powerReceiver, intentFilter)
}
@@ -75,13 +87,30 @@ class HardwareInfoFragment : BaseRvFragment() {
requireActivity().unregisterReceiver(powerReceiver)
}
override fun onStop() {
infoItemsAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun setupRecyclerViewAdapter() {
infoItemsAdapter = InfoItemsAdapter(viewModel.listLiveData,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT, onClickListener = this)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(infoItemsAdapter))
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
recyclerView.adapter = infoItemsAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_CREATE_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
viewModel.saveListToFile(uri)
}
}
}
}
}
companion object {
private const val RC_CREATE_FILE = 100
private const val DUMP_FILENAME = "hardware_info.txt"
}
}
@@ -19,25 +19,26 @@
package com.kgurgul.cpuinfo.features.information.hardware
import android.annotation.SuppressLint
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.hardware.Camera
import android.net.Uri
import android.os.BatteryManager
import android.os.Build
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import com.kgurgul.cpuinfo.features.settings.SettingsFragment
import com.kgurgul.cpuinfo.features.temperature.TemperatureFormatter
import com.kgurgul.cpuinfo.features.temperature.TemperatureProvider
import com.kgurgul.cpuinfo.utils.Utils
import com.kgurgul.cpuinfo.utils.round2
import com.kgurgul.cpuinfo.utils.runOnApiAbove
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.opencsv.CSVWriter
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.FileFilter
import java.io.FileWriter
import java.io.RandomAccessFile
import java.util.*
import java.util.regex.Pattern
@@ -55,10 +56,11 @@ class HardwareInfoViewModel @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val packageManager: PackageManager,
private val contentResolver: ContentResolver,
private val batteryStatusProvider: BatteryStatusProvider)
: ViewModel(), SharedPreferences.OnSharedPreferenceChangeListener {
private val batteryStatusProvider: BatteryStatusProvider,
private val dispatchersProvider: DispatchersProvider
) : ScopedViewModel(dispatchersProvider), SharedPreferences.OnSharedPreferenceChangeListener {
val dataObservableList = AdapterArrayList<Pair<String, String>>()
val listLiveData = ListLiveData<Pair<String, String>>()
init {
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
@@ -71,25 +73,44 @@ class HardwareInfoViewModel @Inject constructor(
*/
@Synchronized
fun refreshHardwareInfo() {
if (dataObservableList.isNotEmpty()) {
dataObservableList.clear()
if (listLiveData.isNotEmpty()) {
listLiveData.clear()
}
dataObservableList.add(Pair(resources.getString(R.string.battery), ""))
dataObservableList.addAll(getBatteryStatus())
listLiveData.add(Pair(resources.getString(R.string.battery), ""))
listLiveData.addAll(getBatteryStatus())
if (hasCamera()) {
dataObservableList.add(Pair(resources.getString(R.string.cameras), ""))
dataObservableList.addAll(getCameraInfo())
listLiveData.add(Pair(resources.getString(R.string.cameras), ""))
listLiveData.addAll(getCameraInfo())
}
dataObservableList.add(Pair(resources.getString(R.string.sound_card), ""))
dataObservableList.addAll(getSoundCardInfo())
listLiveData.add(Pair(resources.getString(R.string.sound_card), ""))
listLiveData.addAll(getSoundCardInfo())
val wirelessInfo = getWirelessInfo()
if (wirelessInfo.size > 0) {
dataObservableList.add(Pair(resources.getString(R.string.wireless), ""))
dataObservableList.addAll(wirelessInfo)
listLiveData.add(Pair(resources.getString(R.string.wireless), ""))
listLiveData.addAll(wirelessInfo)
}
}
/**
* Invoked when user wants to export whole list to the CSV file
*/
fun saveListToFile(uri: Uri) {
launch(context = dispatchersProvider.ioDispatcher) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
CSVWriter(FileWriter(it.fileDescriptor)).use { csvWriter ->
listLiveData.forEach { pair ->
csvWriter.writeNext(pair.toList().toTypedArray())
}
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
@@ -197,12 +218,12 @@ class HardwareInfoViewModel @Inject constructor(
val hasBluetooth = if (packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH))
resources.getString(R.string.yes) else resources.getString(R.string.no)
functionsList.add(Pair(resources.getString(R.string.bluetooth), hasBluetooth))
runOnApiAbove(Build.VERSION_CODES.JELLY_BEAN_MR1, {
runOnApiAbove(Build.VERSION_CODES.JELLY_BEAN_MR1) {
val hasBluetoothLe =
if (packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE))
resources.getString(R.string.yes) else resources.getString(R.string.no)
functionsList.add(Pair(resources.getString(R.string.bluetooth_le), hasBluetoothLe))
})
}
val bluetoothMac = android.provider.Settings.Secure.getString(contentResolver,
@@ -217,9 +238,7 @@ class HardwareInfoViewModel @Inject constructor(
val value = reader.readLine()
reader.close()
functionsList.add(Pair(resources.getString(R.string.wifi_mac), value))
} catch (e: NumberFormatException) {
e.printStackTrace()
} catch (e: Exception) {
} catch (ignored: Exception) {
}
return functionsList
@@ -230,8 +249,8 @@ class HardwareInfoViewModel @Inject constructor(
*/
private fun getSoundCardNumber(): Int {
class AudioFilter : FileFilter {
// http://alsa.opensrc.org/Proc_asound_documentation
override fun accept(pathname: File): Boolean =
// http://alsa.opensrc.org/Proc_asound_documentation
Pattern.matches("card[0-7]+", pathname.name)
}
@@ -247,19 +266,14 @@ class HardwareInfoViewModel @Inject constructor(
/**
* Get available data connected with sound card like ALSA version etc.
*/
private fun getSoundCardInfo(): ArrayList<Pair<String, String>> {
val functionsList = ArrayList<Pair<String, String>>()
private fun getSoundCardInfo(): List<Pair<String, String>> {
val functionsList = mutableListOf<Pair<String, String>>()
val soundCardNumber = getSoundCardNumber()
functionsList.add(Pair(resources.getString(R.string.amount), soundCardNumber.toString()))
var iterator = 0
while (iterator < soundCardNumber) {
functionsList.add(Pair(" ${resources.getString(R.string.card)} $iterator",
tryToGetSoundCardId(iterator)))
iterator++
for (i in 0 until soundCardNumber) {
functionsList.add(Pair(" ${resources.getString(R.string.card)} $i", tryToGetSoundCardId(i)))
}
// ALSA
val alsa = tryToGetAlsa()
Utils.addPairIfExists(functionsList, "ALSA", alsa)
@@ -281,8 +295,7 @@ class HardwareInfoViewModel @Inject constructor(
try {
reader = RandomAccessFile(filePath, "r")
id = reader.readLine()
} catch (e: Exception) {
// Do nothing
} catch (ignored: Exception) {
} finally {
reader?.close()
}
@@ -302,8 +315,7 @@ class HardwareInfoViewModel @Inject constructor(
reader = RandomAccessFile(filePath, "r")
val version = reader.readLine()
alsa = version
} catch (e: Exception) {
// Do nothing
} catch (ignored: Exception) {
} finally {
reader?.close()
}
@@ -321,8 +333,8 @@ class HardwareInfoViewModel @Inject constructor(
* Get number, type and orientation of the cameras
*/
@Suppress("DEPRECATION")
private fun getCameraInfo(): ArrayList<Pair<String, String>> {
val functionsList = ArrayList<Pair<String, String>>()
private fun getCameraInfo(): List<Pair<String, String>> {
val functionsList = mutableListOf<Pair<String, String>>()
val numbersOfCameras = Camera.getNumberOfCameras()
functionsList.add(Pair(resources.getString(R.string.amount), numbersOfCameras.toString()))
@@ -330,22 +342,17 @@ class HardwareInfoViewModel @Inject constructor(
val cameraName = resources.getString(R.string.camera)
val cameraType = resources.getString(R.string.type)
val cameraOrientation = resources.getString(R.string.orientation)
var iterator = 0
while (iterator < numbersOfCameras) {
functionsList.add(Pair(" $cameraName $iterator", " "))
// Strange error on some SM-G930F with getCameraInfo
for (i in 0 until numbersOfCameras) {
functionsList.add(Pair(" $cameraName $i", " "))
try {
val info = Camera.CameraInfo()
Camera.getCameraInfo(iterator, info)
Camera.getCameraInfo(i, info)
val type = getCameraType(info)
functionsList.add(Pair(" $cameraType", type))
functionsList.add(Pair(" $cameraOrientation", info.orientation.toString()))
} catch (e: Exception) {
Timber.e(e)
}
iterator++
}
return functionsList
@@ -1,85 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.features.information.ram
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.AsyncTask
import android.os.Build
import com.kgurgul.cpuinfo.CpuInfoApp
import com.kgurgul.cpuinfo.utils.processmanager.AndroidProcesses
import java.util.*
/**
* Simple task for all Android which reset most popular apps and runs GC
*
* @author kgurgul
*/
class ClearRamAsyncTask : AsyncTask<Void, Void, Void?>() {
override fun doInBackground(vararg params: Void?): Void? {
freeRam()
return null
}
private fun freeRam() {
// GC operations
System.runFinalization()
Runtime.getRuntime().gc()
System.gc()
// Not my solution - really bad idea but user wants ram :-)
// For SDK 24 we can't get running processes list
if (Build.VERSION.SDK_INT < 24) {
freeMemoryUsingVoodoo(CpuInfoApp.instance)
}
}
private fun freeMemoryUsingVoodoo(app: Application) {
val reservedPackages = ArrayList<String>()
reservedPackages.add("system")
reservedPackages.add("com.android.launcher2")
reservedPackages.add("com.android.inputmethod.latin")
reservedPackages.add("com.android.phone")
reservedPackages.add("com.android.wallpaper")
reservedPackages.add("com.google.process.gapps")
reservedPackages.add("android.process.acore")
reservedPackages.add("android.process.media")
reservedPackages.add("com.android.bluetooth")
reservedPackages.add(app.packageName)
val packagesToKill = ArrayList<String>()
val am = app.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (process in AndroidProcesses.getRunningAppProcesses()) {
try {
val appInfo = process.getPackageInfo(app, 0).applicationInfo
if (!reservedPackages.contains(process.packageName)
&& appInfo.flags and ApplicationInfo.FLAG_PERSISTENT == 0) {
packagesToKill.add(process.packageName)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
for (pck in packagesToKill) {
am.killBackgroundProcesses(pck)
}
}
}
@@ -16,16 +16,19 @@
package com.kgurgul.cpuinfo.features.information.ram
import android.arch.lifecycle.ViewModelProvider
import android.os.Build
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.view.*
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.google.android.material.snackbar.Snackbar
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.features.information.base.InfoItemsAdapter
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import javax.inject.Inject
/**
@@ -41,41 +44,30 @@ class RamInfoFragment : BaseRvFragment() {
private lateinit var viewModel: RamInfoViewModel
private lateinit var infoItemsAdapter: InfoItemsAdapter
private lateinit var mainContainer: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(RamInfoViewModel::class.java)
infoItemsAdapter = InfoItemsAdapter(requireContext(), viewModel.dataObservableList,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val view = super.onCreateView(inflater, container, savedInstanceState)
mainContainer = view.findViewById(R.id.main_container)
return view
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onStart() {
super.onStart()
infoItemsAdapter.registerListChangeNotifier()
viewModel.startProvidingData()
}
override fun onStop() {
viewModel.stopProvidingData()
infoItemsAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Cleaning options works only for old Android
if (Build.VERSION.SDK_INT < 24) {
runOnApiBelow(24) {
inflater.inflate(R.menu.ram_menu, menu)
}
runOnApiAbove(18) {
inflater.inflate(R.menu.info_menu, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
@@ -87,11 +79,37 @@ class RamInfoFragment : BaseRvFragment() {
Snackbar.LENGTH_SHORT).show()
true
}
R.id.action_export_to_csv -> {
createSafFile(MIME_TEXT_PLAIN, DUMP_FILENAME, RC_CREATE_FILE)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun setupRecyclerViewAdapter() {
infoItemsAdapter = InfoItemsAdapter(viewModel.listLiveData,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT, onClickListener = this)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(infoItemsAdapter))
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
recyclerView.adapter = infoItemsAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_CREATE_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
viewModel.saveListToFile(uri)
}
}
}
}
}
companion object {
private const val RC_CREATE_FILE = 100
private const val DUMP_FILENAME = "ram_info.txt"
}
}
@@ -18,20 +18,24 @@ package com.kgurgul.cpuinfo.features.information.ram
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.content.res.Resources
import android.os.AsyncTask
import android.net.Uri
import android.os.Build
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.Utils
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.kgurgul.cpuinfo.utils.runOnApiAbove
import com.opencsv.CSVWriter
import io.reactivex.Flowable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.FileWriter
import java.io.RandomAccessFile
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import javax.inject.Inject
@@ -41,8 +45,12 @@ import javax.inject.Inject
*
* @author kgurgul
*/
class RamInfoViewModel @Inject constructor(private val activityManager: ActivityManager,
private val resources: Resources) : ViewModel() {
class RamInfoViewModel @Inject constructor(
private val activityManager: ActivityManager,
private val resources: Resources,
private val dispatchersProvider: DispatchersProvider,
private val contentResolver: ContentResolver
) : ScopedViewModel(dispatchersProvider) {
companion object {
private const val REFRESHING_INTERVAL = 5L
@@ -50,9 +58,8 @@ class RamInfoViewModel @Inject constructor(private val activityManager: Activity
private val memoryInfo = ActivityManager.MemoryInfo()
private var ramRefreshingDisposable: Disposable? = null
private var clearRamAsyncTask: ClearRamAsyncTask? = null
val dataObservableList = AdapterArrayList<Pair<String, String>>()
val listLiveData = ListLiveData<Pair<String, String>>()
@Synchronized
fun startProvidingData() {
@@ -74,7 +81,7 @@ class RamInfoViewModel @Inject constructor(private val activityManager: Activity
@SuppressLint("InlinedApi")
private fun refreshRamData() {
Timber.i("refreshRamData()")
val memoryInfoList = ArrayList<Pair<String, String>>()
val memoryInfoList = mutableListOf<Pair<String, String>>()
activityManager.getMemoryInfo(memoryInfo)
runOnApiAbove(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1, {
@@ -99,7 +106,7 @@ class RamInfoViewModel @Inject constructor(private val activityManager: Activity
memoryInfoList.add(Pair(resources.getString(R.string.threshold),
Utils.humanReadableByteCount(memoryInfo.threshold)))
dataObservableList.replace(memoryInfoList)
listLiveData.replace(memoryInfoList)
}
/**
@@ -135,10 +142,29 @@ class RamInfoViewModel @Inject constructor(private val activityManager: Activity
* Run task to clear RAM memory
*/
fun clearRam() {
if (clearRamAsyncTask == null || clearRamAsyncTask?.status != AsyncTask.Status.RUNNING) {
clearRamAsyncTask?.cancel(true)
clearRamAsyncTask = ClearRamAsyncTask()
clearRamAsyncTask?.execute()
launch(context = dispatchersProvider.ioDispatcher) {
System.runFinalization()
Runtime.getRuntime().gc()
System.gc()
}
}
/**
* Invoked when user wants to export whole list to the CSV file
*/
fun saveListToFile(uri: Uri) {
launch(context = dispatchersProvider.ioDispatcher) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
CSVWriter(FileWriter(it.fileDescriptor)).use { csvWriter ->
listLiveData.forEach { pair ->
csvWriter.writeNext(pair.toList().toTypedArray())
}
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
}
@@ -16,12 +16,18 @@
package com.kgurgul.cpuinfo.features.information.screen
import android.arch.lifecycle.ViewModelProvider
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.features.information.base.InfoItemsAdapter
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import javax.inject.Inject
/**
@@ -39,24 +45,50 @@ class ScreenInfoFragment : BaseRvFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(ScreenInfoViewModel::class.java)
infoItemsAdapter = InfoItemsAdapter(requireContext(), viewModel.dataObservableList,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT)
setHasOptionsMenu(true)
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onStart() {
super.onStart()
infoItemsAdapter.registerListChangeNotifier()
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
runOnApiAbove(18) {
inflater.inflate(R.menu.info_menu, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onStop() {
infoItemsAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_export_to_csv -> {
createSafFile(MIME_TEXT_PLAIN, DUMP_FILENAME, RC_CREATE_FILE)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun setupRecyclerViewAdapter() {
infoItemsAdapter = InfoItemsAdapter(viewModel.listLiveData,
InfoItemsAdapter.LayoutType.HORIZONTAL_LAYOUT, onClickListener = this)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(infoItemsAdapter))
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
recyclerView.adapter = infoItemsAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_CREATE_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
viewModel.saveListToFile(uri)
}
}
}
}
}
companion object {
private const val RC_CREATE_FILE = 100
private const val DUMP_FILENAME = "screen_info.txt"
}
}
@@ -16,17 +16,23 @@
package com.kgurgul.cpuinfo.features.information.screen
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.content.res.Configuration
import android.content.res.Resources
import android.net.Uri
import android.os.Build
import android.util.DisplayMetrics
import android.view.Display
import android.view.WindowManager
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.kgurgul.cpuinfo.utils.round2
import java.util.*
import com.opencsv.CSVWriter
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.FileWriter
import javax.inject.Inject
/**
@@ -36,9 +42,12 @@ import javax.inject.Inject
*/
class ScreenInfoViewModel @Inject constructor(
private val resources: Resources,
private val windowManager: WindowManager) : ViewModel() {
private val windowManager: WindowManager,
private val dispatchersProvider: DispatchersProvider,
private val contentResolver: ContentResolver
) : ScopedViewModel(dispatchersProvider) {
val dataObservableList = AdapterArrayList<Pair<String, String>>()
val listLiveData = ListLiveData<Pair<String, String>>()
init {
getScreenData()
@@ -48,12 +57,12 @@ class ScreenInfoViewModel @Inject constructor(
* Get all screen details
*/
private fun getScreenData() {
if (dataObservableList.isNotEmpty()) {
dataObservableList.clear()
if (listLiveData.isNotEmpty()) {
listLiveData.clear()
}
dataObservableList.add(getScreenClass())
dataObservableList.add(getDensityClass())
dataObservableList.addAll(getInfoFromDisplayMetrics())
listLiveData.add(getScreenClass())
listLiveData.add(getDensityClass())
listLiveData.addAll(getInfoFromDisplayMetrics())
}
/**
@@ -115,8 +124,8 @@ class ScreenInfoViewModel @Inject constructor(
return Pair(resources.getString(R.string.density_class), densityClass)
}
private fun getInfoFromDisplayMetrics(): ArrayList<Pair<String, String>> {
val functionsList = ArrayList<Pair<String, String>>()
private fun getInfoFromDisplayMetrics(): List<Pair<String, String>> {
val functionsList = mutableListOf<Pair<String, String>>()
val display = windowManager.defaultDisplay
@@ -159,4 +168,23 @@ class ScreenInfoViewModel @Inject constructor(
return functionsList
}
/**
* Invoked when user wants to export whole list to the CSV file
*/
fun saveListToFile(uri: Uri) {
launch(context = dispatchersProvider.ioDispatcher) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
CSVWriter(FileWriter(it.fileDescriptor)).use { csvWriter ->
listLiveData.forEach { pair ->
csvWriter.writeNext(pair.toList().toTypedArray())
}
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
}
@@ -16,12 +16,18 @@
package com.kgurgul.cpuinfo.features.information.sensors
import android.arch.lifecycle.ViewModelProvider
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.features.information.base.InfoItemsAdapter
import com.kgurgul.cpuinfo.utils.*
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import javax.inject.Inject
/**
@@ -39,26 +45,60 @@ class SensorsInfoFragment : BaseRvFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(SensorsInfoViewModel::class.java)
infoItemsAdapter = InfoItemsAdapter(requireContext(), viewModel.dataObservableList,
InfoItemsAdapter.LayoutType.VERTICAL_LAYOUT)
setHasOptionsMenu(true)
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
runOnApiAbove(18) {
inflater.inflate(R.menu.info_menu, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_export_to_csv -> {
createSafFile(MIME_TEXT_PLAIN, DUMP_FILENAME, RC_CREATE_FILE)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onStart() {
super.onStart()
infoItemsAdapter.registerListChangeNotifier()
viewModel.startProvidingData()
}
override fun onStop() {
viewModel.stopProvidingData()
infoItemsAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun setupRecyclerViewAdapter() {
infoItemsAdapter = InfoItemsAdapter(viewModel.listLiveData,
InfoItemsAdapter.LayoutType.VERTICAL_LAYOUT, onClickListener = this)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(infoItemsAdapter))
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
recyclerView.adapter = infoItemsAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RC_CREATE_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
viewModel.saveListToFile(uri)
}
}
}
}
}
companion object {
private const val RC_CREATE_FILE = 100
private const val DUMP_FILENAME = "sensors_info.txt"
}
}
@@ -16,42 +16,48 @@
package com.kgurgul.cpuinfo.features.information.sensors
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import android.net.Uri
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import com.kgurgul.cpuinfo.utils.round1
import java.util.*
import com.kgurgul.cpuinfo.utils.runOnApiAbove
import com.opencsv.CSVWriter
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.FileWriter
import javax.inject.Inject
/**
* ViewModel for reading sensors data
* ViewModel for sensors data
*
* @author kgurgul
*/
class SensorsInfoViewModel @Inject constructor(
private val sensorManager: SensorManager) : ViewModel(), SensorEventListener {
private val sensorManager: SensorManager,
private val dispatchersProvider: DispatchersProvider,
private val contentResolver: ContentResolver
) : ScopedViewModel(dispatchersProvider), SensorEventListener {
val dataObservableList = AdapterArrayList<Pair<String, String>>()
val listLiveData = ListLiveData<Pair<String, String>>()
private val sensorList: List<Sensor> = sensorManager.getSensorList(Sensor.TYPE_ALL)
private val sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL)
@Synchronized
fun startProvidingData() {
if (dataObservableList.isEmpty()) {
val functionsList = sensorList.mapTo(ArrayList<Pair<String, String>>()) {
Pair(it.name, " ")
}
dataObservableList.addAll(functionsList)
if (listLiveData.isEmpty()) {
listLiveData.addAll(sensorList.map { Pair(it.name, " ") })
}
// Start register process on new Thread to avoid UI block
Thread {
for (sensor in sensorList) {
sensorManager.registerListener(SensorsViewModel@ this, sensor,
sensorManager.registerListener(this, sensor,
SensorManager.SENSOR_DELAY_NORMAL)
}
}.start()
@@ -59,10 +65,29 @@ class SensorsInfoViewModel @Inject constructor(
fun stopProvidingData() {
Thread {
sensorManager.unregisterListener(SensorsFragment@ this)
sensorManager.unregisterListener(this)
}.start()
}
/**
* Invoked when user wants to export whole list to the CSV file
*/
fun saveListToFile(uri: Uri) {
launch(context = dispatchersProvider.ioDispatcher) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
CSVWriter(FileWriter(it.fileDescriptor)).use { csvWriter ->
listLiveData.forEach { pair ->
csvWriter.writeNext(pair.toList().toTypedArray())
}
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
// Do nothing
}
@@ -77,7 +102,7 @@ class SensorsInfoViewModel @Inject constructor(
@Synchronized
private fun updateSensorInfo(event: SensorEvent) {
val updatedRowId = sensorList.indexOf(event.sensor)
dataObservableList[updatedRowId] = Pair(event.sensor.name, getSensorData(event))
listLiveData[updatedRowId] = Pair(event.sensor.name, getSensorData(event))
}
/**
@@ -119,7 +144,7 @@ class SensorsInfoViewModel @Inject constructor(
}
// TODO: Multiline support for this kind of data is necessary
if (Build.VERSION.SDK_INT >= 18) {
runOnApiAbove(17) {
when (sensorType) {
Sensor.TYPE_GYROSCOPE_UNCALIBRATED ->
data = "X=${event.values[0].round1()}rad/s Y=${
@@ -141,7 +166,7 @@ class SensorsInfoViewModel @Inject constructor(
}
}
if (Build.VERSION.SDK_INT >= 19) {
runOnApiAbove(18) {
when (sensorType) {
Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR ->
data = "X=${event.values[0].round1()} Y=${
@@ -16,14 +16,12 @@
package com.kgurgul.cpuinfo.features.information.storage
import android.databinding.ObservableList
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.ObservableListAdapter
import com.kgurgul.cpuinfo.utils.Utils
import com.kgurgul.cpuinfo.utils.round2
import com.kgurgul.cpuinfo.widgets.progress.IconRoundCornerProgressBar
@@ -33,11 +31,10 @@ import com.kgurgul.cpuinfo.widgets.progress.IconRoundCornerProgressBar
*
* @author kgurgul
*/
class StorageAdapter(private val storageObservableList: ObservableList<StorageItem>)
: ObservableListAdapter<StorageItem, StorageAdapter.ViewHolder>(storageObservableList) {
class StorageAdapter(private val storageList: List<StorageItem>)
: RecyclerView.Adapter<StorageAdapter.ViewHolder>() {
override fun getItemCount(): Int =
storageObservableList.size
override fun getItemCount(): Int = storageList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
@@ -45,7 +42,7 @@ class StorageAdapter(private val storageObservableList: ObservableList<StorageIt
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindViewHolder(storageObservableList[position])
holder.bindViewHolder(storageList[position])
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -16,7 +16,6 @@
package com.kgurgul.cpuinfo.features.information.storage
import android.arch.lifecycle.ViewModelProvider
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -26,6 +25,8 @@ import android.os.Handler
import com.kgurgul.cpuinfo.di.Injectable
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.information.base.BaseRvFragment
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import com.kgurgul.cpuinfo.utils.viewModelProvider
import javax.inject.Inject
/**
@@ -54,13 +55,7 @@ class StorageInfoFragment : BaseRvFragment(), Injectable {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(StorageInfoViewModel::class.java)
}
override fun onStart() {
super.onStart()
storageAdapter.registerListChangeNotifier()
viewModel = viewModelProvider(viewModelInjectionFactory)
}
override fun onResume() {
@@ -94,13 +89,10 @@ class StorageInfoFragment : BaseRvFragment(), Injectable {
super.onPause()
}
override fun onStop() {
storageAdapter.unregisterListChangeNotifier()
super.onStop()
}
override fun setupRecyclerViewAdapter() {
storageAdapter = StorageAdapter(viewModel.storageItemList)
storageAdapter = StorageAdapter(viewModel.listLiveData)
viewModel.listLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(storageAdapter))
recyclerView.adapter = storageAdapter
}
}
@@ -16,20 +16,19 @@
package com.kgurgul.cpuinfo.features.information.storage
import android.arch.lifecycle.ViewModel
import android.content.res.Resources
import android.os.Environment
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import io.reactivex.Single
import io.reactivex.SingleEmitter
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import org.jetbrains.anko.coroutines.experimental.asReference
import org.jetbrains.anko.coroutines.experimental.bg
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.*
import java.util.*
@@ -40,13 +39,15 @@ import javax.inject.Inject
*
* @author kgurgul
*/
class StorageInfoViewModel @Inject constructor(private val resources: Resources) : ViewModel() {
class StorageInfoViewModel @Inject constructor(
private val dispatchersProvider: DispatchersProvider,
private val resources: Resources
) : ScopedViewModel(dispatchersProvider) {
enum class MemoryType { INTERNAL, EXTERNAL }
val storageItemList = AdapterArrayList<StorageItem>()
val listLiveData = ListLiveData<StorageItem>()
private val ref = asReference()
private var sdCardFinderDisposable: Disposable? = null
init {
@@ -57,12 +58,13 @@ class StorageInfoViewModel @Inject constructor(private val resources: Resources)
* Get all available details about internal, external and secondary (SD card) storage
*/
private fun getStorageInfo() {
async(UI) {
val result = bg { getExternalAndInternalMemoryPair() }
val memoryPair = result.await()
ref().storageItemList.add(memoryPair.first)
launch {
val memoryPair = withContext(dispatchersProvider.ioDispatcher) {
getExternalAndInternalMemoryPair()
}
listLiveData.add(memoryPair.first)
if (memoryPair.second != null) {
ref().storageItemList.add(memoryPair.second as StorageItem)
listLiveData.add(memoryPair.second as StorageItem)
}
refreshSdCard()
}
@@ -103,15 +105,15 @@ class StorageInfoViewModel @Inject constructor(private val resources: Resources)
val sdUsed = sdTotal - sdCardFile.usableSpace
val sdMemory = StorageItem(resources.getString(R.string.external),
R.drawable.sdcard, sdTotal, sdUsed)
storageItemList.add(sdMemory)
listLiveData.add(sdMemory)
}
}, {
Timber.i("Cannot find SD card file")
val storageItem = storageItemList.find {
it.iconRes == R.drawable.sdcard
val storageItem = listLiveData.find { storageItem ->
storageItem.iconRes == R.drawable.sdcard
}
if (storageItem != null) {
storageItemList.remove(storageItem)
listLiveData.remove(storageItem)
}
})
}
@@ -158,7 +160,7 @@ class StorageInfoViewModel @Inject constructor(private val resources: Resources)
* @return [Single] with the SD card file or onError in case of missing that one.
*/
private fun getSDCardFinder(): Single<File> {
return Single.create({ emitter: SingleEmitter<File> ->
return Single.create { emitter: SingleEmitter<File> ->
val mountedList = getExternalSDMounts()
var strSDCardPath: String? = null
@@ -173,15 +175,15 @@ class StorageInfoViewModel @Inject constructor(private val resources: Resources)
val externalFilePath = File(strSDCardPath)
if (externalFilePath.exists()) {
if (externalFilePath.exists() && !emitter.isDisposed) {
emitter.onSuccess(externalFilePath)
} else {
} else if (!emitter.isDisposed) {
emitter.onError(FileNotFoundException("Cannot find SD card file"))
}
} else if (!emitter.isDisposed) {
emitter.onError(FileNotFoundException("Cannot find SD card file"))
}
})
}
}
/**
@@ -17,14 +17,12 @@
package com.kgurgul.cpuinfo.features.processes
import android.annotation.SuppressLint
import android.databinding.ObservableList
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.ObservableListAdapter
import com.kgurgul.cpuinfo.utils.Utils
/**
@@ -32,8 +30,8 @@ import com.kgurgul.cpuinfo.utils.Utils
*
* @author kgurgul
*/
class ProcessesAdapter(private val processList: ObservableList<ProcessItem>)
: ObservableListAdapter<ProcessItem, ProcessesAdapter.ViewHolder>(processList) {
class ProcessesAdapter(private val processList: List<ProcessItem>)
: RecyclerView.Adapter<ProcessesAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_process, parent, false)
@@ -16,20 +16,20 @@
package com.kgurgul.cpuinfo.features.processes
import android.arch.lifecycle.ViewModelProvider
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.SimpleItemAnimator
import android.view.*
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.list.DividerItemDecoration
import com.kgurgul.cpuinfo.databinding.FragmentProcessesBinding
import com.kgurgul.cpuinfo.di.Injectable
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.utils.AutoClearedValue
import com.kgurgul.cpuinfo.utils.DividerItemDecoration
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import com.kgurgul.cpuinfo.utils.viewModelProvider
import javax.inject.Inject
/**
@@ -43,35 +43,37 @@ class ProcessesFragment : Fragment(), Injectable {
lateinit var viewModelInjectionFactory: ViewModelInjectionFactory<ProcessesViewModel>
private lateinit var viewModel: ProcessesViewModel
private lateinit var binding: AutoClearedValue<FragmentProcessesBinding>
private lateinit var processesAdapter: AutoClearedValue<ProcessesAdapter>
private lateinit var binding: FragmentProcessesBinding
private lateinit var processesAdapter: ProcessesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(ProcessesViewModel::class.java)
viewModel = viewModelProvider(viewModelInjectionFactory)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = AutoClearedValue(this,
DataBindingUtil.inflate(inflater, R.layout.fragment_processes, container, false))
binding.get().viewModel = viewModel
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_processes, container,
false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
setupRecyclerView()
return binding.get().root
return binding.root
}
/**
* Setup for [RecyclerView]
*/
private fun setupRecyclerView() {
processesAdapter = AutoClearedValue(this, ProcessesAdapter(viewModel.processList))
processesAdapter = ProcessesAdapter(viewModel.processList)
viewModel.processList.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(processesAdapter))
val rvLayoutManager = LinearLayoutManager(context)
binding.get().apply {
binding.apply {
recyclerView.layoutManager = rvLayoutManager
recyclerView.adapter = processesAdapter.get()
recyclerView.adapter = processesAdapter
recyclerView.addItemDecoration(DividerItemDecoration(requireContext()))
(recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}
@@ -79,13 +81,11 @@ class ProcessesFragment : Fragment(), Injectable {
override fun onStart() {
super.onStart()
processesAdapter.get().registerListChangeNotifier()
viewModel.startProcessRefreshing()
}
override fun onStop() {
viewModel.stopProcessRefreshing()
processesAdapter.get().unregisterListChangeNotifier()
super.onStop()
}
@@ -16,19 +16,18 @@
package com.kgurgul.cpuinfo.features.processes
import android.arch.lifecycle.ViewModel
import android.support.annotation.VisibleForTesting
import com.kgurgul.cpuinfo.common.Prefs
import com.kgurgul.cpuinfo.common.list.AdapterArrayList
import androidx.annotation.VisibleForTesting
import com.kgurgul.cpuinfo.utils.DispatchersProvider
import com.kgurgul.cpuinfo.utils.Prefs
import com.kgurgul.cpuinfo.utils.ScopedViewModel
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import org.jetbrains.anko.coroutines.experimental.asReference
import org.jetbrains.anko.coroutines.experimental.bg
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -38,16 +37,17 @@ import javax.inject.Inject
*
* @author kgurgul
*/
class ProcessesViewModel @Inject constructor(private val prefs: Prefs,
private val psProvider: PsProvider) : ViewModel() {
class ProcessesViewModel @Inject constructor(
private val dispatchersProvider: DispatchersProvider,
private val prefs: Prefs,
private val psProvider: PsProvider) : ScopedViewModel(dispatchersProvider) {
companion object {
private const val SORTING_PROCESSES_KEY = "SORTING_PROCESSES_KEY"
}
val processList = AdapterArrayList<ProcessItem>()
val processList = ListLiveData<ProcessItem>()
private val ref = asReference()
private var isSortingAsc = prefs.get(SORTING_PROCESSES_KEY, true)
private var refreshingDisposable: Disposable? = null
@@ -87,10 +87,11 @@ class ProcessesViewModel @Inject constructor(private val prefs: Prefs,
* Change process list sorting type from ascending to descending or or vice versa
*/
fun changeProcessSorting() {
async(UI) {
val result = bg { getProcessSortedList(!isSortingAsc) }
val sortedAppList = result.await()
ref().processList.replace(sortedAppList)
launch {
val sortedAppList = withContext(dispatchersProvider.ioDispatcher) {
getProcessSortedList(!isSortingAsc)
}
processList.replace(sortedAppList)
}
}
@@ -16,7 +16,7 @@
package com.kgurgul.cpuinfo.features.processes
import android.support.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting
import io.reactivex.Single
import timber.log.Timber
import java.util.*
@@ -47,10 +47,10 @@ class PsProvider @Inject constructor() {
* exception
*/
fun getPsList(): Single<List<ProcessItem>> {
return Single.fromCallable({
return Single.fromCallable {
val psCmdList = readPsCmd()
return@fromCallable parsePs(psCmdList)
})
}
}
/**
@@ -70,10 +70,10 @@ class RamUsageWidgetProvider : AppWidgetProvider() {
}
// Won't work on Android O!
runOnApiBelow(Build.VERSION_CODES.O, {
runOnApiBelow(Build.VERSION_CODES.O) {
val intent = Intent(context, RefreshService::class.java)
context.startService(intent)
})
}
}
super.onUpdate(context, appWidgetManager, appWidgetIds)
@@ -59,14 +59,14 @@ class RefreshService : Service() {
powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
prefs = PreferenceManager.getDefaultSharedPreferences(this)
ramUpdateDelay = prefs.getString(SettingsFragment.KEY_RAM_REFRESHING, "10000").toLong()
ramUpdateDelay = prefs.getString(SettingsFragment.KEY_RAM_REFRESHING,
"10000")!!.toLong()
refreshHandler = Handler()
refreshHandler!!.postDelayed(object : Runnable {
refreshHandler?.postDelayed(object : Runnable {
override fun run() {
ramUpdateDelay =
prefs.getString(SettingsFragment.KEY_RAM_REFRESHING, "10000").toLong()
ramUpdateDelay = prefs.getString(SettingsFragment.KEY_RAM_REFRESHING,
"10000")!!.toLong()
val isDeviceActive: Boolean =
if (Build.VERSION.SDK_INT >= 20) {
powerManager.isInteractive
@@ -88,9 +88,9 @@ class RefreshService : Service() {
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetId)
this@RefreshService.sendBroadcast(intent)
refreshHandler!!.postDelayed(this, ramUpdateDelay)
refreshHandler?.postDelayed(this, ramUpdateDelay)
} else {
refreshHandler!!.postDelayed(this, RAM_BACKGROUND_DELAY)
refreshHandler?.postDelayed(this, RAM_BACKGROUND_DELAY)
}
}
}, ramUpdateDelay)
@@ -109,14 +109,13 @@ class RefreshService : Service() {
}
@Suppress("UNUSED_PARAMETER")
@Suppress("unused", "UNUSED_PARAMETER")
@Subscribe
fun killServiceEvent(event: KillRefreshServiceEvent) {
stopSelf()
}
override fun onBind(intent: Intent?): IBinder? =
null
override fun onBind(intent: Intent?): IBinder? = null
class KillRefreshServiceEvent
}
@@ -19,9 +19,9 @@ package com.kgurgul.cpuinfo.features.settings
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.support.v7.preference.ListPreference
import android.support.v7.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceFragmentCompat
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.utils.ThemeHelper
import com.kgurgul.cpuinfo.utils.runOnApiAbove
/**
@@ -35,33 +35,28 @@ class SettingsFragment : PreferenceFragmentCompat(),
companion object {
const val KEY_TEMPERATURE_UNIT = "temperature_unit"
const val KEY_RAM_REFRESHING = "ram_refreshing"
const val KEY_THEME_CONFIG = "key_theme"
private const val KEY_RAM_CATEGORIES = "pref_key_ram_settings"
}
private lateinit var temperatureUnitPreference: ListPreference
private lateinit var ramRefreshingPreference: ListPreference
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
temperatureUnitPreference = preferenceScreen.findPreference(KEY_TEMPERATURE_UNIT)
as ListPreference
ramRefreshingPreference = preferenceScreen.findPreference(KEY_RAM_REFRESHING)
as ListPreference
// RAM widget isn't supported currently on O and above
runOnApiAbove(Build.VERSION_CODES.N_MR1, {
runOnApiAbove(Build.VERSION_CODES.N_MR1) {
preferenceScreen.removePreference(preferenceScreen.findPreference(KEY_RAM_CATEGORIES))
})
}
}
override fun onResume() {
super.onResume()
temperatureUnitPreference.summary = temperatureUnitPreference.entry.toString()
ramRefreshingPreference.summary = ramRefreshingPreference.entry.toString()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
@@ -73,10 +68,11 @@ class SettingsFragment : PreferenceFragmentCompat(),
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
KEY_TEMPERATURE_UNIT ->
temperatureUnitPreference.summary = temperatureUnitPreference.entry.toString()
KEY_RAM_REFRESHING ->
ramRefreshingPreference.summary = ramRefreshingPreference.entry.toString()
KEY_THEME_CONFIG -> {
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
ThemeHelper.applyTheme(sharedPreferences.getString(ThemeHelper.KEY_THEME,
ThemeHelper.DEFAULT_MODE))
}
}
}
}
@@ -16,9 +16,9 @@
package com.kgurgul.cpuinfo.features.temperature
import com.kgurgul.cpuinfo.common.Prefs
import com.kgurgul.cpuinfo.di.FragmentScope
import com.kgurgul.cpuinfo.features.settings.SettingsFragment
import com.kgurgul.cpuinfo.utils.Prefs
import com.kgurgul.cpuinfo.utils.round2
import javax.inject.Inject
@@ -16,24 +16,21 @@
package com.kgurgul.cpuinfo.features.temperature
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProvider
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SimpleItemAnimator
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.databinding.FragmentTemperatureBinding
import com.kgurgul.cpuinfo.di.Injectable
import com.kgurgul.cpuinfo.di.ViewModelInjectionFactory
import com.kgurgul.cpuinfo.features.temperature.list.TemperatureAdapter
import com.kgurgul.cpuinfo.features.temperature.list.TemperatureItem
import com.kgurgul.cpuinfo.utils.AutoClearedValue
import timber.log.Timber
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveDataObserver
import com.kgurgul.cpuinfo.utils.viewModelProvider
import javax.inject.Inject
/**
@@ -48,24 +45,26 @@ class TemperatureFragment : Fragment(), Injectable {
lateinit var viewModelInjectionFactory: ViewModelInjectionFactory<TemperatureViewModel>
@Inject
lateinit var temperatureAdapter: AutoClearedValue<TemperatureAdapter>
lateinit var temperatureFormatter: TemperatureFormatter
private lateinit var viewModel: TemperatureViewModel
private lateinit var binding: AutoClearedValue<FragmentTemperatureBinding>
private lateinit var binding: FragmentTemperatureBinding
private lateinit var temperatureAdapter: TemperatureAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelInjectionFactory)
.get(TemperatureViewModel::class.java)
viewModel = viewModelProvider(viewModelInjectionFactory)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = AutoClearedValue(this,
DataBindingUtil.inflate(inflater, R.layout.fragment_temperature, container, false))
binding.get().viewModel = viewModel
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_temperature, container,
false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
setupRecycleView()
return binding.value?.root
return binding.root
}
override fun onStart() {
@@ -82,16 +81,14 @@ class TemperatureFragment : Fragment(), Injectable {
* Set all necessary data for [android.support.v7.widget.RecyclerView]
*/
private fun setupRecycleView() {
val layoutManager = LinearLayoutManager(context)
binding.get().apply {
tempRv.layoutManager = layoutManager
tempRv.adapter = temperatureAdapter.get()
temperatureAdapter = TemperatureAdapter(temperatureFormatter,
viewModel.temperatureListLiveData)
viewModel.temperatureListLiveData.listStatusChangeNotificator.observe(viewLifecycleOwner,
ListLiveDataObserver(temperatureAdapter))
binding.apply {
tempRv.layoutManager = LinearLayoutManager(context)
tempRv.adapter = temperatureAdapter
(tempRv.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}
viewModel.temperatureItemsLiveData.observe(this, Observer<List<TemperatureItem>> { tempList ->
Timber.i("temperatureItemsLiveData observer refreshed")
temperatureAdapter.get().setTempItems(tempList)
})
}
}
@@ -1,39 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.features.temperature
import com.kgurgul.cpuinfo.di.FragmentScope
import com.kgurgul.cpuinfo.features.temperature.list.TemperatureAdapter
import com.kgurgul.cpuinfo.utils.AutoClearedValue
import dagger.Module
import dagger.Provides
/**
* Provides instances for [TemperatureFragment]
*
* @author kgurgul
*/
@Module
class TemperatureModule {
@Provides
@FragmentScope
fun provideAutoClearedTempAdapter(temperatureFragment: TemperatureFragment,
temperatureFormatter: TemperatureFormatter):
AutoClearedValue<TemperatureAdapter> =
AutoClearedValue(temperatureFragment, TemperatureAdapter(temperatureFormatter))
}
@@ -41,7 +41,8 @@ class TemperatureProvider @Inject constructor(val app: Application) {
fun getBatteryTemperature(): Int {
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = app.registerReceiver(null, iFilter)
return batteryStatus.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) / 10
return (batteryStatus?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE,
0) ?: 0) / 10
}
/**
@@ -16,14 +16,14 @@
package com.kgurgul.cpuinfo.features.temperature
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import android.content.res.Resources
import android.databinding.ObservableBoolean
import android.support.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.ViewModel
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.common.Prefs
import com.kgurgul.cpuinfo.features.temperature.list.TemperatureItem
import com.kgurgul.cpuinfo.utils.NonNullMutableLiveData
import com.kgurgul.cpuinfo.utils.Prefs
import com.kgurgul.cpuinfo.utils.lifecycleawarelist.ListLiveData
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@@ -49,10 +49,10 @@ class TemperatureViewModel @Inject constructor(
}
// Binding fields
val isLoading = ObservableBoolean(false)
val isError = ObservableBoolean(false)
val isLoading = NonNullMutableLiveData(false)
val isError = NonNullMutableLiveData(false)
val temperatureItemsLiveData = MutableLiveData<List<TemperatureItem>>()
val temperatureListLiveData = ListLiveData<TemperatureItem>()
private var temperatureDisposable: Disposable? = null
private var refreshingDisposable: Disposable? = null
@@ -90,11 +90,11 @@ class TemperatureViewModel @Inject constructor(
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
isLoading.set(true)
isError.set(false)
isLoading.value = true
isError.value = false
}
.doFinally {
isLoading.set(false)
isLoading.value = false
verifyTemperaturesAvailability()
}
.subscribe({ temperatureResult ->
@@ -118,7 +118,7 @@ class TemperatureViewModel @Inject constructor(
if (isBatteryTemperatureAvailable || cpuTemperatureResult != null) {
scheduleRefreshing()
} else {
isError.set(true)
isError.value = true
}
}
@@ -160,7 +160,7 @@ class TemperatureViewModel @Inject constructor(
batteryTemp.toFloat()))
}
temperatureItemsLiveData.value = temporaryTempList
temperatureListLiveData.replace(temporaryTempList)
}, Timber::e)
}
@@ -16,55 +16,45 @@
package com.kgurgul.cpuinfo.features.temperature.list
import android.databinding.DataBindingUtil
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.kgurgul.cpuinfo.R
import com.kgurgul.cpuinfo.databinding.ItemTemperatureBinding
import com.kgurgul.cpuinfo.features.temperature.TemperatureFormatter
import kotlinx.android.synthetic.main.item_temperature.view.*
/**
* Temperature list adapter which observe temperatureItemsLiveData list
* Temperature list adapter which observe temperatureListLiveData
*
* @author kgurgul
*/
class TemperatureAdapter(private val temperatureFormatter: TemperatureFormatter)
: RecyclerView.Adapter<TemperatureAdapter.BindingViewHolder>() {
class TemperatureAdapter(
private val temperatureFormatter: TemperatureFormatter,
private val temperatureList: List<TemperatureItem>)
: RecyclerView.Adapter<TemperatureAdapter.ViewHolder>() {
private var temperatureItems: List<TemperatureItem>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
val viewHolderBinding = DataBindingUtil.inflate<ItemTemperatureBinding>(
LayoutInflater.from(parent.context),
R.layout.item_temperature,
parent,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_temperature, parent,
false)
return BindingViewHolder(viewHolderBinding)
return ViewHolder(view, temperatureFormatter)
}
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
val temperatureItem = temperatureItems!![position]
holder.bindTemperatureItem(temperatureItem, temperatureFormatter)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(temperatureList[position])
}
override fun getItemCount(): Int = temperatureItems?.size ?: 0
override fun getItemCount(): Int = temperatureList.size
fun setTempItems(tempItems: List<TemperatureItem>?) {
temperatureItems = tempItems
notifyDataSetChanged()
}
class ViewHolder(
private val view: View,
private val temperatureFormatter: TemperatureFormatter)
: RecyclerView.ViewHolder(view) {
class BindingViewHolder(private val binding: ItemTemperatureBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bindTemperatureItem(temperatureItem: TemperatureItem,
temperatureFormatter: TemperatureFormatter) {
if (binding.viewModel == null) {
binding.viewModel = TemperatureItemViewModel(temperatureItem, temperatureFormatter)
} else {
binding.viewModel!!.setTempItem(temperatureItem)
}
fun bind(temperatureItem: TemperatureItem) {
view.temperatureIv.setImageResource(temperatureItem.iconRes)
view.temperatureTypeTv.text = temperatureItem.name
view.temperatureTv.text = temperatureFormatter.format(temperatureItem.temperature)
}
}
}
@@ -1,54 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.features.temperature.list
import android.databinding.BaseObservable
import android.databinding.BindingAdapter
import android.widget.ImageView
import com.kgurgul.cpuinfo.features.temperature.TemperatureFormatter
/**
* ViewModel for temperature item
*
* @author kgurgul
*/
class TemperatureItemViewModel(
private var temperatureItem: TemperatureItem,
private val temperatureFormatter: TemperatureFormatter) : BaseObservable() {
companion object {
@JvmStatic
@BindingAdapter("srcCompat")
fun loadImage(imageView: ImageView, image: Int) {
imageView.setImageResource(image)
}
}
fun setTempItem(temperatureItem: TemperatureItem) {
this.temperatureItem = temperatureItem
notifyChange()
}
fun getUnitIcon(): Int =
temperatureItem.iconRes
fun getUnitName(): String =
temperatureItem.name
fun getTempValue(): String =
temperatureFormatter.format(temperatureItem.temperature)
}
@@ -1,43 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
/**
* Wrapper which automatically release resources
*
* @author kgurgul
*/
class AutoClearedValue<T>(fragment: Fragment, var value: T?) {
init {
val fragmentManager = fragment.fragmentManager
fragmentManager?.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) {
if (fragment == f) {
this@AutoClearedValue.value = null
fragmentManager.unregisterFragmentLifecycleCallbacks(this)
}
}
}, false)
}
fun get(): T = value!!
}
@@ -14,18 +14,6 @@
* limitations under the License.
*/
package com.kgurgul.cpuinfo.analytics
package com.kgurgul.cpuinfo.utils
/**
* Container for analytics events
*
* @author kgurgul
*/
object AnalyticsEvents {
// Screen names
val INFO_SCREEN = "Info Screen"
val PROCESSES_SCREEN = "Processes Screen"
val APPLICATIONS_SCREEN = "Applications Screen"
val TEMPERATURE_SCREEN = "Temperature Screen"
val SETTINGS_SCREEN = "Settings Screen"
}
const val MIME_TEXT_PLAIN = "text/plain"
@@ -0,0 +1,17 @@
package com.kgurgul.cpuinfo.utils
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import javax.inject.Inject
import javax.inject.Singleton
/**
* Dispatchers used by coroutines
*
* @author kgurgul
*/
@Singleton
class DispatchersProvider @Inject constructor() {
val mainDispatcher: CoroutineDispatcher = Dispatchers.Main
val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
}
@@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.kgurgul.cpuinfo.common.list
package com.kgurgul.cpuinfo.utils
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
/**
* Customized version of internal [RecyclerView] decorator
@@ -40,7 +40,7 @@ class DividerItemDecoration : RecyclerView.ItemDecoration {
*/
constructor(context: Context) {
val styledAttributes = context.obtainStyledAttributes(ATTRS)
mDivider = styledAttributes.getDrawable(0)
mDivider = styledAttributes.getDrawable(0)!!
styledAttributes.recycle()
}
@@ -51,7 +51,7 @@ class DividerItemDecoration : RecyclerView.ItemDecoration {
mDivider = ContextCompat.getDrawable(context, resId)!!
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State?) {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft
val right = parent.width - parent.paddingRight
@@ -18,8 +18,17 @@
package com.kgurgul.cpuinfo.utils
import android.annotation.TargetApi
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import com.kgurgul.cpuinfo.BuildConfig
import com.kgurgul.cpuinfo.R
/**
* All basic extensions
@@ -42,6 +51,18 @@ inline fun runOnApi(api: Int, f: () -> Unit, otherwise: () -> Unit = {}) {
}
}
inline fun runOnApiBelow(api: Int, f: () -> Unit) {
if (Build.VERSION.SDK_INT < api) {
f()
}
}
inline fun runOnApiAbove(api: Int, f: () -> Unit) {
if (Build.VERSION.SDK_INT > api) {
f()
}
}
inline fun runOnApiBelow(api: Int, f: () -> Unit, otherwise: () -> Unit = {}) {
if (Build.VERSION.SDK_INT < api) {
f()
@@ -62,3 +83,38 @@ inline fun runOnApiAbove(api: Int, f: () -> Unit, otherwise: () -> Unit = {}) {
* @return true for Debug build, otherwise false
*/
fun isDebugBuild(): Boolean = BuildConfig.DEBUG
/**
* @return true if used device is tablet
*/
fun Context.isTablet(): Boolean = this.resources.getBoolean(R.bool.isTablet)
/**
* For Actvities, allows declarations like
* ```
* val myViewModel = viewModelProvider(myViewModelFactory)
* ```
*/
inline fun <reified VM : ViewModel> FragmentActivity.viewModelProvider(
provider: ViewModelProvider.Factory) =
ViewModelProviders.of(this, provider).get(VM::class.java)
/**
* For Fragments, allows declarations like
* ```
* val myViewModel = viewModelProvider(myViewModelFactory)
* ```
*/
inline fun <reified VM : ViewModel> Fragment.viewModelProvider(
provider: ViewModelProvider.Factory) =
ViewModelProviders.of(this, provider).get(VM::class.java)
@TargetApi(19)
fun Fragment.createSafFile(mimeType: String, fileName: String, requestCode: Int) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, fileName)
}
startActivityForResult(intent, requestCode)
}
@@ -0,0 +1,116 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils
import android.os.Bundle
import android.os.Handler
import android.view.Menu
import android.view.MenuItem
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavOptions
import com.google.android.material.navigation.NavigationView
import java.lang.ref.WeakReference
/**
* Modified version of NavigationUI class
*/
object NavigationUtils {
private const val FRAGMENT_OPEN_DELAY = 300L
/**
* Setup [NavigationView] for passed [navController]
*/
fun setupNavigationView(navigationView: NavigationView, navController: NavController) {
navigationView.setNavigationItemSelectedListener { item ->
val handled = onNavDestinationSelected(item, navController)
if (handled) {
val parent = navigationView.parent
if (parent is DrawerLayout) {
parent.closeDrawer(navigationView)
}
}
handled
}
val weakReference = WeakReference<NavigationView>(navigationView)
navController.addOnDestinationChangedListener(
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(controller: NavController,
destination: NavDestination,
arguments: Bundle?) {
val view = weakReference.get()
if (view == null) {
navController.removeOnDestinationChangedListener(this)
return
}
val menu = view.menu
var h = 0
val size = menu.size()
while (h < size) {
val item = menu.getItem(h)
item.isChecked = matchDestination(destination, item.itemId)
h++
}
}
})
}
private fun onNavDestinationSelected(item: MenuItem, navController: NavController): Boolean {
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
findStartDestination(navController.graph)?.let {
builder.setPopUpTo(it.id, false)
}
}
val options = builder.build()
return try {
Handler().postDelayed({ navController.navigate(item.itemId, null, options) },
FRAGMENT_OPEN_DELAY)
true
} catch (e: IllegalArgumentException) {
false
}
}
/**
* Finds the actual start destination of the graph, handling cases where the graph's starting
* destination is itself a NavGraph.
*/
private fun findStartDestination(graph: NavGraph): NavDestination? {
var startDestination: NavDestination? = graph
while (startDestination is NavGraph) {
startDestination = startDestination.findNode(startDestination.startDestination)
}
return startDestination
}
/**
* Determines whether the given `destId` matches the NavDestination. This handles
* both the default case (the destination's id matches the given id) and the nested case where
* the given id is a parent/grandparent/etc of the destination.
*/
private fun matchDestination(destination: NavDestination, destId: Int): Boolean {
var currentDestination: NavDestination = destination
while (currentDestination.id != destId && currentDestination.parent != null) {
currentDestination = currentDestination.parent as NavDestination
}
return currentDestination.id == destId
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils
import androidx.lifecycle.MutableLiveData
/**
* [MutableLiveData] with default value support.
*
* @author kgurgul
*/
class NonNullMutableLiveData<T>(defaultValue: T) : MutableLiveData<T>() {
init {
value = defaultValue
}
override fun getValue(): T {
return super.getValue()!!
}
override fun setValue(value: T) {
value ?: throw NullPointerException("Cannot set null value")
super.setValue(value)
}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.kgurgul.cpuinfo.common
package com.kgurgul.cpuinfo.utils
import android.content.SharedPreferences
import com.google.gson.Gson
@@ -65,7 +65,7 @@ class Prefs @Inject constructor(private val sharedPreferences: SharedPreferences
is Long -> return sharedPreferences.getLong(key, default) as T
else -> {
val value = sharedPreferences.getString(key, "")
if (value.isNotEmpty()) {
if (!value.isNullOrEmpty()) {
val typedObject = default as Any
return Gson().fromJson(value, typedObject.javaClass) as T
}
@@ -0,0 +1,26 @@
package com.kgurgul.cpuinfo.utils
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
/**
* ViewModel with [CoroutineScope] which is responsible for job lifecycle
*
* @author kgurgul
*/
open class ScopedViewModel(
private val dispatchersProvider: DispatchersProvider
) : ViewModel(), CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + dispatchersProvider.mainDispatcher
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
@@ -1,76 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils
import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.Observer
import android.support.annotation.MainThread
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
* Note that only one observer is going to be notified of changes.
*
* Fork from: https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp
* /app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
@MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private const val TAG = "SingleLiveEvent"
}
}
@@ -0,0 +1,35 @@
package com.kgurgul.cpuinfo.utils
import androidx.appcompat.app.AppCompatDelegate
/**
* Helper class to set app theme
*
* @author kgurgul
*/
object ThemeHelper {
const val KEY_THEME = "key_theme"
const val LIGHT_MODE = "light"
const val DARK_MODE = "dark"
const val DEFAULT_MODE = "default"
fun applyTheme(themePref: String) {
when (themePref) {
LIGHT_MODE -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
DARK_MODE -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
else -> {
runOnApiAbove(28, {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}, {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
})
}
}
}
}
@@ -19,7 +19,6 @@ package com.kgurgul.cpuinfo.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
@@ -38,9 +37,9 @@ object Utils {
/**
* Helper which adds item to list if value is not null and not empty
*/
fun addPairIfExists(array: ArrayList<Pair<String, String>>, key: String, value: String?) {
fun addPairIfExists(list: MutableList<Pair<String, String>>, key: String, value: String?) {
if (value != null && !value.isEmpty()) {
array.add(Pair(key, value))
list.add(Pair(key, value))
}
}
@@ -101,16 +100,4 @@ object Utils {
return value
}
/**
* Get device hardware info like: Manufacturer, Brand, Model, Serial
*
* @return ID concatenated hardware info
*/
fun getHardwareInfo(): String {
return Build.MANUFACTURER + " " +
Build.BRAND + " " +
Build.MODEL + " " +
Build.VERSION.SDK_INT
}
}
@@ -1,60 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils.glide
import android.content.Context
import android.content.pm.ApplicationInfo
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.drawable.DrawableResource
import com.bumptech.glide.util.Util
/**
* Decoder for application icons based on:
* https://gist.github.com/csshuai/9ccab646e35194eddafca9929cd9ad01
*
* @author kgurgul
*/
class ApplicationIconDecoder(private val context: Context) :
ResourceDecoder<ApplicationInfo, Drawable> {
override fun decode(source: ApplicationInfo, width: Int, height: Int, options: Options):
Resource<Drawable> {
val icon = source.loadIcon(context.packageManager)
return object : DrawableResource<Drawable>(icon) {
override fun getResourceClass(): Class<Drawable> = Drawable::class.java
override fun getSize(): Int {
return if (drawable is BitmapDrawable) {
Util.getBitmapByteSize(drawable.bitmap)
} else {
1
}
}
override fun recycle() {
}
}
}
override fun handles(source: ApplicationInfo, options: Options): Boolean =
true
}
@@ -1,60 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils.glide
import android.content.pm.ApplicationInfo
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.signature.ObjectKey
/**
* Loader for application icons decoder module based on:
* https://gist.github.com/csshuai/9ccab646e35194eddafca9929cd9ad01
*
* @author kgurgul
*/
class ApplicationIconModelLoader : ModelLoader<ApplicationInfo, ApplicationInfo> {
override fun buildLoadData(applicationInfo: ApplicationInfo?, width: Int, height: Int,
options: Options?): ModelLoader.LoadData<ApplicationInfo>? {
return ModelLoader.LoadData(ObjectKey(applicationInfo),
object : DataFetcher<ApplicationInfo> {
override fun loadData(priority: Priority,
callback: DataFetcher.DataCallback<in ApplicationInfo>) {
callback.onDataReady(applicationInfo)
}
override fun cleanup() {
}
override fun cancel() {
}
override fun getDataClass(): Class<ApplicationInfo> =
ApplicationInfo::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
})
}
override fun handles(model: ApplicationInfo?): Boolean =
true
}
@@ -1,54 +0,0 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils.glide
import android.content.Context
import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.module.LibraryGlideModule
/**
* Module for application icons decoding based on:
* https://gist.github.com/csshuai/9ccab646e35194eddafca9929cd9ad01
*
* @author kgurgul
*/
@GlideModule
class ApplicationIconModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.append(ApplicationInfo::class.java, ApplicationInfo::class.java,
object : ModelLoaderFactory<ApplicationInfo, ApplicationInfo> {
override fun build(
multiFactory: MultiModelLoaderFactory):
ModelLoader<ApplicationInfo, ApplicationInfo> =
ApplicationIconModelLoader()
override fun teardown() {
}
})
.append(ApplicationInfo::class.java, Drawable::class.java,
ApplicationIconDecoder(context))
}
}
@@ -0,0 +1,137 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils.lifecycleawarelist
import androidx.lifecycle.MutableLiveData
import java.util.*
/**
* [ArrayList] with additional [listStatusChangeNotificator] which will notify all observers about
* changes in that list. All operations MUST be invoked from main thread to avoid inconsistency.
*
* In addition to normal [ArrayList] it contains [replace] method to make nicer animations with
* [androidx.recyclerview.widget.RecyclerView].
*
* @author kgurgul
*/
class ListLiveData<T> : ArrayList<T>() {
val listStatusChangeNotificator = MutableLiveData<ListLiveDataChangeEvent>()
override fun add(element: T): Boolean {
super.add(element)
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_INSERTED,
startIndex = size - 1,
itemCount = 1
)
return true
}
override fun add(index: Int, element: T) {
super.add(index, element)
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_INSERTED,
startIndex = index,
itemCount = 1
)
}
override fun addAll(elements: Collection<T>): Boolean {
val oldSize = size
val added = super.addAll(elements)
if (added) {
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_INSERTED,
startIndex = oldSize,
itemCount = size - oldSize
)
}
return added
}
override fun addAll(index: Int, elements: Collection<T>): Boolean {
val added = super.addAll(index, elements)
if (added) {
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_INSERTED,
startIndex = index,
itemCount = elements.size
)
}
return added
}
override fun clear() {
val oldSize = size
super.clear()
if (oldSize != 0) {
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_REMOVED,
startIndex = 0,
itemCount = oldSize
)
}
}
override fun removeAt(index: Int): T {
val value = super.removeAt(index)
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_REMOVED,
startIndex = index,
itemCount = 1
)
return value
}
override fun remove(element: T): Boolean {
val index = indexOf(element)
return if (index >= 0) {
removeAt(index)
true
} else {
false
}
}
override fun set(index: Int, element: T): T {
val value = super.set(index, element)
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_CHANGED,
startIndex = index,
itemCount = 1
)
return value
}
fun replace(elements: Collection<T>) {
super.clear()
super.addAll(elements)
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.CHANGED
)
}
override fun removeRange(fromIndex: Int, toIndex: Int) {
super.removeRange(fromIndex, toIndex)
listStatusChangeNotificator.value = ListLiveDataChangeEvent(
listLiveDataState = ListLiveDataState.ITEM_RANGE_REMOVED,
startIndex = fromIndex,
itemCount = toIndex - fromIndex
)
}
}
@@ -14,23 +14,14 @@
* limitations under the License.
*/
object Versions {
// Build sdk
val minSdk = 14
val targetSdk = 27
val compiledSdk = 27
val buildTools = "27.0.3"
package com.kgurgul.cpuinfo.utils.lifecycleawarelist
// Kotlin
val kotlin = "1.2.31"
val anko = "0.10.1"
// Libs
val androidSupport = "27.1.1"
val androidDatabinding = "2.3.0"
val firebase = "15.0.0"
val archLifecycle = "1.1.1"
val dagger = "2.15"
val glide = "4.3.1"
val espresso = "3.0.1"
}
/**
* Wrapper for [ListLiveData] changes which contains type of the change [ListLiveDataState], index
* of the changes and count of affected items.
*
* @author kgurgul
*/
data class ListLiveDataChangeEvent(val listLiveDataState: ListLiveDataState,
val startIndex: Int = -1,
val itemCount: Int = -1)
@@ -0,0 +1,45 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils.lifecycleawarelist
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView
/**
* [android.arch.lifecycle.LiveData] observer for [ListLiveData] which will notify [adapter] about
* list changes.
*
* @author kgurgul
*/
class ListLiveDataObserver(val adapter: RecyclerView.Adapter<*>)
: Observer<ListLiveDataChangeEvent> {
override fun onChanged(changeEvent: ListLiveDataChangeEvent?) {
changeEvent?.let {
when (it.listLiveDataState) {
ListLiveDataState.CHANGED ->
adapter.notifyDataSetChanged()
ListLiveDataState.ITEM_RANGE_CHANGED ->
adapter.notifyItemRangeChanged(it.startIndex, it.itemCount)
ListLiveDataState.ITEM_RANGE_INSERTED ->
adapter.notifyItemRangeInserted(it.startIndex, it.itemCount)
ListLiveDataState.ITEM_RANGE_REMOVED ->
adapter.notifyItemRangeRemoved(it.startIndex, it.itemCount)
}
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils.lifecycleawarelist
/**
* Representation of all possible list state changes
*
* @author kgurgul
*/
enum class ListLiveDataState {
CHANGED, // change of unknown type has occurred
ITEM_RANGE_CHANGED, // one or more items in the list have changed
ITEM_RANGE_INSERTED, // items have been inserted into the list
ITEM_RANGE_REMOVED // items in the list have been deleted
}
@@ -0,0 +1,78 @@
/*
* Copyright 2017 KG Soft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kgurgul.cpuinfo.utils.wrappers
import androidx.lifecycle.Observer
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
override fun toString(): String {
return content.toString()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Event<*>
if (content != other.content) return false
return true
}
override fun hashCode(): Int {
return content?.hashCode() ?: 0
}
}
/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
* already been handled.
*
* [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
@@ -17,7 +17,6 @@
package com.kgurgul.cpuinfo.widgets.swiperv;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
@@ -30,6 +29,8 @@ import com.kgurgul.cpuinfo.widgets.swiperv.swiper.LeftHorizontalSwiper;
import com.kgurgul.cpuinfo.widgets.swiperv.swiper.RightHorizontalSwiper;
import com.kgurgul.cpuinfo.widgets.swiperv.swiper.Swiper;
import androidx.core.view.ViewCompat;
public class SwipeHorizontalMenuLayout extends SwipeMenuLayout {
protected int mPreScrollX;
@@ -17,7 +17,6 @@
package com.kgurgul.cpuinfo.widgets.swiperv;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@@ -26,10 +25,13 @@ import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
import androidx.core.view.ViewCompat;
/**
* Created by tubingbing on 16/8/11.
*/
@SuppressWarnings("WeakerAccess")
public class SwipeMenuHelper {
public static final int INVALID_POSITION = -1;
@@ -17,11 +17,12 @@
package com.kgurgul.cpuinfo.widgets.swiperv;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
public class SwipeMenuRecyclerView extends RecyclerView implements SwipeMenuHelper.Callback {
protected SwipeMenuHelper mHelper;
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/accent" android:state_checked="true" />
<item android:color="?attr/colorOnBackground" android:state_checked="false" />
</selector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Some files were not shown because too many files have changed in this diff Show More