Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7881c7a15 | ||
|
|
22050d6e8a | ||
|
|
a27691171c | ||
|
|
3cc4314ffd | ||
|
|
4744f9a7d0 | ||
|
|
bad64d8f20 | ||
|
|
3d198c1f4e | ||
|
|
ebb90a95b0 | ||
|
|
2df1e1bfc8 | ||
|
|
8013cae879 | ||
|
|
6560215e26 | ||
|
|
91535cd653 | ||
|
|
5e1add510d | ||
|
|
60aeb29590 | ||
|
|
a59bc4891a | ||
|
|
5826eea632 | ||
|
|
aee9826097 | ||
|
|
03c51a51c7 | ||
|
|
bb5665e2d8 | ||
|
|
c2738e5540 | ||
|
|
4f962063f6 | ||
|
|
a03a40e396 | ||
|
|
cb3c7c4f45 | ||
|
|
c5c421572b | ||
|
|
94517190b1 | ||
|
|
a244fe2a41 | ||
|
|
85e6b05be4 | ||
|
|
3f759ad47f | ||
|
|
946be85f00 | ||
|
|
22da53f39d | ||
|
|
d7ff7bf371 | ||
|
|
c9b9353846 | ||
|
|
694952c458 | ||
|
|
627dcfc014 | ||
|
|
5a46bbe73a | ||
|
|
035bab3d82 | ||
|
|
2922e70505 | ||
|
|
efe39d77dc | ||
|
|
3ff723fd45 | ||
|
|
319c7d66eb | ||
|
|
c7d15a02b7 | ||
|
|
27854c071c | ||
|
|
a7afc31fd4 | ||
|
|
a7ef7b622c | ||
|
|
a73271b49c | ||
|
|
cccb0f7698 | ||
|
|
32c1cd634c | ||
|
|
26d40e9563 | ||
|
|
d902298b98 | ||
|
|
c46ecfbbc8 | ||
|
|
8e0c680eca | ||
|
|
5d5b02178c | ||
|
|
8ee511d0a5 | ||
|
|
43f0f2f7ab | ||
|
|
9ead029ec3 | ||
|
|
05a6014c6b | ||
|
|
2b7055ecc6 | ||
|
|
96b0d1c2e9 | ||
|
|
116af9c59b | ||
|
|
65645e7275 |
+2
-10
@@ -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 &
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<img src="info/icon_glow.png" width="100" height="100" />
|
||||
|
||||
[](https://travis-ci.org/kamgurgul/cpu-info)<br/>
|
||||
CPU Info
|
||||
CPU Info [](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 />
|
||||
[](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
@@ -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'
|
||||
|
||||
@@ -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.
Vendored
+27
-6
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
-97
@@ -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)
|
||||
}
|
||||
}
|
||||
+39
-55
@@ -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, _ ->
|
||||
|
||||
+75
-52
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+45
-13
@@ -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"
|
||||
}
|
||||
}
|
||||
+77
-31
@@ -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()
|
||||
}
|
||||
}
|
||||
+53
-26
@@ -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"
|
||||
}
|
||||
}
|
||||
+43
-19
@@ -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"
|
||||
}
|
||||
}
|
||||
+41
-12
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -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
|
||||
}
|
||||
}
|
||||
+45
-16
@@ -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"
|
||||
}
|
||||
}
|
||||
+54
-47
@@ -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"
|
||||
}
|
||||
}
|
||||
+40
-14
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-13
@@ -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"
|
||||
}
|
||||
}
|
||||
+40
-12
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
-8
@@ -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"
|
||||
}
|
||||
}
|
||||
+43
-18
@@ -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=${
|
||||
|
||||
+5
-8
@@ -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) {
|
||||
|
||||
+6
-14
@@ -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
|
||||
}
|
||||
}
|
||||
+24
-22
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+13
-13
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+22
-32
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-54
@@ -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!!
|
||||
}
|
||||
+2
-14
@@ -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
|
||||
}
|
||||
+5
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+10
-19
@@ -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)
|
||||
+45
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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
Reference in New Issue
Block a user