├── .gitignore ├── .idea ├── encodings.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── STATUS.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── simonk │ │ └── projects │ │ └── taskmanager │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── simonk │ │ │ └── projects │ │ │ └── taskmanager │ │ │ ├── database │ │ │ ├── DatabaseManager.java │ │ │ ├── LocalDatabase.java │ │ │ ├── entity │ │ │ │ ├── BlacklistEntity.java │ │ │ │ └── TerminalSnapshotEntity.java │ │ │ └── providers │ │ │ │ ├── BlacklistProvider.java │ │ │ │ └── TerminalSnapshotProvider.java │ │ │ ├── entity │ │ │ ├── AppInfo.java │ │ │ ├── ProcessInfo.java │ │ │ └── TerminalCall.java │ │ │ ├── repository │ │ │ ├── BlacklistRepository.java │ │ │ └── ProcessRepository.java │ │ │ ├── services │ │ │ └── blacklist │ │ │ │ └── BlacklistJobService.java │ │ │ ├── terminal │ │ │ ├── StringTerminalListener.java │ │ │ ├── Terminal.java │ │ │ ├── TerminalListener.java │ │ │ ├── TerminalService.java │ │ │ ├── handlers │ │ │ │ ├── BlankCommandHandler.java │ │ │ │ ├── CdCommandHandler.java │ │ │ │ ├── EchoCommandHandler.java │ │ │ │ ├── ExportCommandHandler.java │ │ │ │ ├── PwdCommandHandler.java │ │ │ │ └── RequestHandler.java │ │ │ └── interceptors │ │ │ │ ├── TerminalRequestInterceptor.java │ │ │ │ └── TopCommandInterceptor.java │ │ │ ├── ui │ │ │ ├── BindingActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── blacklist │ │ │ │ ├── BlackListAdapter.java │ │ │ │ ├── BlacklistFragment.java │ │ │ │ └── viewmodels │ │ │ │ │ └── BlacklistViewModel.java │ │ │ ├── process │ │ │ │ ├── ChangeDetailsDialog.java │ │ │ │ ├── CleanedDialog.java │ │ │ │ ├── ProcessAdapter.java │ │ │ │ ├── ProcessFragment.java │ │ │ │ └── viewmodels │ │ │ │ │ └── ProcessViewModel.java │ │ │ ├── terminal │ │ │ │ ├── TerminalFragment.java │ │ │ │ └── viewmodels │ │ │ │ │ └── TerminalViewModel.java │ │ │ └── util │ │ │ │ └── ObjectListAdapter.java │ │ │ └── util │ │ │ ├── MemoryUtils.java │ │ │ └── ProcessCompat.java │ └── res │ │ ├── drawable-anydpi │ │ └── ic_close.xml │ │ ├── drawable-hdpi │ │ └── ic_close.png │ │ ├── drawable-mdpi │ │ └── ic_close.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── ic_close.png │ │ ├── drawable-xxhdpi │ │ └── ic_close.png │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── blacklist_item.xml │ │ ├── blacklist_item_label.xml │ │ ├── change_details_dialog.xml │ │ ├── cleaned_dialog.xml │ │ ├── fragment_blacklist.xml │ │ ├── fragment_process.xml │ │ ├── fragment_terminal.xml │ │ ├── process_list_item.xml │ │ └── process_list_item_details.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-ru │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── simonk │ └── projects │ └── taskmanager │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── apps.png ├── blacklist.png └── terminal.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | /debug_keystore.properties -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android-Process-Manager - beautiful manager of processes, apps and terminal for Android 2 | 3 | Apps provide you information about memory usage and help you to clean unnecessary running apps. 4 | Let you block running apps in backgroud and also show scary message when you (or not you) launch other app that was added in blacklist of this app - parental control you need! 5 | But if you wanna work with processes and apps like a pro, you can use terminal and send requests to Android shell 6 | 7 | ### You've never seen anything like this before!!! 8 | 9 | ### Features: 10 | * Show running processes 11 | * Kill a process you want 12 | * Show you memory usage 13 | * Clean memory as much as possible 14 | * Show additional information about process 15 | * Show all installed (and not system) apps 16 | * Add apps in blacklist 17 | * Stop background processes of applications that were added to blacklist 18 | * Show notification with scary message when blocked app is running 19 | * Show last time blocked app was opened (for parental control, yeah) 20 | * Let you work with Android shell 21 | 22 | ### Languages supported: 23 | * English 24 | * Russian 25 | 26 | ### Screenshots: 27 |

28 | 29 |

30 |

31 | 32 |

33 |

34 | 35 |

36 | 37 | #### Check STATUS.md to see tasks and progress :) 38 | -------------------------------------------------------------------------------- /STATUS.md: -------------------------------------------------------------------------------- 1 | | Title | Description | Deadline | Finished | 2 | |----------------------|:---------------------:|-------:|------:| 3 | | Init and configure project | Initialize project, configure build.gradle and other settings for modern work process. Modern dependencies, debug keystore, versioning of apk | 15.04 | 29.03 | 4 | | Show working processes | Create fragment with recycler view that shows list of running proccesses. Icon, process name, process package. Tap on item should show additional information | 15.04 | 07.04 | 5 | | Let process be killed | Let user to kill a process. Button in list item that appears when user makes one tap | 15.04 | 27.04 | 6 | | Clean memory | Add feature to kill all apps. Show memory usage and percent of free memory. Button like Clean up to remove process from memory. Show result dialog: memory before cleaning, memory after cleaning, effectiveness | 15.04 | 11.04 | 7 | | Change process details | Long tap should show dialog where you can change process details. Figure out what we can change | 15.04 | 11.04 | 8 | | Clean architecture, Tabs | Add tabs in main acitivity, all other ui logic in corresponding fragments. Introduce Clean architecture with Room, View Model, Live Data, Repository | 20.04 | 21.04 (Lack of time) | 9 | | Blacklist fragment add. All apps | Show all installed apps in recyclerview in special list. Create separated adapter with twe different lists in one | 01.05 | 28.04 | 10 | | Blacklist. Persistance storage | Create database to store blacklist data. Show blacklist apps in special part of recyelr view (may be in top) | 01.05 | 28.04 | 11 | | Background killing | Make blacklist apps to be killed by app. Create JobScheduler service for this puprose | 01.05 | 29.04 | 12 | | Notification | Send notification to user when blacklist app is running. Show scary message. Make notification sound. Save timestamp to show user when blacklist app was opened last time | 01.05 | 29.04 | 13 | | Terminal fragment add. Terminal simple output | Create simple fragment where corresponding edit text and text view will be created dynamically. Create terminal class where new processes for commands will be created | 01.05 | 23.04 | 14 | | Terminal. Add request handlers | Create request handlers for commands: pwd, cd, echo, export to handle it manually. Use brain to create it maintainable | 01.05 | 23.04 | 15 | | Terminal. Add multithreading | Create HandlerThread to perform temrinal process in other thread. Add listeners to get data in ui. Add handlers to communicate with thread | 01.05 | 23.04 | 16 | | Terminal. Top command | Let terminal to handle input stream while command is running and perform some commands in other way. Support top command. Show output continuously while user will not stop. Show top command output correctly: first memory usage, than table headers and then all processes. When new refresh occure, refresh textview and not add new output to bottom of fragment, just change current textview content | 01.05 | 26.04 | 17 | | Terminal. Database | Add history to temrinal. Save history in database to let user see previous commands. Also let user tap on previous command to reuse it | 01.05 | 27.04 | 18 | | Terminal. Database | Add history to temrinal. Save history in database to let user see previous commands. Also let user tap on previous command to reuse it | 01.05 | 27.04 | 19 | | Localization. Russion. English | Add localization to app: russian, english | 03.05 | 01.05 | 20 | | Process and blacklist multithreading | Add advanced multithreading to repositories, make database manager work with thread pool executor | 15.05 | in progress | 21 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | def getVersion(){ 4 | def cmd = 'git describe --tags' 5 | Process process = cmd.execute() 6 | String[] descriptionInfo = process.in.text.split('-') 7 | if (descriptionInfo.length == 1) { 8 | descriptionInfo[0] = descriptionInfo[0].replace('\n', '') 9 | return descriptionInfo[0]+"."+0 10 | } 11 | return descriptionInfo[0] + "." + descriptionInfo[1] 12 | } 13 | 14 | def debugKeystoreProperties = new Properties() 15 | debugKeystoreProperties.load(new FileInputStream(rootProject.file("debug_keystore.properties"))) 16 | 17 | android { 18 | signingConfigs { 19 | debug_config { 20 | keyAlias debugKeystoreProperties['keyAlias'] 21 | keyPassword debugKeystoreProperties['keyPassword'] 22 | storeFile file(debugKeystoreProperties['storeFile']) 23 | storePassword debugKeystoreProperties['storePassword'] 24 | } 25 | } 26 | 27 | compileSdkVersion 28 28 | defaultConfig { 29 | applicationId "com.simonk.projects.taskmanager" 30 | minSdkVersion 21 31 | targetSdkVersion 28 32 | versionCode 1 33 | versionName getVersion() 34 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 35 | } 36 | 37 | android.applicationVariants.all { variant -> 38 | variant.outputs.all { 39 | outputFileName = "${rootProject.name}-${variant.versionName}.apk" 40 | } 41 | } 42 | 43 | buildTypes { 44 | debug { 45 | signingConfig signingConfigs.debug_config 46 | } 47 | release { 48 | minifyEnabled false 49 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 50 | } 51 | } 52 | 53 | compileOptions { 54 | sourceCompatibility JavaVersion.VERSION_1_8 55 | targetCompatibility JavaVersion.VERSION_1_8 56 | } 57 | 58 | dataBinding.enabled = true 59 | } 60 | 61 | dependencies { 62 | implementation fileTree(dir: 'libs', include: ['*.jar']) 63 | 64 | implementation 'androidx.appcompat:appcompat:1.0.2' 65 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 66 | implementation "androidx.recyclerview:recyclerview:1.0.0" 67 | implementation "androidx.cardview:cardview:1.0.0" 68 | implementation 'com.google.android.material:material:1.1.0-alpha05' 69 | 70 | implementation "androidx.lifecycle:lifecycle-extensions:2.0.0" 71 | 72 | implementation "androidx.room:room-runtime:2.1.0-alpha07" 73 | annotationProcessor "androidx.room:room-compiler:2.1.0-alpha07" 74 | 75 | implementation 'com.google.guava:guava:27.1-jre' 76 | 77 | testImplementation 'junit:junit:4.12' 78 | androidTestImplementation 'androidx.test:runner:1.1.1' 79 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 80 | } 81 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/simonk/projects/taskmanager/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.simonk.projects.taskmanager", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/database/DatabaseManager.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.database; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.lifecycle.LiveData; 6 | 7 | import com.simonk.projects.taskmanager.database.entity.BlacklistEntity; 8 | import com.simonk.projects.taskmanager.database.entity.TerminalSnapshotEntity; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * Makes using LocalDatabase easier. You can still work with LocalDatabase by yourself, 14 | * this class only help you avoid long method calls 15 | * 16 | * Provide multithreading for insert, update, delete operations 17 | * 18 | * TODO: work with threads using thread pool executor 19 | * 20 | */ 21 | public class DatabaseManager { 22 | 23 | public static LiveData> loadSnapshots(Context context) { 24 | return LocalDatabase.getInstance(context) 25 | .terminalSnapshotProvider() 26 | .loadSnapshots(); 27 | } 28 | 29 | public static void insertSnapshot(Context context, TerminalSnapshotEntity snapshot) { 30 | Thread thread = new Thread(() -> { 31 | LocalDatabase.getInstance(context) 32 | .terminalSnapshotProvider() 33 | .insertSnapshot(snapshot); 34 | }); 35 | thread.setName("TaskManager-Database"); 36 | thread.start(); 37 | } 38 | 39 | public static void deleteAllSnapshots(Context context) { 40 | Thread thread = new Thread(() -> { 41 | LocalDatabase.getInstance(context) 42 | .terminalSnapshotProvider() 43 | .clearSnapshots(); 44 | }); 45 | thread.setName("TaskManager-Database"); 46 | thread.start(); 47 | } 48 | 49 | public static LiveData> loadBlacklistEntities(Context context) { 50 | return LocalDatabase.getInstance(context) 51 | .blacklistProvider() 52 | .loadBlacklistEntities(); 53 | } 54 | 55 | public static void insertBlacklistEntity(Context context, BlacklistEntity entity) { 56 | Thread thread = new Thread(() -> { 57 | LocalDatabase.getInstance(context) 58 | .blacklistProvider() 59 | .insertBlacklistEntity(entity); 60 | }); 61 | thread.setName("TaskManager-Database"); 62 | thread.start(); 63 | } 64 | 65 | public static void deleteBlacklistEntityByPackage(Context context, String ppackage) { 66 | Thread thread = new Thread(() -> { 67 | LocalDatabase.getInstance(context) 68 | .blacklistProvider() 69 | .deleteBlacklistEntityByPackage(ppackage); 70 | }); 71 | thread.setName("TaskManager-Database"); 72 | thread.start(); 73 | } 74 | 75 | public static void updateBlacklistEntityOpenDate(Context context, String ppackage, long openDate) { 76 | Thread thread = new Thread(() -> { 77 | LocalDatabase.getInstance(context) 78 | .blacklistProvider() 79 | .updateBlacklistEntityOpenDate(ppackage, openDate); 80 | }); 81 | thread.setName("TaskManager-Database"); 82 | thread.start(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/database/LocalDatabase.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.database; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.room.Room; 6 | import androidx.room.RoomDatabase; 7 | 8 | import com.simonk.projects.taskmanager.database.entity.BlacklistEntity; 9 | import com.simonk.projects.taskmanager.database.entity.TerminalSnapshotEntity; 10 | import com.simonk.projects.taskmanager.database.providers.BlacklistProvider; 11 | import com.simonk.projects.taskmanager.database.providers.TerminalSnapshotProvider; 12 | 13 | @androidx.room.Database(entities = {TerminalSnapshotEntity.class, BlacklistEntity.class}, version = 1, exportSchema = false) 14 | public abstract class LocalDatabase extends RoomDatabase { 15 | 16 | private static LocalDatabase sInstance; 17 | 18 | private static final String DATABASE_NAME = "TaskManagerDatabase"; 19 | 20 | /** 21 | * Provider for working with snapshots of terminal. 22 | * See {@link TerminalSnapshotEntity} for more details 23 | * @return 24 | */ 25 | public abstract TerminalSnapshotProvider terminalSnapshotProvider(); 26 | /** 27 | * Provider for working with blacklist entries. 28 | * See {@link BlacklistEntity} for more details 29 | * @return 30 | */ 31 | public abstract BlacklistProvider blacklistProvider(); 32 | 33 | public static LocalDatabase getInstance(final Context context) { 34 | if (sInstance == null) { 35 | synchronized (LocalDatabase.class) { 36 | if (sInstance == null) { 37 | sInstance = Room.databaseBuilder(context.getApplicationContext(), LocalDatabase.class, DATABASE_NAME) 38 | .build(); 39 | } 40 | } 41 | } 42 | return sInstance; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/database/entity/BlacklistEntity.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.database.entity; 2 | 3 | import androidx.room.Entity; 4 | import androidx.room.PrimaryKey; 5 | 6 | /** 7 | * Entity class for database. Represents information about blocked app, which will be saved in database 8 | */ 9 | @Entity(tableName = "blacklist") 10 | public class BlacklistEntity { 11 | 12 | @PrimaryKey(autoGenerate = true) 13 | public int id; 14 | 15 | /** 16 | * Package full name of blocked app 17 | */ 18 | public String apppackage; 19 | 20 | /** 21 | * Last time when app was opened in milliseconds 22 | */ 23 | public long lastOpenDate; 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/database/entity/TerminalSnapshotEntity.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.database.entity; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.room.Entity; 5 | import androidx.room.PrimaryKey; 6 | 7 | /** 8 | * Entity class for database. Represents information about terminal snapshot, which will be saved in database 9 | * Terminal snapshot is piece of data which contains request and response as strings only. 10 | * Helpful for creating history of terminal 11 | */ 12 | @Entity(tableName = "terminal_snapshot") 13 | public class TerminalSnapshotEntity { 14 | 15 | @PrimaryKey(autoGenerate = true) 16 | public int id; 17 | 18 | /** 19 | * Request to terminal 20 | */ 21 | public String request; 22 | 23 | /** 24 | * Response of terminal 25 | */ 26 | public String response; 27 | 28 | public TerminalSnapshotEntity() { 29 | 30 | } 31 | 32 | public TerminalSnapshotEntity(String request, String response) { 33 | this.request = request; 34 | this.response = response; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/database/providers/BlacklistProvider.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.database.providers; 2 | 3 | import androidx.lifecycle.LiveData; 4 | import androidx.room.Dao; 5 | import androidx.room.Delete; 6 | import androidx.room.Insert; 7 | import androidx.room.Query; 8 | import androidx.room.Update; 9 | 10 | import com.simonk.projects.taskmanager.database.entity.BlacklistEntity; 11 | import com.simonk.projects.taskmanager.database.entity.TerminalSnapshotEntity; 12 | 13 | import java.util.List; 14 | 15 | @Dao 16 | public interface BlacklistProvider { 17 | 18 | @Query("SELECT * FROM blacklist WHERE id=:id") 19 | LiveData loadBlacklistEntity(String id); 20 | 21 | @Query("SELECT * FROM blacklist WHERE id=:id") 22 | BlacklistEntity getBlacklistEntity(String id); 23 | 24 | @Query("SELECT * FROM blacklist") 25 | LiveData> loadBlacklistEntities(); 26 | 27 | @Query("SELECT * FROM blacklist") 28 | List getBlacklistEntities(); 29 | 30 | @Insert 31 | void insertBlacklistEntity(BlacklistEntity entity); 32 | 33 | @Update 34 | void updateBlacklistEntity(BlacklistEntity entity); 35 | 36 | @Delete 37 | void deleteBlacklistEntity(BlacklistEntity entity); 38 | 39 | @Query("DELETE FROM blacklist WHERE apppackage=:ppackage") 40 | void deleteBlacklistEntityByPackage(String ppackage); 41 | 42 | @Query("DELETE FROM blacklist") 43 | void clearBlacklistEntities(); 44 | 45 | @Query("UPDATE blacklist SET lastOpenDate = :openDate WHERE apppackage=:ppackage") 46 | void updateBlacklistEntityOpenDate(String ppackage, long openDate); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/database/providers/TerminalSnapshotProvider.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.database.providers; 2 | 3 | import androidx.lifecycle.LiveData; 4 | import androidx.room.Dao; 5 | import androidx.room.Delete; 6 | import androidx.room.Insert; 7 | import androidx.room.Query; 8 | import androidx.room.Update; 9 | 10 | import com.simonk.projects.taskmanager.database.entity.TerminalSnapshotEntity; 11 | 12 | import java.util.List; 13 | 14 | @Dao 15 | public interface TerminalSnapshotProvider { 16 | 17 | @Query("SELECT * FROM terminal_snapshot WHERE id=:id") 18 | LiveData loadSnapshot(String id); 19 | 20 | @Query("SELECT * FROM terminal_snapshot WHERE id=:id") 21 | TerminalSnapshotEntity getSnapshot(String id); 22 | 23 | @Query("SELECT * FROM terminal_snapshot") 24 | LiveData> loadSnapshots(); 25 | 26 | @Query("SELECT * FROM terminal_snapshot") 27 | List getSnapshots(); 28 | 29 | @Insert 30 | void insertSnapshot(TerminalSnapshotEntity entity); 31 | 32 | @Update 33 | void updateSnapshot(TerminalSnapshotEntity entity); 34 | 35 | @Delete 36 | void deleteSnapshot(TerminalSnapshotEntity entity); 37 | 38 | @Query("DELETE FROM terminal_snapshot") 39 | void clearSnapshots(); 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/entity/AppInfo.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.entity; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import java.util.Objects; 6 | 7 | /** 8 | * POJO represents usefull for this app information about other apps. 9 | */ 10 | public class AppInfo { 11 | 12 | private String text; 13 | private Drawable image; 14 | private String ppackage; 15 | private boolean isEnabled; 16 | private boolean isSystem; 17 | private boolean isInBlacklist; 18 | private long lastOpenDate; 19 | 20 | public String getText() { 21 | return text; 22 | } 23 | 24 | public void setText(String text) { 25 | this.text = text; 26 | } 27 | 28 | public Drawable getImage() { 29 | return image; 30 | } 31 | 32 | public void setImage(Drawable image) { 33 | this.image = image; 34 | } 35 | 36 | public String getPpackage() { 37 | return ppackage; 38 | } 39 | 40 | public void setPpackage(String ppackage) { 41 | this.ppackage = ppackage; 42 | } 43 | 44 | public boolean isEnabled() { 45 | return isEnabled; 46 | } 47 | 48 | public void setEnabled(boolean enabled) { 49 | isEnabled = enabled; 50 | } 51 | 52 | public boolean getIsSystem() { 53 | return isSystem; 54 | } 55 | 56 | public void setIsSystem(boolean isSystem) { 57 | this.isSystem = isSystem; 58 | } 59 | 60 | public boolean isInBlacklist() { 61 | return isInBlacklist; 62 | } 63 | 64 | public void setInBlacklist(boolean inBlacklist) { 65 | isInBlacklist = inBlacklist; 66 | } 67 | 68 | public long getLastOpenDate() { 69 | return lastOpenDate; 70 | } 71 | 72 | public void setLastOpenDate(long lastOpenDate) { 73 | this.lastOpenDate = lastOpenDate; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/entity/ProcessInfo.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.entity; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | 8 | /** 9 | * POJO represents usefull for this app information about a running process 10 | */ 11 | public class ProcessInfo implements Serializable { 12 | private String text; 13 | private Drawable image; 14 | private String ppackage; 15 | private int priority; 16 | private boolean status; 17 | private int minSdk; 18 | private int uid; 19 | private String description; 20 | private int pid; 21 | 22 | public String getText() { 23 | return text; 24 | } 25 | 26 | public void setText(String text) { 27 | this.text = text; 28 | } 29 | 30 | public Drawable getImage() { 31 | return image; 32 | } 33 | 34 | public void setImage(Drawable image) { 35 | this.image = image; 36 | } 37 | 38 | public String getPpackage() { 39 | return ppackage; 40 | } 41 | 42 | public void setPpackage(String ppackage) { 43 | this.ppackage = ppackage; 44 | } 45 | 46 | public int getPriority() { 47 | return priority; 48 | } 49 | 50 | public void setPriority(int priority) { 51 | this.priority = priority; 52 | } 53 | 54 | public boolean isStatus() { 55 | return status; 56 | } 57 | 58 | public void setStatus(boolean status) { 59 | this.status = status; 60 | } 61 | 62 | public int getMinSdk() { 63 | return minSdk; 64 | } 65 | 66 | public void setMinSdk(int minSdk) { 67 | this.minSdk = minSdk; 68 | } 69 | 70 | public int getUid() { 71 | return uid; 72 | } 73 | 74 | public void setUid(int uid) { 75 | this.uid = uid; 76 | } 77 | 78 | public String getDescription() { 79 | return description; 80 | } 81 | 82 | public void setDescription(String description) { 83 | this.description = description; 84 | } 85 | 86 | public int getPid() { 87 | return pid; 88 | } 89 | 90 | public void setPid(int pid) { 91 | this.pid = pid; 92 | } 93 | 94 | @Override 95 | public boolean equals(Object o) { 96 | if (this == o) return true; 97 | if (o == null || getClass() != o.getClass()) return false; 98 | ProcessInfo that = (ProcessInfo) o; 99 | return getPriority() == that.getPriority() && 100 | isStatus() == that.isStatus() && 101 | getMinSdk() == that.getMinSdk() && 102 | getUid() == that.getUid() && 103 | getPid() == that.getPid() && 104 | Objects.equals(getText(), that.getText()) && 105 | Objects.equals(getImage(), that.getImage()) && 106 | Objects.equals(getPpackage(), that.getPpackage()) && 107 | Objects.equals(getDescription(), that.getDescription()); 108 | } 109 | 110 | @Override 111 | public int hashCode() { 112 | return Objects.hash(getText(), getImage(), getPpackage(), getPriority(), isStatus(), 113 | getMinSdk(), getUid(), getDescription(), getPid()); 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/entity/TerminalCall.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.entity; 2 | 3 | import java.io.InputStream; 4 | import java.io.OutputStream; 5 | import java.util.Objects; 6 | 7 | /** 8 | * POJO represents terminal call. Contains request to terminal and different streams of corresponding response 9 | */ 10 | public class TerminalCall { 11 | 12 | private String request; 13 | private Exception exception; 14 | 15 | private Process process; 16 | 17 | private InputStream inputStream; 18 | 19 | /** 20 | * @return request to terminal. Additionally trim output 21 | */ 22 | public String getRequest() { 23 | return request.trim(); 24 | } 25 | 26 | public void setRequest(String request) { 27 | this.request = request; 28 | } 29 | 30 | public void setProcess(Process process) { 31 | this.process = process; 32 | } 33 | 34 | public Process getProcess() { 35 | return process; 36 | } 37 | 38 | /** 39 | * By default input stream will be retrieved from Process object supplied by setProcess method 40 | * But you can explicitly set input stream which will have bigger priority than process's one 41 | * @param inputStream 42 | */ 43 | public void setResponseInputStream(InputStream inputStream) { 44 | this.inputStream = inputStream; 45 | } 46 | 47 | /** 48 | * @return input stream provided by setResponseInputStream or input stream retrieved from Process 49 | */ 50 | public InputStream getResponseInputStream() { 51 | return inputStream == null 52 | ? (process == null ? null : process.getInputStream()) 53 | : inputStream; 54 | } 55 | 56 | public InputStream getResponseErrorStream() { 57 | return process.getErrorStream(); 58 | } 59 | 60 | public OutputStream getResponseOutputStream() { 61 | return process.getOutputStream(); 62 | } 63 | 64 | public Exception getException() { 65 | return exception; 66 | } 67 | 68 | public void setException(Exception exception) { 69 | this.exception = exception; 70 | } 71 | 72 | @Override 73 | public boolean equals(Object o) { 74 | if (this == o) return true; 75 | if (o == null || getClass() != o.getClass()) return false; 76 | TerminalCall that = (TerminalCall) o; 77 | return Objects.equals(getRequest(), that.getRequest()) && 78 | Objects.equals(getException(), that.getException()) && 79 | Objects.equals(getProcess(), that.getProcess()); 80 | } 81 | 82 | @Override 83 | public int hashCode() { 84 | return Objects.hash(getRequest(), getException(), getProcess()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/repository/BlacklistRepository.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.repository; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | import android.content.pm.ApplicationInfo; 6 | import android.content.pm.PackageManager; 7 | import android.os.Build; 8 | 9 | import androidx.lifecycle.LiveData; 10 | import androidx.lifecycle.MutableLiveData; 11 | import androidx.lifecycle.Observer; 12 | import androidx.lifecycle.Transformations; 13 | import androidx.room.Database; 14 | 15 | import com.simonk.projects.taskmanager.database.DatabaseManager; 16 | import com.simonk.projects.taskmanager.database.entity.BlacklistEntity; 17 | import com.simonk.projects.taskmanager.entity.AppInfo; 18 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * Repository for blacklist. Provides base functions for work with blacklist 25 | * Retrieves information from different sources: system's PackageManager and local database 26 | */ 27 | public class BlacklistRepository { 28 | 29 | /** 30 | * Returns all installed on device apps. Uses system's PackageManager 31 | * 32 | * All returned apps supposed to be not in black list 33 | * 34 | * Note: not returns current application in list 35 | * 36 | * TODO: perform logic in separated thread with LiveData 37 | * 38 | * @param context 39 | * @param notSystem if true all system applications will be ignored 40 | * @return list of applications 41 | */ 42 | public List getAllInstalledApplicationsInfo(Context context, boolean notSystem) { 43 | PackageManager packageManager = context.getPackageManager(); 44 | List installedApplications = packageManager.getInstalledApplications(PackageManager.GET_META_DATA); 45 | 46 | List appInfoList = new ArrayList<>(); 47 | for (ApplicationInfo info : installedApplications) { 48 | AppInfo appInfo = new AppInfo(); 49 | appInfo.setText(packageManager.getApplicationLabel(info).toString()); 50 | appInfo.setImage(packageManager.getApplicationIcon(info)); 51 | appInfo.setPpackage(info.processName); 52 | appInfo.setEnabled(info.enabled); 53 | appInfo.setInBlacklist(false); 54 | boolean isSystem = (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 55 | if (isSystem && notSystem) { 56 | continue; 57 | } 58 | appInfo.setIsSystem(isSystem); 59 | 60 | if (appInfo.getPpackage().contains(context.getPackageName())) { 61 | continue; 62 | } 63 | 64 | appInfoList.add(appInfo); 65 | } 66 | 67 | return appInfoList; 68 | } 69 | 70 | /** 71 | * Returns LiveData of blacklist retrieved from database 72 | * @param context 73 | * @return 74 | */ 75 | public LiveData> getAllBlacklistApplicationInfo(Context context) { 76 | LiveData> blacklistEntitiesData = DatabaseManager.loadBlacklistEntities(context); 77 | return Transformations.map(blacklistEntitiesData, blacklistEntities -> { 78 | List appInfoList = new ArrayList<>(); 79 | PackageManager packageManager = context.getPackageManager(); 80 | for (BlacklistEntity entity : blacklistEntities) { 81 | ApplicationInfo info = null; 82 | try { 83 | info = packageManager.getApplicationInfo(entity.apppackage, PackageManager.GET_META_DATA); 84 | } catch (PackageManager.NameNotFoundException exception) { 85 | continue; 86 | } 87 | AppInfo appInfo = new AppInfo(); 88 | appInfo.setText(packageManager.getApplicationLabel(info).toString()); 89 | appInfo.setImage(packageManager.getApplicationIcon(info)); 90 | appInfo.setPpackage(info.processName); 91 | appInfo.setEnabled(info.enabled); 92 | appInfo.setIsSystem((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0); 93 | appInfo.setInBlacklist(true); 94 | appInfo.setLastOpenDate(entity.lastOpenDate); 95 | 96 | appInfoList.add(appInfo); 97 | } 98 | 99 | return appInfoList; 100 | }); 101 | } 102 | 103 | /** 104 | * Insert an application to blacklist 105 | * @param context 106 | * @param appInfo 107 | */ 108 | public void insertAppInBlacklist(Context context, AppInfo appInfo) { 109 | BlacklistEntity blacklistEntity = new BlacklistEntity(); 110 | blacklistEntity.apppackage = appInfo.getPpackage(); 111 | blacklistEntity.lastOpenDate = appInfo.getLastOpenDate(); 112 | DatabaseManager.insertBlacklistEntity(context, blacklistEntity); 113 | } 114 | 115 | /** 116 | * Delete an application from blacklsit 117 | * @param context 118 | * @param appInfo 119 | */ 120 | public void deleteAppFromBlackList(Context context, AppInfo appInfo) { 121 | BlacklistEntity blacklistEntity = new BlacklistEntity(); 122 | blacklistEntity.apppackage = appInfo.getPpackage(); 123 | blacklistEntity.lastOpenDate = appInfo.getLastOpenDate(); 124 | DatabaseManager.deleteBlacklistEntityByPackage(context, blacklistEntity.apppackage); 125 | } 126 | 127 | /** 128 | * Change last open date of an application in blacklsit 129 | * @param context 130 | * @param appInfo 131 | * @param openDate 132 | */ 133 | public void setBlacklistAppOpenDate(Context context, AppInfo appInfo, long openDate) { 134 | DatabaseManager.updateBlacklistEntityOpenDate(context, appInfo.getPpackage(), openDate); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/repository/ProcessRepository.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.repository; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | import android.content.pm.ApplicationInfo; 6 | import android.content.pm.PackageManager; 7 | 8 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * Repository for process management. Works mainly with system's API 15 | * 16 | */ 17 | public class ProcessRepository { 18 | 19 | /** 20 | * @param context 21 | * @return all currently running processes 22 | */ 23 | public List getAllProcessInfo(Context context) { 24 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 25 | List runningAppProcessInfos 26 | = activityManager.getRunningAppProcesses(); 27 | 28 | PackageManager packageManager = context.getPackageManager(); 29 | 30 | List processInfoList = new ArrayList<>(); 31 | for (ActivityManager.RunningAppProcessInfo info : runningAppProcessInfos) { 32 | ProcessInfo processInfo = new ProcessInfo(); 33 | try { 34 | ApplicationInfo applicationInfo = 35 | packageManager.getApplicationInfo(info.processName, 0); 36 | processInfo.setText(packageManager.getApplicationLabel(applicationInfo).toString()); 37 | processInfo.setImage(packageManager.getApplicationIcon(applicationInfo)); 38 | processInfo.setPpackage(info.processName); 39 | processInfo.setPriority(info.importance); 40 | processInfo.setStatus(applicationInfo.enabled); 41 | processInfo.setMinSdk(applicationInfo.targetSdkVersion); 42 | processInfo.setUid(applicationInfo.uid); 43 | processInfo.setDescription(applicationInfo.descriptionRes != 0 44 | ? context.getResources().getString(applicationInfo.descriptionRes) 45 | : ""); 46 | processInfo.setPid(info.pid); 47 | } catch (PackageManager.NameNotFoundException e) { 48 | continue; 49 | } 50 | processInfoList.add(processInfo); 51 | } 52 | 53 | return processInfoList; 54 | } 55 | 56 | /** 57 | * @param context 58 | * @param packageName 59 | * @return return process info for corresponding package 60 | * or null if there is not application or application is not running 61 | */ 62 | public ProcessInfo getRunningProcessInfoForPackage(Context context, String packageName) { 63 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 64 | List runningAppProcessInfos 65 | = activityManager.getRunningAppProcesses(); 66 | 67 | PackageManager packageManager = context.getPackageManager(); 68 | 69 | for (ActivityManager.RunningAppProcessInfo info : runningAppProcessInfos) { 70 | if (info.processName.equals(packageName)) { 71 | ProcessInfo processInfo = new ProcessInfo(); 72 | try { 73 | ApplicationInfo applicationInfo = 74 | packageManager.getApplicationInfo(info.processName, 0); 75 | processInfo.setText(packageManager.getApplicationLabel(applicationInfo).toString()); 76 | processInfo.setImage(packageManager.getApplicationIcon(applicationInfo)); 77 | processInfo.setPpackage(info.processName); 78 | processInfo.setPriority(info.importance); 79 | processInfo.setStatus(applicationInfo.enabled); 80 | processInfo.setMinSdk(applicationInfo.targetSdkVersion); 81 | processInfo.setUid(applicationInfo.uid); 82 | processInfo.setDescription(applicationInfo.descriptionRes != 0 83 | ? context.getResources().getString(applicationInfo.descriptionRes) 84 | : ""); 85 | processInfo.setPid(info.pid); 86 | } catch (PackageManager.NameNotFoundException e) { 87 | return null; 88 | } 89 | return processInfo; 90 | } 91 | } 92 | 93 | return null; 94 | } 95 | 96 | /** 97 | * Kill process. 98 | * 99 | * Important: there is not guarantee that process will be destroyed actually. 100 | * It only sends kill signals 101 | * 102 | * Since there is not root permissions on device foreground process will never be killed 103 | * 104 | * @param context 105 | * @param processInfo 106 | */ 107 | public void killProcess(Context context, ProcessInfo processInfo) { 108 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 109 | 110 | android.os.Process.killProcess(processInfo.getPid()); 111 | android.os.Process.sendSignal(processInfo.getPid(), android.os.Process.SIGNAL_KILL); 112 | activityManager.killBackgroundProcesses(processInfo.getPpackage()); 113 | } 114 | 115 | /** 116 | * Tries to change process priority 117 | * 118 | * Important: there is not guarantee that process will accept new priority. 119 | * 120 | * @param context 121 | * @param processInfo 122 | * @param priority 123 | */ 124 | public void changeProcessPriority(Context context, ProcessInfo processInfo, int priority) { 125 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 126 | List runningAppProcessInfos 127 | = activityManager.getRunningAppProcesses(); 128 | for (ActivityManager.RunningAppProcessInfo runningAppProcessInfo : runningAppProcessInfos) { 129 | if (runningAppProcessInfo.processName.equals(processInfo.getPpackage())) { 130 | runningAppProcessInfo.importance = priority; 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Kill processes as much as possible 137 | * 138 | * Important: there is not guarantee that process will be destroyed actually. 139 | * It only sends kill signals 140 | * 141 | * Since there is not root permissions on device foreground process will never be killed 142 | * 143 | * @param context 144 | */ 145 | public void killAllProcesses(Context context) { 146 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 147 | List runningAppProcessInfos 148 | = activityManager.getRunningAppProcesses(); 149 | 150 | for (ActivityManager.RunningAppProcessInfo info : runningAppProcessInfos) { 151 | if (!info.processName.contains("simonk")) { 152 | android.os.Process.killProcess(info.pid); 153 | android.os.Process.sendSignal(info.pid, android.os.Process.SIGNAL_KILL); 154 | activityManager.killBackgroundProcesses(info.processName); 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/services/blacklist/BlacklistJobService.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.services.blacklist; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.app.job.JobParameters; 7 | import android.app.job.JobService; 8 | import android.content.Intent; 9 | import android.util.Log; 10 | 11 | import androidx.core.app.NotificationCompat; 12 | import androidx.core.app.NotificationManagerCompat; 13 | import androidx.lifecycle.LiveData; 14 | import androidx.lifecycle.Observer; 15 | 16 | import com.simonk.projects.taskmanager.R; 17 | import com.simonk.projects.taskmanager.entity.AppInfo; 18 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 19 | import com.simonk.projects.taskmanager.repository.BlacklistRepository; 20 | import com.simonk.projects.taskmanager.repository.ProcessRepository; 21 | import com.simonk.projects.taskmanager.ui.MainActivity; 22 | 23 | import java.util.List; 24 | 25 | /** 26 | * Background service for blacklist. Tries to remove users process if the process is in blacklist 27 | * Send notification to user if blocked process was found running 28 | */ 29 | public class BlacklistJobService extends JobService { 30 | 31 | @Override 32 | public boolean onStartJob(JobParameters params) { 33 | Log.d("TEST", "Start job"); 34 | LiveData> appInfoListData = 35 | new BlacklistRepository().getAllBlacklistApplicationInfo(getApplicationContext()); 36 | appInfoListData.observeForever(new Observer>() { 37 | @Override 38 | public void onChanged(List appInfos) { 39 | Log.d("TEST", "New data. Length: " + appInfos.size()); 40 | for (AppInfo appInfo : appInfos) { 41 | ProcessInfo processInfo = 42 | new ProcessRepository().getRunningProcessInfoForPackage(getApplicationContext(), appInfo.getPpackage()); 43 | if (processInfo != null) { 44 | Log.d("TEST", "Send kill process: " + processInfo.getPpackage()); 45 | new ProcessRepository().killProcess(getApplicationContext(), processInfo); 46 | notifyUser(appInfo); 47 | new BlacklistRepository().setBlacklistAppOpenDate(getApplicationContext(), appInfo, System.currentTimeMillis()); 48 | } 49 | } 50 | appInfoListData.removeObserver(this); 51 | } 52 | }); 53 | return false; 54 | } 55 | 56 | @Override 57 | public boolean onStopJob(JobParameters params) { 58 | return true; 59 | } 60 | 61 | /** 62 | * Notify user about running blocked app. Priority is set to high and sound is on 63 | * so notification will be shown on top of all other apps 64 | * @param appInfo 65 | */ 66 | private void notifyUser(AppInfo appInfo) { 67 | Intent intent = new Intent(this, MainActivity.class); 68 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 69 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); 70 | 71 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this); 72 | builder.setSmallIcon(R.drawable.ic_launcher_background) 73 | .setColor(getResources().getColor(R.color.colorPrimary)) 74 | .setContentTitle(getApplicationContext().getString(R.string.notify_title)) 75 | .setContentIntent(pendingIntent) 76 | .setAutoCancel(true) 77 | .setContentText(String.format(getApplicationContext().getString(R.string.notify_content), appInfo.getText())) 78 | .setDefaults(Notification.DEFAULT_ALL) 79 | .setPriority(NotificationCompat.PRIORITY_MAX); 80 | 81 | NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); 82 | notificationManager.notify(1000, builder.build()); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/StringTerminalListener.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal; 2 | 3 | /** 4 | * Util class handles byte array to string transformations 5 | */ 6 | public abstract class StringTerminalListener implements TerminalListener { 7 | 8 | public abstract void onInput(String input); 9 | 10 | public abstract void onError(String input); 11 | 12 | public abstract String onOutputString(); 13 | 14 | @Override 15 | public void onInput(byte[] input) { 16 | onInput(new String(input)); 17 | } 18 | 19 | @Override 20 | public void onError(byte[] error) { 21 | onError(new String(error)); 22 | } 23 | 24 | @Override 25 | public byte[] onOutput() { 26 | return onOutputString().getBytes(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/Terminal.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.common.io.CharStreams; 5 | import com.simonk.projects.taskmanager.entity.TerminalCall; 6 | import com.simonk.projects.taskmanager.terminal.handlers.BlankCommandHandler; 7 | import com.simonk.projects.taskmanager.terminal.handlers.CdCommandHandler; 8 | import com.simonk.projects.taskmanager.terminal.handlers.EchoCommandHandler; 9 | import com.simonk.projects.taskmanager.terminal.handlers.ExportCommandHandler; 10 | import com.simonk.projects.taskmanager.terminal.handlers.PwdCommandHandler; 11 | import com.simonk.projects.taskmanager.terminal.handlers.RequestHandler; 12 | 13 | import java.io.File; 14 | import java.io.InputStreamReader; 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.concurrent.BlockingQueue; 20 | import java.util.concurrent.Executor; 21 | import java.util.concurrent.LinkedBlockingQueue; 22 | import java.util.concurrent.ThreadFactory; 23 | import java.util.concurrent.ThreadPoolExecutor; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.concurrent.atomic.AtomicInteger; 26 | 27 | /** 28 | * Class represents terminal of Android shell. Let you make requests and change system settings 29 | * 30 | * Note: all request is synchronized 31 | */ 32 | public class Terminal { 33 | 34 | private File mCurrentDirectory; 35 | 36 | private List mRequestHandlers; 37 | 38 | private Map mEnvironmentVars; 39 | 40 | public Terminal() { 41 | mEnvironmentVars = new HashMap<>(System.getenv()); 42 | mCurrentDirectory = new File("/"); 43 | 44 | mRequestHandlers = new ArrayList<>(); 45 | registerRequestHandlers(); 46 | } 47 | 48 | private void registerRequestHandlers() { 49 | mRequestHandlers.add(new BlankCommandHandler()); 50 | mRequestHandlers.add(new CdCommandHandler()); 51 | mRequestHandlers.add(new PwdCommandHandler()); 52 | mRequestHandlers.add(new EchoCommandHandler()); 53 | mRequestHandlers.add(new ExportCommandHandler()); 54 | } 55 | 56 | /** 57 | * Perform request to terminal. Uses only request filed in provided terminalRequest 58 | * @param terminalRequest 59 | * @return complete call with request and corresponding response 60 | */ 61 | public TerminalCall makeNewRequest(TerminalCall terminalRequest) { 62 | String request = terminalRequest.getRequest().trim(); 63 | 64 | for (RequestHandler requestHandler : mRequestHandlers) { 65 | if (requestHandler.isHandleRequest(terminalRequest)) { 66 | requestHandler.handleRequest(terminalRequest, this); 67 | return terminalRequest; 68 | } 69 | } 70 | 71 | try { 72 | Process process = Runtime.getRuntime().exec( 73 | request, 74 | convertEnvironmentVarsToString(mEnvironmentVars), 75 | mCurrentDirectory 76 | ); 77 | terminalRequest.setProcess(process); 78 | } catch (Exception e) { 79 | terminalRequest.setException(new RuntimeException( 80 | String.format("Command '%s' not found", request), 81 | e 82 | )); 83 | } 84 | return terminalRequest; 85 | } 86 | 87 | /** 88 | * Sets current working directory to terminal. Works like cd command 89 | * @param currentDirectory 90 | */ 91 | public void setCurrentDirectory(File currentDirectory) { 92 | mCurrentDirectory = currentDirectory; 93 | } 94 | 95 | public File getCurrentDirectory() { 96 | return mCurrentDirectory; 97 | } 98 | 99 | /** 100 | * Sets environment variables to terminal. Works like export command 101 | * @param name 102 | * @param data 103 | */ 104 | public void addEnvironmentVar(String name, String data) { 105 | mEnvironmentVars.put(name, data); 106 | } 107 | 108 | public String getEnvironmentVar(String name) { 109 | return mEnvironmentVars.get(name); 110 | } 111 | 112 | private String[] convertEnvironmentVarsToString(Map environmentVarsMap) { 113 | if (environmentVarsMap.size() == 0) { 114 | return null; 115 | } 116 | 117 | List output = new ArrayList<>(); 118 | for (String key : environmentVarsMap.keySet()) { 119 | output.add(String.format("%s=%s", key, environmentVarsMap.get(key))); 120 | } 121 | return output.toArray(new String[0]); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/TerminalListener.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal; 2 | 3 | /** 4 | * Listener for terminal requests. Use to get response and supply additional output to terminal 5 | */ 6 | public interface TerminalListener { 7 | 8 | /** 9 | * New part of input data received 10 | * @param input 11 | */ 12 | void onInput(byte[] input); 13 | 14 | /** 15 | * Start reading input stream from response 16 | */ 17 | void onInputStarted(); 18 | 19 | 20 | /** 21 | * Finish reading input stream from response 22 | * @param exception 23 | */ 24 | void onInputFinished(Exception exception); 25 | 26 | 27 | /** 28 | * New part of error data received 29 | * @param error 30 | */ 31 | void onError(byte[] error); 32 | 33 | /** 34 | * Start reading error stream from response 35 | */ 36 | void onErrorStarted(); 37 | 38 | 39 | /** 40 | * Finish reading error stream form response 41 | * @param exception 42 | */ 43 | void onErrorFinished(Exception exception); 44 | 45 | /** 46 | * Called when terminal wait some data to send in command's process 47 | * @return 48 | */ 49 | byte[] onOutput(); 50 | 51 | /** 52 | * Request finished 53 | * @param exception 54 | */ 55 | void onFinished(Exception exception); 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/TerminalService.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal; 2 | 3 | import android.os.Handler; 4 | import android.os.HandlerThread; 5 | import android.os.Looper; 6 | import android.os.Message; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.MainThread; 10 | import androidx.annotation.WorkerThread; 11 | 12 | import com.simonk.projects.taskmanager.entity.TerminalCall; 13 | import com.simonk.projects.taskmanager.terminal.interceptors.TerminalRequestInterceptor; 14 | import com.simonk.projects.taskmanager.terminal.interceptors.TopCommandInterceptor; 15 | import com.simonk.projects.taskmanager.util.ProcessCompat; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Performs async requests to terminal. Retrieve data from terminal and dispatch it to TerminalListener 24 | */ 25 | public class TerminalService { 26 | 27 | private static final int BUFFER_SIZE = 4096; 28 | 29 | private Terminal mTerminal; 30 | private TerminalThread mTerminalThread; 31 | private TerminalHandler mTerminalHandler; 32 | private MainThreadHandler mMainThreadHandler; 33 | 34 | private TerminalListener mTerminalListener; 35 | 36 | private static final int REQUEST_NEW = 1; 37 | private static final int REQUEST_INPUT = 2; 38 | private static final int REQUEST_INPUT_FINISH = 3; 39 | private static final int REQUEST_INPUT_STARTED = 4; 40 | private static final int REQUEST_ERROR = 5; 41 | private static final int REQUEST_ERROR_FINISH = 6; 42 | private static final int REQUEST_ERROR_STARTED = 7; 43 | private static final int REQUEST_FINISH = 8; 44 | private static final int REQUEST_OUTPUT = 9; 45 | 46 | private static byte[] BUFFER = new byte[BUFFER_SIZE]; 47 | 48 | private List mInterceptors; 49 | 50 | private boolean mTerminated = true; 51 | 52 | private TerminalCall mCurrentTerminalCall; 53 | 54 | public TerminalService() { 55 | mTerminal = new Terminal(); 56 | mTerminalThread = new TerminalThread(); 57 | mTerminalThread.start(); 58 | mTerminalHandler = new TerminalHandler(mTerminalThread.getLooper(), this); 59 | mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper(), this); 60 | 61 | mInterceptors = new ArrayList<>(); 62 | mInterceptors.add(new TopCommandInterceptor()); 63 | } 64 | 65 | /** 66 | * Make async terminal request 67 | * @param terminalCall 68 | * @param terminalListener 69 | */ 70 | @MainThread 71 | public void makeTerminalRequest(TerminalCall terminalCall, TerminalListener terminalListener) { 72 | mTerminalListener = terminalListener; 73 | mTerminalHandler.sendMessage(mTerminalHandler.obtainMessage(REQUEST_NEW, terminalCall)); 74 | } 75 | 76 | @MainThread 77 | public void sendOutputString(String output) { 78 | 79 | } 80 | 81 | /** 82 | * Stops currently running terminal request 83 | */ 84 | @MainThread public void stopTerminalRequest() { 85 | mTerminated = true; 86 | } 87 | 88 | @WorkerThread 89 | private void performTerminalRequest(TerminalCall terminalCall) { 90 | clearBuffer(); 91 | mTerminated = false; 92 | 93 | TerminalCall response = mTerminal.makeNewRequest(terminalCall); 94 | mCurrentTerminalCall = response; 95 | 96 | if (response.getException() != null) { 97 | dispatchSendFinish(response.getException()); 98 | return; 99 | } 100 | 101 | TerminalRequestInterceptor requestInterceptor = null; 102 | for (TerminalRequestInterceptor interceptor : mInterceptors) { 103 | if (interceptor.willIntercept(terminalCall)) { 104 | requestInterceptor = interceptor; 105 | break; 106 | } 107 | } 108 | 109 | if (response.getProcess() != null) { 110 | performProcessWork(response, requestInterceptor); 111 | } else { 112 | performInputStreamWork(response, requestInterceptor); 113 | } 114 | 115 | stop(); 116 | } 117 | 118 | private void performInputStreamWork(TerminalCall response, TerminalRequestInterceptor requestInterceptor) { 119 | InputStream contentInputStream = response.getResponseInputStream(); 120 | if (contentInputStream == null) { 121 | return; 122 | } 123 | try { 124 | int length = 0; 125 | while (length != -1) { 126 | if (mTerminated) { 127 | stop(); 128 | return; 129 | } 130 | length = contentInputStream.read(BUFFER); 131 | if (requestInterceptor != null) { 132 | requestInterceptor.interceptInput(this, response, BUFFER); 133 | } else { 134 | dispatchSendInput(BUFFER); 135 | } 136 | } 137 | dispatchSendFinishInput(null); 138 | } catch (IOException exception) { 139 | dispatchSendFinishInput(exception); 140 | } 141 | } 142 | 143 | private void performProcessWork(TerminalCall response, TerminalRequestInterceptor requestInterceptor) { 144 | Process process = response.getProcess(); 145 | performInputStreamWork(response, requestInterceptor); 146 | if (mTerminated) { 147 | process.destroy(); 148 | return; 149 | } 150 | 151 | InputStream errorInputStream = response.getResponseErrorStream(); 152 | try { 153 | int length = 0; 154 | while (length != -1) { 155 | if (mTerminated) { 156 | stop(); 157 | return; 158 | } 159 | length = errorInputStream.read(BUFFER); 160 | dispatchSendInput(BUFFER); 161 | } 162 | dispatchSendFinishInput(null); 163 | } catch (IOException exception) { 164 | dispatchSendFinishInput(exception); 165 | } 166 | } 167 | 168 | public void dispatchSendInput(byte[] buffer) { 169 | mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(REQUEST_INPUT, buffer)); 170 | } 171 | 172 | public void dispatchSendFinishInput(Exception exception) { 173 | mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(REQUEST_INPUT_FINISH, exception)); 174 | } 175 | 176 | private void dispatchSendError(byte[] buffer) { 177 | mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(REQUEST_ERROR, buffer)); 178 | } 179 | 180 | public void dispatchSendFinishError(Exception exception) { 181 | mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(REQUEST_ERROR_FINISH, exception)); 182 | } 183 | 184 | public void dispatchSendFinish(Exception exception) { 185 | mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(REQUEST_FINISH, exception)); 186 | } 187 | 188 | @WorkerThread 189 | private void performOutput(String output) { 190 | if (mCurrentTerminalCall != null) { 191 | try { 192 | mCurrentTerminalCall.getResponseOutputStream().write(output.getBytes()); 193 | } catch (IOException ignore) { 194 | } 195 | } 196 | } 197 | 198 | @WorkerThread 199 | private void stop() { 200 | mTerminated = true; 201 | dispatchSendFinish(null); 202 | clearBuffer(); 203 | } 204 | 205 | @MainThread 206 | private void dispatchInput(byte[] input) { 207 | if (mTerminalListener != null) { 208 | mTerminalListener.onInput(input); 209 | } 210 | } 211 | 212 | @MainThread 213 | private void dispatchInputStarted() { 214 | if (mTerminalListener != null) { 215 | mTerminalListener.onInputStarted(); 216 | } 217 | } 218 | 219 | @MainThread 220 | private void dispatchErrorFinished(Exception exception) { 221 | if (mTerminalListener != null) { 222 | mTerminalListener.onErrorFinished(exception); 223 | } 224 | } 225 | 226 | @MainThread 227 | private void dispatchError(byte[] input) { 228 | if (mTerminalListener != null) { 229 | mTerminalListener.onError(input); 230 | } 231 | } 232 | 233 | @MainThread 234 | private void dispatchErrorStarted() { 235 | if (mTerminalListener != null) { 236 | mTerminalListener.onErrorStarted(); 237 | } 238 | } 239 | 240 | @MainThread 241 | private void dispatchInputFinished(Exception exception) { 242 | if (mTerminalListener != null) { 243 | mTerminalListener.onInputFinished(exception); 244 | } 245 | } 246 | 247 | @MainThread 248 | private void dispatchFinished(Exception exception) { 249 | if (mTerminalListener != null) { 250 | mTerminalListener.onFinished(exception); 251 | } 252 | } 253 | 254 | private void clearBuffer() { 255 | BUFFER = new byte[BUFFER_SIZE]; 256 | } 257 | 258 | /** 259 | * Handler to send information in background service thread 260 | */ 261 | private static class TerminalHandler extends Handler { 262 | private final TerminalService service; 263 | 264 | TerminalHandler(Looper looper, TerminalService dispatcher) { 265 | super(looper); 266 | this.service = dispatcher; 267 | } 268 | 269 | @Override 270 | public void handleMessage(final Message msg) { 271 | switch (msg.what) { 272 | case REQUEST_NEW: 273 | service.performTerminalRequest((TerminalCall) msg.obj); 274 | break; 275 | case REQUEST_OUTPUT: 276 | service.performOutput((String) msg.obj); 277 | break; 278 | } 279 | } 280 | } 281 | 282 | /** 283 | * Handler to send information in main ui thread 284 | */ 285 | private static class MainThreadHandler extends Handler { 286 | private final TerminalService service; 287 | 288 | MainThreadHandler(Looper looper, TerminalService dispatcher) { 289 | super(looper); 290 | this.service = dispatcher; 291 | } 292 | 293 | @Override 294 | public void handleMessage(final Message msg) { 295 | switch (msg.what) { 296 | case REQUEST_INPUT: 297 | service.dispatchInput((byte[]) msg.obj); 298 | break; 299 | case REQUEST_INPUT_FINISH: 300 | service.dispatchInputFinished((Exception) msg.obj); 301 | break; 302 | case REQUEST_INPUT_STARTED: 303 | service.dispatchInputStarted(); 304 | break; 305 | case REQUEST_FINISH: 306 | service.dispatchFinished((Exception) msg.obj); 307 | break; 308 | case REQUEST_ERROR: 309 | service.dispatchError((byte[]) msg.obj); 310 | break; 311 | case REQUEST_ERROR_FINISH: 312 | service.dispatchErrorFinished((Exception) msg.obj); 313 | break; 314 | case REQUEST_ERROR_STARTED: 315 | service.dispatchErrorStarted(); 316 | break; 317 | } 318 | } 319 | } 320 | 321 | private static class TerminalThread extends HandlerThread { 322 | public TerminalThread() { 323 | super("TaskManager-Terminal"); 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/handlers/BlankCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.handlers; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | 6 | public class BlankCommandHandler extends RequestHandler { 7 | 8 | @Override 9 | public boolean isHandleRequest(TerminalCall call) { 10 | return call.getRequest().isEmpty(); 11 | } 12 | 13 | @Override 14 | public void handleRequest(TerminalCall call, Terminal terminal) { 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/handlers/CdCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.handlers; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | 6 | import java.io.File; 7 | 8 | public class CdCommandHandler extends RequestHandler { 9 | 10 | private static final String COMMAND = "cd"; 11 | 12 | @Override 13 | public boolean isHandleRequest(TerminalCall call) { 14 | return requestMatch(call.getRequest(), COMMAND); 15 | } 16 | 17 | @Override 18 | public void handleRequest(TerminalCall call, Terminal terminal) { 19 | String request = call.getRequest(); 20 | if (request.length() == 2) { 21 | return; 22 | } 23 | String newPath = request.substring(3); // remove 'cd ' part from request 24 | 25 | if (newPath.isEmpty()) { 26 | return; 27 | } 28 | 29 | if (newPath.equals("/")) { 30 | terminal.setCurrentDirectory(new File("/")); 31 | return; 32 | } 33 | 34 | String[] subPaths = newPath.split("/"); 35 | 36 | File currentDirectory; 37 | if (subPaths[0].isEmpty()) { 38 | currentDirectory = new File("/"); 39 | } else { 40 | currentDirectory = terminal.getCurrentDirectory(); 41 | } 42 | for (String subPath : subPaths) { 43 | currentDirectory = handleSubpath(currentDirectory, subPath); 44 | } 45 | terminal.setCurrentDirectory(currentDirectory); 46 | } 47 | 48 | private File handleSubpath(File currentDirectory, String subPath) { 49 | if (subPath.equals(".")) { 50 | return currentDirectory; 51 | } 52 | 53 | if (subPath.equals("..")) { 54 | File parent = currentDirectory.getParentFile(); 55 | if (parent == null) { 56 | return currentDirectory; 57 | } 58 | return parent; 59 | } 60 | 61 | return new File(currentDirectory, subPath); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/handlers/EchoCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.handlers; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | 6 | import java.io.ByteArrayInputStream; 7 | 8 | public class EchoCommandHandler extends RequestHandler { 9 | 10 | private static final String COMMAND = "echo"; 11 | 12 | @Override 13 | public boolean isHandleRequest(TerminalCall call) { 14 | return requestMatch(call.getRequest(), COMMAND); 15 | } 16 | 17 | @Override 18 | public void handleRequest(TerminalCall call, Terminal terminal) { 19 | String request = call.getRequest(); 20 | String vars = request.substring(COMMAND.length() + 1); 21 | String[] subVars = vars.split(" "); 22 | 23 | StringBuilder responseBuilder = new StringBuilder(); 24 | for (String subVar : subVars) { 25 | int dollarIndex = subVar.indexOf('$'); 26 | if (dollarIndex != -1) { 27 | subVar = subVar.substring(dollarIndex + 1); 28 | } 29 | String value = terminal.getEnvironmentVar(subVar); 30 | if (value != null) { 31 | responseBuilder.append(value); 32 | } 33 | } 34 | 35 | call.setResponseInputStream(new ByteArrayInputStream(responseBuilder.toString().getBytes())); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/handlers/ExportCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.handlers; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | 6 | public class ExportCommandHandler extends RequestHandler { 7 | 8 | private static final String COMMAND = "export"; 9 | 10 | @Override 11 | public boolean isHandleRequest(TerminalCall call) { 12 | return requestMatch(call.getRequest(), COMMAND); 13 | } 14 | 15 | @Override 16 | public void handleRequest(TerminalCall call, Terminal terminal) { 17 | String request = call.getRequest(); 18 | String vars = request.substring(COMMAND.length() + 1); 19 | String[] subVars = vars.split(" "); 20 | 21 | for (String var : subVars) { 22 | String[] varAssignment = var.split("="); 23 | if (varAssignment.length != 2) { 24 | continue; 25 | } 26 | 27 | String name = varAssignment[0]; 28 | String value = varAssignment[1]; 29 | terminal.addEnvironmentVar(name, value); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/handlers/PwdCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.handlers; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | 6 | import java.io.ByteArrayInputStream; 7 | 8 | 9 | public class PwdCommandHandler extends RequestHandler { 10 | 11 | private static final String COMMAND = "pwd"; 12 | 13 | @Override 14 | public boolean isHandleRequest(TerminalCall call) { 15 | return requestMatch(call.getRequest(), COMMAND); 16 | } 17 | 18 | @Override 19 | public void handleRequest(TerminalCall call, Terminal terminal) { 20 | call.setResponseInputStream(new ByteArrayInputStream(terminal.getCurrentDirectory().getAbsolutePath().getBytes())); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/handlers/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.handlers; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | 6 | /** 7 | * Interface lets you to handle a terminal request by yourself. 8 | * Don't forget to register it in Terminal 9 | */ 10 | public abstract class RequestHandler { 11 | 12 | /** 13 | * Checks if call should be handled by this handler 14 | * @param call 15 | * @return 16 | */ 17 | public abstract boolean isHandleRequest(TerminalCall call); 18 | 19 | /** 20 | * Handle request. Will be called only if isHandleRequest returned true 21 | * @param call 22 | * @param terminal 23 | */ 24 | public abstract void handleRequest(TerminalCall call, Terminal terminal); 25 | 26 | /** 27 | * Util method. Checks is there is command in request 28 | * @param requestString 29 | * @param command 30 | * @return 31 | */ 32 | boolean requestMatch(String requestString, String command) { 33 | String[] words = requestString.split(" "); 34 | return words.length > 0 && words[0].equals(command); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/interceptors/TerminalRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.interceptors; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | import com.simonk.projects.taskmanager.terminal.TerminalService; 6 | 7 | /** 8 | * Interface lets you intercept terminal response and work with response data in other way 9 | */ 10 | public interface TerminalRequestInterceptor { 11 | 12 | boolean willIntercept(TerminalCall terminalCall); 13 | 14 | void interceptInput(TerminalService terminalService, TerminalCall terminalCall, byte[] data); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/terminal/interceptors/TopCommandInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.terminal.interceptors; 2 | 3 | import com.simonk.projects.taskmanager.entity.TerminalCall; 4 | import com.simonk.projects.taskmanager.terminal.Terminal; 5 | import com.simonk.projects.taskmanager.terminal.TerminalService; 6 | 7 | /** 8 | * Interceptor read data and represents it as string. 9 | * When it faces special separator it sends whole data up to previous separator to listener 10 | */ 11 | public class TopCommandInterceptor implements TerminalRequestInterceptor { 12 | 13 | private static final String COMMAND = "top"; 14 | 15 | private String mData = ""; 16 | 17 | @Override 18 | public boolean willIntercept(TerminalCall terminalCall) { 19 | String[] words = terminalCall.getRequest().split(" "); 20 | return words.length > 0 && words[0].equals(COMMAND); 21 | } 22 | 23 | @Override 24 | public void interceptInput(TerminalService terminalService, TerminalCall terminalCall, byte[] data) { 25 | String stringRepr = new String(data); 26 | int deviderIndex = stringRepr.indexOf("\n\n\n"); 27 | if (deviderIndex != -1) { 28 | mData += stringRepr.substring(0, deviderIndex); 29 | terminalService.dispatchSendInput(mData.getBytes()); 30 | mData = stringRepr.substring(deviderIndex); 31 | } else { 32 | mData += stringRepr; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/BindingActivity.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.appcompat.app.AppCompatActivity; 6 | import androidx.databinding.ViewDataBinding; 7 | 8 | /** 9 | * Simple activity to support binding 10 | */ 11 | public abstract class BindingActivity extends AppCompatActivity { 12 | 13 | private ViewDataBinding mBinding; 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | mBinding = initBinding(); 19 | if (mBinding == null) { 20 | throw new IllegalStateException("Binding can not be null"); 21 | } 22 | setContentView(mBinding.getRoot()); 23 | } 24 | 25 | /** 26 | * Child must implement this method to retrieve corresponding binding 27 | * @return 28 | */ 29 | protected abstract ViewDataBinding initBinding(); 30 | 31 | /** 32 | * Child can override this method to specify binding subclass it want 33 | * @return 34 | */ 35 | public ViewDataBinding getBinding() { 36 | return mBinding; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.app.ActivityManager; 5 | import android.content.Context; 6 | import android.content.pm.ApplicationInfo; 7 | import android.content.pm.PackageManager; 8 | import android.graphics.Color; 9 | import android.graphics.drawable.ColorDrawable; 10 | import android.graphics.drawable.Drawable; 11 | import android.os.Bundle; 12 | import android.os.Handler; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.Button; 17 | import android.widget.FrameLayout; 18 | import android.widget.ImageView; 19 | import android.widget.TextView; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.appcompat.widget.Toolbar; 23 | import androidx.databinding.DataBindingUtil; 24 | import androidx.databinding.ViewDataBinding; 25 | import androidx.fragment.app.Fragment; 26 | import androidx.fragment.app.FragmentManager; 27 | import androidx.fragment.app.FragmentPagerAdapter; 28 | import androidx.lifecycle.Lifecycle; 29 | import androidx.lifecycle.ViewModelProviders; 30 | import androidx.recyclerview.widget.LinearLayoutManager; 31 | import androidx.recyclerview.widget.RecyclerView; 32 | import androidx.viewpager.widget.ViewPager; 33 | 34 | import com.google.android.material.tabs.TabLayout; 35 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 36 | import com.simonk.projects.taskmanager.repository.ProcessRepository; 37 | import com.simonk.projects.taskmanager.ui.blacklist.BlacklistFragment; 38 | import com.simonk.projects.taskmanager.ui.process.ChangeDetailsDialog; 39 | import com.simonk.projects.taskmanager.ui.process.CleanedDialog; 40 | import com.simonk.projects.taskmanager.ui.process.ProcessFragment; 41 | import com.simonk.projects.taskmanager.ui.process.viewmodels.ProcessViewModel; 42 | import com.simonk.projects.taskmanager.ui.terminal.TerminalFragment; 43 | import com.simonk.projects.taskmanager.ui.util.ObjectListAdapter; 44 | import com.simonk.projects.taskmanager.R; 45 | import com.simonk.projects.taskmanager.databinding.ActivityMainBinding; 46 | import com.simonk.projects.taskmanager.util.MemoryUtils; 47 | 48 | import java.io.Serializable; 49 | import java.util.ArrayList; 50 | import java.util.List; 51 | 52 | public class MainActivity extends BindingActivity { 53 | 54 | @Override 55 | public ActivityMainBinding getBinding() { 56 | return (ActivityMainBinding) super.getBinding(); 57 | } 58 | 59 | @Override 60 | public ViewDataBinding initBinding() { 61 | return DataBindingUtil.inflate(getLayoutInflater(), R.layout.activity_main, null, false); 62 | } 63 | 64 | @Override 65 | public void onCreate(Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | 68 | ViewPager viewPager = getBinding().viewPager; 69 | viewPager.setOffscreenPageLimit(2); 70 | setupViewPager(viewPager); 71 | 72 | TabLayout tabLayout = getBinding().tabLayout; 73 | tabLayout.setupWithViewPager(viewPager); 74 | } 75 | 76 | private void setupViewPager(ViewPager viewPager) { 77 | ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager(), getBinding().coordinator); 78 | adapter.addFragment(ProcessFragment.newInstance(), 79 | getString(R.string.apps)); 80 | adapter.addFragment(BlacklistFragment.newInstance(), 81 | getString(R.string.blacklist)); 82 | adapter.addFragment(TerminalFragment.newInstance(), 83 | getString(R.string.terminal)); 84 | viewPager.setAdapter(adapter); 85 | } 86 | 87 | private static class ViewPagerAdapter extends FragmentPagerAdapter { 88 | private final List mFragmentList = new ArrayList<>(); 89 | private final List mFragmentTitleList = new ArrayList<>(); 90 | 91 | private View mRootView; 92 | 93 | public ViewPagerAdapter(FragmentManager manager, View root) { 94 | super(manager); 95 | mRootView = root; 96 | } 97 | 98 | @Override 99 | public Fragment getItem(int position) { 100 | return mFragmentList.get(position); 101 | } 102 | 103 | @Override 104 | public int getCount() { 105 | return mFragmentList.size(); 106 | } 107 | 108 | public void addFragment(Fragment fragment, String title) { 109 | mFragmentList.add(fragment); 110 | mFragmentTitleList.add(title); 111 | } 112 | 113 | @Override 114 | public CharSequence getPageTitle(int position) { 115 | return mFragmentTitleList.get(position); 116 | } 117 | 118 | @Override 119 | public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { 120 | super.setPrimaryItem(container, position, object); 121 | if (object instanceof TerminalFragment) { 122 | ((TerminalFragment) object).setCurrent(true); 123 | } else { 124 | ((TerminalFragment) mFragmentList.get(2)).setCurrent(false); 125 | } 126 | if (mRootView.getBackground() instanceof ColorDrawable) { 127 | int installedColor = ((ColorDrawable) mRootView.getBackground()).getColor(); 128 | int newColor = installedColor; 129 | if (position == 2) { 130 | newColor = Color.parseColor("#455A64"); 131 | } else { 132 | newColor = mRootView.getResources().getColor(R.color.colorPrimary); 133 | 134 | } 135 | 136 | if (installedColor != newColor) { 137 | ValueAnimator valueAnimator = ValueAnimator.ofArgb(installedColor, newColor); 138 | valueAnimator.setDuration(300); 139 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 140 | @Override 141 | public void onAnimationUpdate(ValueAnimator animation) { 142 | mRootView.setBackground(new ColorDrawable((Integer) animation.getAnimatedValue())); 143 | } 144 | }); 145 | valueAnimator.start(); 146 | } 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/blacklist/BlackListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.blacklist; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.Button; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | import com.simonk.projects.taskmanager.R; 14 | import com.simonk.projects.taskmanager.entity.AppInfo; 15 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 16 | import com.simonk.projects.taskmanager.ui.process.ProcessAdapter; 17 | import com.simonk.projects.taskmanager.ui.util.ObjectListAdapter; 18 | 19 | import java.text.SimpleDateFormat; 20 | import java.util.ArrayList; 21 | import java.util.Calendar; 22 | import java.util.Collections; 23 | import java.util.List; 24 | 25 | public class BlackListAdapter extends ObjectListAdapter { 26 | 27 | private BlacklistAdapterViewHolder.OnClickListener mItemClickListener; 28 | private BlacklistAdapterViewHolder.OnBlockListener mItemBlockListener; 29 | 30 | private static final int BLACK_ITEM_TYPE = 1; 31 | private static final int ALL_ITEM_TYPE = 2; 32 | private static final int BLACK_LABEL_ITEM_TYPE = 3; 33 | private static final int ALL_LABEL_ITEM_TYPE = 4; 34 | 35 | public static class AdapterDataHolder { 36 | public AppInfo appInfo; 37 | public boolean isBlacklistLabel; 38 | public boolean isAllAppsLabel; 39 | 40 | AdapterDataHolder(AppInfo appInfo) { 41 | this.appInfo = appInfo; 42 | } 43 | } 44 | 45 | public void setAppInfoList(List items) { 46 | List adapterDataHolderList = new ArrayList<>(); 47 | 48 | AdapterDataHolder blacklistLabel = new AdapterDataHolder(null); 49 | blacklistLabel.isBlacklistLabel = true; 50 | adapterDataHolderList.add(blacklistLabel); 51 | 52 | for (AppInfo appInfo : items) { 53 | if (appInfo.isInBlacklist()) { 54 | adapterDataHolderList.add(new AdapterDataHolder(appInfo)); 55 | } 56 | } 57 | 58 | AdapterDataHolder allAppsLabel = new AdapterDataHolder(null); 59 | allAppsLabel.isAllAppsLabel = true; 60 | adapterDataHolderList.add(allAppsLabel); 61 | 62 | for (AppInfo appInfo : items) { 63 | if (!appInfo.isInBlacklist()) { 64 | adapterDataHolderList.add(new AdapterDataHolder(appInfo)); 65 | } 66 | } 67 | 68 | setItemsList(adapterDataHolderList); 69 | } 70 | 71 | @NonNull 72 | @Override 73 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 74 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 75 | if (viewType == BLACK_ITEM_TYPE || viewType == ALL_ITEM_TYPE) { 76 | View v = inflater.inflate(R.layout.blacklist_item, parent, false); 77 | return new BlacklistAdapterViewHolder(v, mItemClickListener, mItemBlockListener); 78 | } 79 | if (viewType == BLACK_LABEL_ITEM_TYPE) { 80 | TextView label = (TextView) inflater.inflate(R.layout.blacklist_item_label, parent, false); 81 | label.setText(parent.getResources().getString(R.string.blacklist_label)); 82 | return new LabelViewHolder(label); 83 | } 84 | if (viewType == ALL_LABEL_ITEM_TYPE) { 85 | TextView label = (TextView) inflater.inflate(R.layout.blacklist_item_label, parent, false); 86 | label.setText(parent.getResources().getString(R.string.all_apps_label)); 87 | return new LabelViewHolder(label); 88 | } 89 | return null; 90 | } 91 | 92 | @Override 93 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 94 | if (holder.getItemViewType() == BLACK_ITEM_TYPE || holder.getItemViewType() == ALL_ITEM_TYPE) { 95 | ((BlacklistAdapterViewHolder)holder).bind(getItem(position).appInfo); 96 | } 97 | } 98 | 99 | @Override 100 | public int getItemViewType(int position) { 101 | AdapterDataHolder dataHolder = getItem(position); 102 | if (dataHolder.isAllAppsLabel) { 103 | return ALL_LABEL_ITEM_TYPE; 104 | } 105 | if (dataHolder.isBlacklistLabel) { 106 | return BLACK_LABEL_ITEM_TYPE; 107 | } 108 | if (dataHolder.appInfo.isInBlacklist()) { 109 | return BLACK_ITEM_TYPE; 110 | } 111 | return ALL_ITEM_TYPE; 112 | } 113 | 114 | public void setItemClickListener(BlacklistAdapterViewHolder.OnClickListener onClickListener) { 115 | mItemClickListener = onClickListener; 116 | } 117 | 118 | public void setItemBlockListener(BlacklistAdapterViewHolder.OnBlockListener onBlockListener) { 119 | mItemBlockListener = onBlockListener; 120 | } 121 | 122 | public static class BlacklistAdapterViewHolder extends RecyclerView.ViewHolder { 123 | 124 | private TextView mName; 125 | private ImageView mImage; 126 | private TextView mPackage; 127 | private Button mActionButton; 128 | private TextView mLastOpenDate; 129 | 130 | private AppInfo mItem; 131 | 132 | private OnBlockListener mOnBlockListener; 133 | 134 | public interface OnClickListener { 135 | void onClick(View v, AppInfo ppackage); 136 | boolean onLongClick(AppInfo info); 137 | } 138 | 139 | public interface OnBlockListener { 140 | void onBlock(AppInfo appInfo); 141 | void onRemove(AppInfo appInfo); 142 | } 143 | 144 | public BlacklistAdapterViewHolder(@NonNull View itemView, 145 | OnClickListener onClickListener, 146 | OnBlockListener onBlockListener) { 147 | super(itemView); 148 | mName = itemView.findViewById(R.id.blacklist_item_text); 149 | mImage = itemView.findViewById(R.id.blacklist_item_logo); 150 | mPackage = itemView.findViewById(R.id.blacklist_item_package); 151 | mActionButton = itemView.findViewById(R.id.blacklist_action); 152 | mLastOpenDate = itemView.findViewById(R.id.blacklist_last_open); 153 | itemView.setOnClickListener(v -> { 154 | if (onClickListener != null) { 155 | onClickListener.onClick(v, mItem); 156 | } 157 | }); 158 | itemView.setOnLongClickListener(v -> { 159 | if (onClickListener != null) { 160 | return onClickListener.onLongClick(mItem); 161 | } 162 | return false; 163 | }); 164 | mOnBlockListener = onBlockListener; 165 | } 166 | 167 | public void bind(AppInfo info) { 168 | mItem = info; 169 | mName.setText(info.getText()); 170 | mImage.setImageDrawable(info.getImage()); 171 | mPackage.setText(info.getPpackage()); 172 | if (info.isInBlacklist()) { 173 | mActionButton.setText(itemView.getResources().getString(R.string.remove)); 174 | mActionButton.setOnClickListener((v) -> { 175 | if (mOnBlockListener != null) { 176 | mOnBlockListener.onRemove(mItem); 177 | } 178 | }); 179 | } else { 180 | mActionButton.setText(itemView.getResources().getString(R.string.block)); 181 | mActionButton.setOnClickListener((v) -> { 182 | if (mOnBlockListener != null) { 183 | mOnBlockListener.onBlock(mItem); 184 | } 185 | }); 186 | } 187 | if (info.getLastOpenDate() != 0) { 188 | Calendar calendar = Calendar.getInstance(); 189 | calendar.setTimeInMillis(info.getLastOpenDate()); 190 | String date = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(calendar.getTime()); 191 | mLastOpenDate.setVisibility(View.VISIBLE); 192 | mLastOpenDate.setText(itemView.getResources().getString(R.string.last_open_date, date)); 193 | } else { 194 | mLastOpenDate.setVisibility(View.GONE); 195 | } 196 | } 197 | } 198 | 199 | public static class LabelViewHolder extends RecyclerView.ViewHolder { 200 | public LabelViewHolder(@NonNull View itemView) { 201 | super(itemView); 202 | } 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/blacklist/BlacklistFragment.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.blacklist; 2 | 3 | import android.app.job.JobInfo; 4 | import android.app.job.JobScheduler; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.os.Bundle; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.databinding.DataBindingUtil; 14 | import androidx.fragment.app.Fragment; 15 | import androidx.lifecycle.ViewModelProviders; 16 | import androidx.recyclerview.widget.LinearLayoutManager; 17 | import androidx.recyclerview.widget.RecyclerView; 18 | 19 | import com.simonk.projects.taskmanager.R; 20 | import com.simonk.projects.taskmanager.databinding.FragmentBlacklistBinding; 21 | import com.simonk.projects.taskmanager.databinding.FragmentProcessBinding; 22 | import com.simonk.projects.taskmanager.entity.AppInfo; 23 | import com.simonk.projects.taskmanager.services.blacklist.BlacklistJobService; 24 | import com.simonk.projects.taskmanager.ui.blacklist.viewmodels.BlacklistViewModel; 25 | import com.simonk.projects.taskmanager.ui.process.ProcessFragment; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | public class BlacklistFragment extends Fragment { 31 | 32 | private static final int JOB_SCHEDULE_ID = 1000; 33 | 34 | private RecyclerView mBlacklistRecyclerView; 35 | 36 | private BlackListAdapter mBlackListAdapter; 37 | 38 | private BlacklistViewModel mViewModel; 39 | 40 | public static BlacklistFragment newInstance() { 41 | BlacklistFragment fragment = new BlacklistFragment(); 42 | return fragment; 43 | } 44 | 45 | @Override 46 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle saveInstanceState) { 47 | View root = bindingView(inflater, container); 48 | 49 | mViewModel = ViewModelProviders.of(this).get(BlacklistViewModel.class); 50 | 51 | mBlacklistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), 52 | RecyclerView.VERTICAL, false)); 53 | mBlackListAdapter = new BlackListAdapter(); 54 | mBlacklistRecyclerView.setAdapter(mBlackListAdapter); 55 | 56 | mBlackListAdapter.setItemBlockListener(new BlackListAdapter.BlacklistAdapterViewHolder.OnBlockListener() { 57 | @Override 58 | public void onBlock(AppInfo appInfo) { 59 | mViewModel.insertAppInBlacklist(appInfo); 60 | } 61 | 62 | @Override 63 | public void onRemove(AppInfo appInfo) { 64 | mViewModel.deleteAppFromBlackList(appInfo); 65 | } 66 | }); 67 | 68 | runBlacklistScheduler(); 69 | 70 | mViewModel.getAppsInfo().observe(this, this::updateAppsInfo); 71 | 72 | return root; 73 | } 74 | 75 | private View bindingView(LayoutInflater inflater, ViewGroup container) { 76 | FragmentBlacklistBinding binding 77 | = DataBindingUtil.inflate(inflater, R.layout.fragment_blacklist, container, false); 78 | 79 | mBlacklistRecyclerView = binding.blacklistApps; 80 | 81 | return binding.getRoot(); 82 | } 83 | 84 | private void updateAppsInfo(BlacklistViewModel.AppsInfoList appsInfoList) { 85 | List infoList = new ArrayList<>(); 86 | if (appsInfoList.allApps != null) { 87 | infoList.addAll(appsInfoList.allApps); 88 | } 89 | if (appsInfoList.blacklistApps != null) { 90 | infoList.addAll(appsInfoList.blacklistApps); 91 | } 92 | mBlackListAdapter.resolveActionChange(() -> { 93 | mBlackListAdapter.setAppInfoList(infoList); 94 | }); 95 | } 96 | 97 | private void runBlacklistScheduler() { 98 | JobScheduler jobScheduler = 99 | (JobScheduler) requireContext().getSystemService(Context.JOB_SCHEDULER_SERVICE); 100 | jobScheduler.schedule(new JobInfo.Builder(JOB_SCHEDULE_ID, 101 | new ComponentName(requireContext(), BlacklistJobService.class)) 102 | .setPeriodic(10*1000) 103 | .build()); 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/blacklist/viewmodels/BlacklistViewModel.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.blacklist.viewmodels; 2 | 3 | import android.app.Application; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.lifecycle.AndroidViewModel; 7 | import androidx.lifecycle.LiveData; 8 | import androidx.lifecycle.MediatorLiveData; 9 | import androidx.lifecycle.MutableLiveData; 10 | 11 | import com.simonk.projects.taskmanager.entity.AppInfo; 12 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 13 | import com.simonk.projects.taskmanager.repository.BlacklistRepository; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public class BlacklistViewModel extends AndroidViewModel { 19 | 20 | private BlacklistRepository mRepository; 21 | 22 | private MutableLiveData> mAllAppsInfo; 23 | private LiveData> mBlacklistInfo; 24 | 25 | private MediatorLiveData mAppsInfo; 26 | 27 | public BlacklistViewModel(@NonNull Application application) { 28 | super(application); 29 | 30 | mRepository = new BlacklistRepository(); 31 | 32 | mAllAppsInfo = new MutableLiveData<>(); 33 | mBlacklistInfo = new MutableLiveData<>(); 34 | 35 | mAllAppsInfo.setValue(mRepository.getAllInstalledApplicationsInfo(application, true)); 36 | mBlacklistInfo = mRepository.getAllBlacklistApplicationInfo(application); 37 | 38 | // important: appsInfo initialization must be after allAppsInfo and blacklistInfo initialization 39 | mAppsInfo = new MediatorLiveData<>(); 40 | mAppsInfo.addSource(mAllAppsInfo, value -> { 41 | AppsInfoList appsInfoList = mAppsInfo.getValue(); 42 | if (appsInfoList == null) { 43 | appsInfoList = new AppsInfoList(); 44 | } 45 | appsInfoList.allApps = value; 46 | mAppsInfo.setValue(appsInfoList); 47 | }); 48 | mAppsInfo.addSource(mBlacklistInfo, value -> { 49 | AppsInfoList appsInfoList = mAppsInfo.getValue(); 50 | if (appsInfoList == null) { 51 | appsInfoList = new AppsInfoList(); 52 | } 53 | appsInfoList.blacklistApps = value; 54 | if (appsInfoList.blacklistApps != null && appsInfoList.allApps != null) { 55 | for (AppInfo info : value) { 56 | for (int i = 0; i < appsInfoList.allApps.size(); i++) { 57 | if (appsInfoList.allApps.get(i).getPpackage().equals(info.getPpackage())) { 58 | appsInfoList.allApps.remove(i); 59 | i--; 60 | } 61 | } 62 | } 63 | } 64 | mAppsInfo.setValue(appsInfoList); 65 | }); 66 | } 67 | 68 | public LiveData> getAllAppsInfo() { 69 | return mAllAppsInfo; 70 | } 71 | 72 | public LiveData> getBlacklistInfo() { 73 | return mBlacklistInfo; 74 | } 75 | 76 | public LiveData getAppsInfo() { 77 | return mAppsInfo; 78 | } 79 | 80 | public void insertAppInBlacklist(AppInfo appInfo) { 81 | mRepository.insertAppInBlacklist(getApplication(), appInfo); 82 | } 83 | 84 | public void deleteAppFromBlackList(AppInfo appInfo) { 85 | mRepository.deleteAppFromBlackList(getApplication(), appInfo); 86 | } 87 | 88 | public static class AppsInfoList { 89 | public List allApps; 90 | public List blacklistApps; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/process/ChangeDetailsDialog.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.process; 2 | 3 | import android.app.AlertDialog; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.os.Bundle; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.widget.EditText; 11 | 12 | import androidx.fragment.app.DialogFragment; 13 | 14 | import com.simonk.projects.taskmanager.R; 15 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 16 | import com.simonk.projects.taskmanager.ui.MainActivity; 17 | 18 | public class ChangeDetailsDialog extends DialogFragment { 19 | 20 | public static final String PROCESS_INFO_ARG = "PROCESS_INFO_ARG"; 21 | 22 | public interface OnChanged { 23 | void onChanged(ProcessInfo info, int priority); 24 | } 25 | 26 | private OnChanged mOnChanged; 27 | 28 | @Override 29 | public void onAttach(Context context) { 30 | super.onAttach(context); 31 | if (context instanceof OnChanged) { 32 | mOnChanged = (OnChanged) context; 33 | } 34 | } 35 | 36 | @Override 37 | public Dialog onCreateDialog(Bundle onSaveInstanceState) { 38 | LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); 39 | View root = layoutInflater.inflate(R.layout.change_details_dialog, null); 40 | 41 | ProcessInfo processInfo = 42 | (ProcessInfo)getArguments().getSerializable(PROCESS_INFO_ARG); 43 | 44 | EditText editText = root.findViewById(R.id.details_priority); 45 | editText.setText(""+processInfo.getPriority()); 46 | 47 | AlertDialog dialog = new AlertDialog.Builder(getActivity()) 48 | .setView(root) 49 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 50 | @Override 51 | public void onClick(DialogInterface dialog, int which) { 52 | if (mOnChanged != null) { 53 | String p = editText.getText().toString(); 54 | int ip = Integer.parseInt(p); 55 | mOnChanged.onChanged(processInfo, ip); 56 | } 57 | } 58 | }) 59 | .create(); 60 | return dialog; 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/process/CleanedDialog.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.process; 2 | 3 | import android.app.ActivityManager; 4 | import android.app.AlertDialog; 5 | import android.app.Dialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.os.Bundle; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.widget.TextView; 12 | 13 | import androidx.fragment.app.DialogFragment; 14 | 15 | import com.simonk.projects.taskmanager.R; 16 | import com.simonk.projects.taskmanager.repository.ProcessRepository; 17 | import com.simonk.projects.taskmanager.util.MemoryUtils; 18 | 19 | import java.util.List; 20 | 21 | public class CleanedDialog extends DialogFragment { 22 | 23 | public interface OnCleaned { 24 | void onCleaned(); 25 | } 26 | 27 | private OnCleaned mOnCleaned; 28 | 29 | @Override 30 | public void onAttach(Context context) { 31 | super.onAttach(context); 32 | if (context instanceof OnCleaned) { 33 | mOnCleaned = (OnCleaned) context; 34 | } 35 | } 36 | 37 | @Override 38 | public Dialog onCreateDialog(Bundle onSaveInstanceState) { 39 | LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); 40 | View root = layoutInflater.inflate(R.layout.cleaned_dialog, null); 41 | 42 | ActivityManager.MemoryInfo memInfo = MemoryUtils.getMemoryInfo(requireContext()); 43 | float beforeAvailMemory = MemoryUtils.getAvailableMemory(memInfo); 44 | ((TextView)root.findViewById(R.id.before_memory)).setText(getString(R.string.before_cleaning, beforeAvailMemory)); 45 | 46 | new ProcessRepository().killAllProcesses(requireContext()); 47 | 48 | memInfo = MemoryUtils.getMemoryInfo(requireContext()); 49 | float afterAvailMemory = MemoryUtils.getAvailableMemory(memInfo); 50 | ((TextView)root.findViewById(R.id.after_memory)).setText(getString(R.string.after_cleaning, afterAvailMemory)); 51 | 52 | ((TextView)root.findViewById(R.id.percent_memory)).setText((100 - ((int)(beforeAvailMemory * 100 / afterAvailMemory))) + "%"); 53 | 54 | AlertDialog dialog = new AlertDialog.Builder(getActivity()) 55 | .setView(root) 56 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 57 | @Override 58 | public void onClick(DialogInterface dialog, int which) { 59 | if (mOnCleaned != null) { 60 | mOnCleaned.onCleaned(); 61 | } 62 | } 63 | }) 64 | .create(); 65 | return dialog; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/process/ProcessAdapter.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.process; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | import com.simonk.projects.taskmanager.R; 13 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 14 | import com.simonk.projects.taskmanager.ui.util.ObjectListAdapter; 15 | 16 | public class ProcessAdapter extends ObjectListAdapter { 17 | 18 | private ProcessAdapterViewHolder.OnClickListener mItemClickListener; 19 | 20 | @NonNull 21 | @Override 22 | public ProcessAdapterViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { 23 | LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); 24 | View v = inflater.inflate(R.layout.process_list_item, viewGroup, false); 25 | 26 | return new ProcessAdapterViewHolder(v, mItemClickListener); 27 | } 28 | 29 | @Override 30 | public void onBindViewHolder(@NonNull ProcessAdapterViewHolder processAdapterViewHolder, int i) { 31 | processAdapterViewHolder.bind(getItem(i)); 32 | } 33 | 34 | public void setItemClickListener(ProcessAdapterViewHolder.OnClickListener onClickListener) { 35 | mItemClickListener = onClickListener; 36 | } 37 | 38 | public static class ProcessAdapterViewHolder extends RecyclerView.ViewHolder { 39 | 40 | private TextView mName; 41 | private ImageView mImage; 42 | private TextView mPackage; 43 | 44 | private ProcessInfo mItem; 45 | 46 | public interface OnClickListener { 47 | void onClick(View v, ProcessInfo ppackage); 48 | boolean onLongClick(ProcessInfo info); 49 | } 50 | 51 | public ProcessAdapterViewHolder(@NonNull View itemView, OnClickListener onClickListener) { 52 | super(itemView); 53 | mName = itemView.findViewById(R.id.process_list_item_text); 54 | mImage = itemView.findViewById(R.id.process_list_item_logo); 55 | mPackage = itemView.findViewById(R.id.process_list_item_package); 56 | itemView.setOnClickListener(v -> { 57 | if (onClickListener != null) { 58 | onClickListener.onClick(v, mItem); 59 | } 60 | }); 61 | itemView.setOnLongClickListener(v -> { 62 | if (onClickListener != null) { 63 | return onClickListener.onLongClick(mItem); 64 | } 65 | return false; 66 | }); 67 | } 68 | 69 | public void bind(ProcessInfo info) { 70 | mItem = info; 71 | mName.setText(info.getText()); 72 | mImage.setImageDrawable(info.getImage()); 73 | mPackage.setText(info.getPpackage()); 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/process/ProcessFragment.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.process; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.Button; 8 | import android.widget.FrameLayout; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.databinding.DataBindingUtil; 13 | import androidx.fragment.app.Fragment; 14 | import androidx.lifecycle.ViewModelProviders; 15 | import androidx.recyclerview.widget.LinearLayoutManager; 16 | import androidx.recyclerview.widget.RecyclerView; 17 | 18 | import com.simonk.projects.taskmanager.R; 19 | import com.simonk.projects.taskmanager.databinding.FragmentProcessBinding; 20 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 21 | import com.simonk.projects.taskmanager.ui.process.viewmodels.ProcessViewModel; 22 | 23 | import java.util.List; 24 | 25 | public class ProcessFragment extends Fragment 26 | implements CleanedDialog.OnCleaned, ChangeDetailsDialog.OnChanged { 27 | 28 | private RecyclerView mAppsRecyclerView; 29 | private Button mClearAllButton; 30 | private TextView mAllMemory; 31 | private TextView mFreeMemory; 32 | private TextView mPercentMemory; 33 | 34 | private ProcessAdapter mProcessAdapter; 35 | 36 | private ProcessViewModel mViewModel; 37 | 38 | public static ProcessFragment newInstance() { 39 | ProcessFragment fragment = new ProcessFragment(); 40 | return fragment; 41 | } 42 | 43 | @Override 44 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle saveInstanceState) { 45 | View root = bindingView(inflater, container); 46 | 47 | mAppsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), 48 | RecyclerView.VERTICAL, false)); 49 | mProcessAdapter = new ProcessAdapter(); 50 | mAppsRecyclerView.setAdapter(mProcessAdapter); 51 | 52 | mClearAllButton.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | CleanedDialog cleanedDialog = new CleanedDialog(); 56 | cleanedDialog.setTargetFragment(ProcessFragment.this, 0); 57 | cleanedDialog.show(requireFragmentManager(), "CleanedDialog"); 58 | } 59 | }); 60 | 61 | mProcessAdapter.setItemClickListener(new ProcessAdapter.ProcessAdapterViewHolder.OnClickListener() { 62 | @Override 63 | public void onClick(View v, ProcessInfo info) { 64 | ViewGroup detailsView = (ViewGroup) 65 | LayoutInflater.from(requireContext()).inflate(R.layout.process_list_item_details, null); 66 | if (((FrameLayout) v.findViewById(R.id.process_list_item_details)).getChildCount() == 0) { 67 | ((TextView) detailsView.findViewById(R.id.priority)).setText(getString(R.string.priority, String.valueOf(info.getPriority()))); 68 | ((TextView) detailsView.findViewById(R.id.status)).setText(getString(R.string.enabled, String.valueOf(info.isStatus()))); 69 | ((TextView) detailsView.findViewById(R.id.uid)).setText(getString(R.string.uid, String.valueOf(info.isStatus()))); 70 | ((TextView) detailsView.findViewById(R.id.min_sdk)).setText(getString(R.string.target_sdk, String.valueOf(info.getMinSdk()))); 71 | ((TextView) detailsView.findViewById(R.id.description)).setText(getString(R.string.description, info.getDescription())); 72 | ((FrameLayout) v.findViewById(R.id.process_list_item_details)).addView(detailsView); 73 | ((Button) detailsView.findViewById(R.id.kill)).setOnClickListener(new View.OnClickListener() { 74 | @Override 75 | public void onClick(View v) { 76 | mViewModel.getRepository().killProcess(requireContext(), info); 77 | updateUi(); 78 | } 79 | }); 80 | } else { 81 | ((FrameLayout) v.findViewById(R.id.process_list_item_details)).removeAllViews(); 82 | } 83 | } 84 | 85 | @Override 86 | public boolean onLongClick(ProcessInfo info) { 87 | Bundle bundle = new Bundle(); 88 | bundle.putSerializable(ChangeDetailsDialog.PROCESS_INFO_ARG, info); 89 | ChangeDetailsDialog changeDetailsDialog = new ChangeDetailsDialog(); 90 | changeDetailsDialog.setArguments(bundle); 91 | changeDetailsDialog.show(requireFragmentManager(), "ChangeDetailsDialog"); 92 | return false; 93 | } 94 | }); 95 | 96 | mViewModel = ViewModelProviders.of(this).get(ProcessViewModel.class); 97 | mViewModel.getAllProcessInfo().observe(this, this::updateProcessAdapter); 98 | 99 | return root; 100 | } 101 | 102 | private View bindingView(LayoutInflater inflater, ViewGroup container) { 103 | FragmentProcessBinding binding 104 | = DataBindingUtil.inflate(inflater, R.layout.fragment_process, container, false); 105 | 106 | mAppsRecyclerView = binding.activityMainRecycler; 107 | mClearAllButton = binding.clearAll; 108 | mAllMemory = binding.allMemory; 109 | mPercentMemory = binding.percentMemory; 110 | mFreeMemory = binding.freeMemory; 111 | 112 | return binding.getRoot(); 113 | } 114 | 115 | @Override 116 | public void onResume() { 117 | super.onResume(); 118 | updateUi(); 119 | } 120 | 121 | private void updateUi() { 122 | float allMemory = mViewModel.getTotalMemory(); 123 | float availMemory = mViewModel.getAvailableMemory(); 124 | mAllMemory.setText("" + allMemory + "G"); 125 | mFreeMemory.setText("" + availMemory + "G"); 126 | mPercentMemory.setText("" + (int)(availMemory * 100 / allMemory) + "%"); 127 | 128 | mViewModel.forceUpdateProcessInfo(); 129 | } 130 | 131 | private void updateProcessAdapter(List processInfoList) { 132 | mProcessAdapter.resolveActionChange(() -> { 133 | mProcessAdapter.setItemsList(processInfoList); 134 | }); 135 | } 136 | 137 | @Override 138 | public void onCleaned() { 139 | updateUi(); 140 | } 141 | 142 | @Override 143 | public void onChanged(ProcessInfo info, int priority) { 144 | mViewModel.getRepository().changeProcessPriority(requireContext(), info, priority); 145 | updateUi(); 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/process/viewmodels/ProcessViewModel.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.process.viewmodels; 2 | 3 | import android.app.ActivityManager; 4 | import android.app.Application; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.lifecycle.AndroidViewModel; 8 | import androidx.lifecycle.LiveData; 9 | import androidx.lifecycle.MutableLiveData; 10 | 11 | import com.simonk.projects.taskmanager.entity.ProcessInfo; 12 | import com.simonk.projects.taskmanager.repository.ProcessRepository; 13 | import com.simonk.projects.taskmanager.util.MemoryUtils; 14 | 15 | import java.util.List; 16 | 17 | public class ProcessViewModel extends AndroidViewModel { 18 | 19 | private ProcessRepository mRepository; 20 | 21 | private MutableLiveData> mAllProcessInfo; 22 | 23 | public ProcessViewModel(@NonNull Application application) { 24 | super(application); 25 | 26 | mRepository = new ProcessRepository(); 27 | 28 | mAllProcessInfo = new MutableLiveData<>(); 29 | mAllProcessInfo.setValue(mRepository.getAllProcessInfo(application)); 30 | } 31 | 32 | public ProcessRepository getRepository() { 33 | return mRepository; 34 | } 35 | 36 | public float getAvailableMemory() { 37 | ActivityManager.MemoryInfo memInfo = MemoryUtils.getMemoryInfo(getApplication()); 38 | return MemoryUtils.getAvailableMemory(memInfo); 39 | } 40 | 41 | public float getTotalMemory() { 42 | ActivityManager.MemoryInfo memInfo = MemoryUtils.getMemoryInfo(getApplication()); 43 | return MemoryUtils.getTotalMemory(memInfo); 44 | } 45 | 46 | public void forceUpdateProcessInfo() { 47 | mAllProcessInfo.setValue(mRepository.getAllProcessInfo(getApplication())); 48 | } 49 | 50 | public LiveData> getAllProcessInfo() { 51 | return mAllProcessInfo; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/terminal/TerminalFragment.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.terminal; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.graphics.drawable.ColorDrawable; 6 | import android.os.Bundle; 7 | import android.text.Editable; 8 | import android.text.Spannable; 9 | import android.text.SpannableStringBuilder; 10 | import android.text.TextWatcher; 11 | import android.text.style.ForegroundColorSpan; 12 | import android.util.AttributeSet; 13 | import android.util.TypedValue; 14 | import android.view.Gravity; 15 | import android.view.LayoutInflater; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.Button; 19 | import android.widget.EditText; 20 | import android.widget.LinearLayout; 21 | import android.widget.TextView; 22 | 23 | import androidx.annotation.NonNull; 24 | import androidx.appcompat.widget.AppCompatTextView; 25 | import androidx.databinding.DataBindingUtil; 26 | import androidx.fragment.app.Fragment; 27 | import androidx.lifecycle.ViewModelProviders; 28 | 29 | import com.simonk.projects.taskmanager.R; 30 | import com.simonk.projects.taskmanager.database.entity.TerminalSnapshotEntity; 31 | import com.simonk.projects.taskmanager.databinding.FragmentTerminalBinding; 32 | import com.simonk.projects.taskmanager.terminal.StringTerminalListener; 33 | import com.simonk.projects.taskmanager.ui.terminal.viewmodels.TerminalViewModel; 34 | 35 | import java.util.List; 36 | 37 | public class TerminalFragment extends Fragment { 38 | 39 | private ViewGroup mRootView; 40 | private Button mStopButton; 41 | private Button mClearButton; 42 | 43 | private TerminalViewModel mViewModel; 44 | 45 | /* 46 | In some cases (like app start) we dont want to request a focus to not show keyboard when it's wrong time 47 | */ 48 | private boolean mCanRequestFocus = true; 49 | 50 | public static TerminalFragment newInstance() { 51 | TerminalFragment fragment = new TerminalFragment(); 52 | return fragment; 53 | } 54 | 55 | @Override 56 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle saveInstanceState) { 57 | View root = bindingView(inflater, container); 58 | 59 | mStopButton.setOnClickListener((v) -> { 60 | mViewModel.stopTerminalRequest(); 61 | }); 62 | 63 | mClearButton.setOnClickListener((v) -> { 64 | mViewModel.clearTerminalSnapshots(); 65 | }); 66 | 67 | mViewModel = ViewModelProviders.of(this).get(TerminalViewModel.class); 68 | mViewModel.getTerminalSnapshots().observe(this, this::updateUi); 69 | 70 | 71 | return root; 72 | } 73 | 74 | /** 75 | * Notify fragment that it became to primari in pager 76 | * @param current 77 | */ 78 | public void setCurrent(boolean current) { 79 | mCanRequestFocus = current; 80 | } 81 | 82 | private View bindingView(LayoutInflater inflater, ViewGroup container) { 83 | FragmentTerminalBinding binding 84 | = DataBindingUtil.inflate(inflater, R.layout.fragment_terminal, container, false); 85 | 86 | mRootView = binding.terminalRoot; 87 | mStopButton = binding.terminalStopButton; 88 | mClearButton = binding.terminalClearButton; 89 | 90 | return binding.getRoot(); 91 | } 92 | 93 | private void updateUi(List requestList) { 94 | mRootView.removeAllViews(); 95 | for (TerminalSnapshotEntity snapshot : requestList) { 96 | EditText editText = addEditTextLayout(mRootView); 97 | editText.setText(snapshot.request); 98 | editText.setEnabled(false); 99 | TextView textView = addResponseTextView(); 100 | textView.setText(snapshot.response); 101 | } 102 | addEditTextLayout(mRootView); 103 | } 104 | 105 | private class TerminalTextWatcher implements TextWatcher { 106 | 107 | @Override 108 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 109 | 110 | } 111 | 112 | @Override 113 | public void onTextChanged(CharSequence s, int start, int before, int count) { 114 | String change = s.subSequence(start, start + count).toString(); 115 | if (change.equals("\n")) { 116 | makeTerminalRequest(s.toString()); 117 | } 118 | } 119 | 120 | @Override 121 | public void afterTextChanged(Editable s) { 122 | String text = s.toString(); 123 | if (text.contains("\n")) { 124 | s.clear(); 125 | s.append(text.replace("\n", "")); 126 | } 127 | } 128 | } 129 | 130 | private void makeTerminalRequest(String request) { 131 | blockAllEditTexts(mRootView); 132 | TextView responseTextView = addResponseTextView(); 133 | mViewModel.makeNewTerminalRequest(request, new StringTerminalListener() { 134 | 135 | private void printAsError(TextView textView, String error) { 136 | String text = textView.getText().toString(); 137 | SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text); 138 | spannableStringBuilder.append(error); 139 | spannableStringBuilder.setSpan(new ForegroundColorSpan(Color.RED), 140 | text.length(), 141 | text.length()+error.length(), 142 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 143 | textView.setText(spannableStringBuilder); 144 | } 145 | 146 | @Override 147 | public void onInput(String input) { 148 | responseTextView.setText(input); 149 | } 150 | 151 | @Override 152 | public void onInputStarted() { 153 | responseTextView.setText(""); 154 | } 155 | 156 | @Override 157 | public void onInputFinished(Exception exception) { 158 | if (exception != null) { 159 | printAsError(responseTextView, exception.getMessage()); 160 | } 161 | } 162 | 163 | @Override 164 | public void onError(String input) { 165 | printAsError(responseTextView, input); 166 | } 167 | 168 | @Override 169 | public void onErrorStarted() { 170 | } 171 | 172 | @Override 173 | public void onErrorFinished(Exception exception) { 174 | if (exception != null) { 175 | printAsError(responseTextView, exception.getMessage()); 176 | } 177 | } 178 | 179 | @Override 180 | public String onOutputString() { 181 | return null; 182 | } 183 | 184 | @Override 185 | public void onFinished(Exception exception) { 186 | if (exception != null) { 187 | printAsError(responseTextView, exception.getMessage()); 188 | } 189 | finishRequest(); 190 | } 191 | }); 192 | } 193 | 194 | private void sendOutputString(String output) { 195 | mViewModel.sendOutputString(output); 196 | } 197 | 198 | private void finishRequest() { 199 | mViewModel.addTerminalSnapshot(new TerminalSnapshotEntity( 200 | findLastEditText(mRootView).getText().toString(), 201 | findLastTextView(mRootView).getText().toString() 202 | )); 203 | blockAllEditTexts(mRootView); 204 | addEditTextLayout(mRootView); 205 | } 206 | 207 | private void blockAllEditTexts(ViewGroup parent) { 208 | for (int i = 0; i < parent.getChildCount(); i++) { 209 | View child = parent.getChildAt(i); 210 | if (child instanceof ViewGroup) { 211 | blockAllEditTexts((ViewGroup) child); 212 | } 213 | if (child instanceof EditText) { 214 | child.setEnabled(false); 215 | if (mCanRequestFocus) { 216 | child.requestFocus(); 217 | } 218 | } 219 | } 220 | } 221 | 222 | private EditText findLastEditText(ViewGroup parent) { 223 | for (int i = parent.getChildCount()-1; i >= 0; i--) { 224 | View child = parent.getChildAt(i); 225 | if (child instanceof ViewGroup) { 226 | EditText editText = findLastEditText((ViewGroup) child); 227 | if (editText != null) { 228 | return editText; 229 | } 230 | } 231 | if (child instanceof EditText) { 232 | return (EditText)child; 233 | } 234 | } 235 | return null; 236 | } 237 | 238 | private TextView findLastTextView(ViewGroup parent) { 239 | for (int i = parent.getChildCount()-1; i >= 0; i--) { 240 | View child = parent.getChildAt(i); 241 | if (child instanceof ViewGroup) { 242 | TextView textView = findLastTextView((ViewGroup) child); 243 | if (textView != null) { 244 | return textView; 245 | } 246 | } 247 | if (child instanceof TextView) { 248 | return (TextView)child; 249 | } 250 | } 251 | return null; 252 | } 253 | 254 | private TextView addResponseTextView() { 255 | int margin16 = (int) (16 * getResources().getDisplayMetrics().density); 256 | 257 | TextView textView = new TextView(requireContext()); 258 | LinearLayout.LayoutParams textViewParams = new LinearLayout.LayoutParams( 259 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 260 | textViewParams.leftMargin = margin16; 261 | textViewParams.rightMargin = margin16; 262 | textViewParams.bottomMargin = margin16; 263 | 264 | textView.setTextColor(Color.parseColor("#FFFFFF")); 265 | 266 | mRootView.addView(textView, textViewParams); 267 | return textView; 268 | } 269 | 270 | private EditText addEditTextLayout(ViewGroup parent) { 271 | int margin16 = (int) (16 * getResources().getDisplayMetrics().density); 272 | int margin8 = (int) (8 * getResources().getDisplayMetrics().density); 273 | 274 | EditText terminalEditText = new EditText(requireContext()); 275 | terminalEditText.addTextChangedListener(new TerminalTextWatcher()); 276 | LinearLayout.LayoutParams terminalEditTextParams = new LinearLayout.LayoutParams( 277 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 278 | terminalEditTextParams.leftMargin = margin16; 279 | terminalEditTextParams.topMargin = margin8; 280 | terminalEditTextParams.rightMargin = margin16; 281 | terminalEditText.setLayoutParams(terminalEditTextParams); 282 | 283 | terminalEditText.setBackground(null); 284 | terminalEditText.setTextColor(Color.parseColor("#FFFFFF")); 285 | 286 | if (mCanRequestFocus) { 287 | terminalEditText.requestFocus(); 288 | } 289 | 290 | SignTextView signTextView = new SignTextView(requireContext()); 291 | 292 | View devider = new View(requireContext()); 293 | ViewGroup.MarginLayoutParams deviderParams = new ViewGroup.MarginLayoutParams( 294 | ViewGroup.LayoutParams.MATCH_PARENT, (int) (getResources().getDisplayMetrics().density)); 295 | devider.setLayoutParams(deviderParams); 296 | devider.setBackground(new ColorDrawable(Color.parseColor("#37474f"))); 297 | 298 | LinearLayout editTextLayout = new LinearLayout(requireContext()); 299 | editTextLayout.setOrientation(LinearLayout.HORIZONTAL); 300 | ViewGroup.MarginLayoutParams editTextLayoutParams = new ViewGroup.MarginLayoutParams( 301 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 302 | editTextLayout.setLayoutParams(editTextLayoutParams); 303 | 304 | editTextLayout.addView(signTextView); 305 | editTextLayout.addView(terminalEditText); 306 | 307 | editTextLayout.setOnClickListener((v) -> { 308 | String text = ((EditText)((ViewGroup)v).getChildAt(1)).getText().toString(); 309 | EditText lastEditText = findLastEditText(mRootView); 310 | if (lastEditText != v && lastEditText.isEnabled()) { 311 | lastEditText.setText(text); 312 | } 313 | }); 314 | 315 | parent.addView(devider); 316 | parent.addView(editTextLayout); 317 | 318 | return terminalEditText; 319 | } 320 | 321 | private static class SignTextView extends AppCompatTextView { 322 | public SignTextView(Context context) { 323 | this(context, null); 324 | } 325 | 326 | public SignTextView(Context context, AttributeSet attrs) { 327 | this(context, attrs, 0); 328 | } 329 | 330 | public SignTextView(Context context, AttributeSet attrs, int defStyleAttr) { 331 | super(context, attrs, defStyleAttr); 332 | init(); 333 | } 334 | 335 | private void init() { 336 | int margin = (int) (8 * getResources().getDisplayMetrics().density); 337 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( 338 | ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 339 | params.leftMargin = margin; 340 | params.gravity = Gravity.CENTER_VERTICAL; 341 | 342 | setLayoutParams(params); 343 | 344 | setText(">>"); 345 | setTextColor(Color.parseColor("#009688")); 346 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); 347 | } 348 | } 349 | 350 | } 351 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/terminal/viewmodels/TerminalViewModel.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.terminal.viewmodels; 2 | 3 | import android.app.Application; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.lifecycle.AndroidViewModel; 7 | import androidx.lifecycle.LiveData; 8 | import androidx.lifecycle.MutableLiveData; 9 | import androidx.lifecycle.Observer; 10 | 11 | import com.simonk.projects.taskmanager.database.DatabaseManager; 12 | import com.simonk.projects.taskmanager.database.LocalDatabase; 13 | import com.simonk.projects.taskmanager.database.entity.TerminalSnapshotEntity; 14 | import com.simonk.projects.taskmanager.entity.TerminalCall; 15 | import com.simonk.projects.taskmanager.terminal.StringTerminalListener; 16 | import com.simonk.projects.taskmanager.terminal.TerminalService; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | public class TerminalViewModel extends AndroidViewModel { 22 | 23 | private MutableLiveData> mTerminalSnapshotsArray; 24 | 25 | private TerminalService mTerminalService; 26 | 27 | public TerminalViewModel(@NonNull Application application) { 28 | super(application); 29 | 30 | mTerminalService = new TerminalService(); 31 | 32 | mTerminalSnapshotsArray = new MutableLiveData<>(); 33 | mTerminalSnapshotsArray.setValue(new ArrayList<>()); 34 | 35 | LiveData> databaseSnapshots = DatabaseManager.loadSnapshots(application); 36 | databaseSnapshots.observeForever(new Observer>() { 37 | @Override 38 | public void onChanged(List snapshotEntityList) { 39 | mTerminalSnapshotsArray.setValue(snapshotEntityList); 40 | databaseSnapshots.removeObserver(this); 41 | } 42 | }); 43 | } 44 | 45 | public void makeNewTerminalRequest(String requestString, StringTerminalListener terminalListener) { 46 | TerminalCall request = new TerminalCall(); 47 | request.setRequest(requestString); 48 | mTerminalService.makeTerminalRequest(request, terminalListener); 49 | } 50 | 51 | public void stopTerminalRequest() { 52 | mTerminalService.stopTerminalRequest(); 53 | } 54 | 55 | public void clearTerminalSnapshots() { 56 | stopTerminalRequest(); 57 | mTerminalSnapshotsArray.setValue(new ArrayList<>()); 58 | DatabaseManager.deleteAllSnapshots(getApplication()); 59 | } 60 | 61 | public LiveData> getTerminalSnapshots() { 62 | return mTerminalSnapshotsArray; 63 | } 64 | 65 | public void addTerminalSnapshot(TerminalSnapshotEntity snapshot) { 66 | mTerminalSnapshotsArray.getValue().add(snapshot); 67 | DatabaseManager.insertSnapshot(getApplication(), snapshot); 68 | } 69 | 70 | public void sendOutputString(String output) { 71 | mTerminalService.sendOutputString(output); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/ui/util/ObjectListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.ui.util; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcelable; 5 | import android.util.SparseArray; 6 | 7 | import androidx.recyclerview.widget.DiffUtil; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | import java.io.Serializable; 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Collections; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | 17 | public abstract class ObjectListAdapter extends RecyclerView.Adapter { 18 | 19 | private static final String INITIAL_ITEMS_PARCELABLE_KEY = "INITIAL_ITEMS_PARCELABLE_KEY"; 20 | private static final String INITIAL_ITEMS_SERIALIZABLE_KEY = "INITIAL_ITEMS_SERIALIZABLE_KEY"; 21 | 22 | private static final String FILTERED_ITEMS_PARCELABLE_KEY = "FILTERED_ITEMS_PARCELABLE_KEY"; 23 | private static final String FILTERED_ITEMS_SERIALIZABLE_KEY = "FILTERED_ITEMS_SERIALIZABLE_KEY"; 24 | 25 | private static final String FILTER_REQUEST_CODE_KEY = "FILTER_CONDITION_KEY"; 26 | private static final String FILTER_CONDITION_KEY = "FILTER_CONDITION_KEY"; 27 | 28 | private List mFilteredItems; //------- 29 | private List mInitialItemsBeforeFiltering; 30 | private SparseArray> mFilterConditions; 31 | private Comparator mSortComparator; 32 | 33 | public ObjectListAdapter() { 34 | mInitialItemsBeforeFiltering = new ArrayList<>(); 35 | mFilterConditions = new SparseArray<>(); 36 | } 37 | 38 | public interface FilterCondition extends Serializable { 39 | boolean isMatch(F item); 40 | } 41 | 42 | private void refilter() { 43 | if (mFilterConditions.size() == 0) { 44 | cancelFiltering(); 45 | return; 46 | } 47 | 48 | List itemsToFilter = mInitialItemsBeforeFiltering; 49 | for(int i = 0; i < mFilterConditions.size(); i++) { 50 | itemsToFilter = processFiltering(itemsToFilter, mFilterConditions.valueAt(i)); 51 | } 52 | mFilteredItems = itemsToFilter; 53 | } 54 | 55 | protected void filter(FilterCondition filterCondition, int requestCode) { 56 | if (filterCondition == null) { 57 | return; 58 | } 59 | 60 | if (isFiltered(requestCode)) { 61 | mFilterConditions.delete(requestCode); 62 | refilter(); 63 | } 64 | 65 | mFilteredItems = processFiltering(getItemsList(), filterCondition); 66 | mFilterConditions.append(requestCode, filterCondition); 67 | } 68 | 69 | protected void sort(Comparator comparator) { 70 | mSortComparator = comparator; 71 | if (mInitialItemsBeforeFiltering != null) { 72 | Collections.sort(mInitialItemsBeforeFiltering, comparator); 73 | } 74 | if (mFilteredItems != null) { 75 | Collections.sort(mFilteredItems, comparator); 76 | } 77 | } 78 | 79 | private List processFiltering(List itemsToFilter, FilterCondition filterCondition) { 80 | List filteredItems = new ArrayList<>(); 81 | 82 | for (T item : itemsToFilter) { 83 | if (filterCondition.isMatch(item)) { 84 | filteredItems.add(item); 85 | } 86 | } 87 | 88 | return filteredItems; 89 | } 90 | 91 | public void cancelFiltering() { 92 | mFilterConditions.clear(); 93 | if (mFilteredItems != null) { 94 | mFilteredItems = null; 95 | } 96 | } 97 | 98 | protected void cancelFiltering(int requestCode) { 99 | mFilterConditions.delete(requestCode); 100 | if (mFilterConditions.size() == 0) { 101 | cancelFiltering(); 102 | } else { 103 | refilter(); 104 | } 105 | } 106 | 107 | protected boolean isFiltered(int requestCode) { 108 | return mFilterConditions.indexOfKey(requestCode) != -1; 109 | } 110 | 111 | public boolean isFiltered() { 112 | return mFilteredItems != null; 113 | } 114 | 115 | public void setItemsList(List items) { 116 | mInitialItemsBeforeFiltering.clear(); 117 | mInitialItemsBeforeFiltering.addAll(items); 118 | 119 | if (mSortComparator != null) { 120 | sort(mSortComparator); 121 | } 122 | 123 | refilter(); 124 | } 125 | 126 | public List getItemsList() { 127 | if (mFilteredItems == null) { 128 | return new ArrayList<>(mInitialItemsBeforeFiltering); 129 | } 130 | return new ArrayList<>(mFilteredItems); 131 | } 132 | 133 | public List getItemsListBeforeFiltering() { 134 | return mInitialItemsBeforeFiltering; 135 | } 136 | 137 | @Override 138 | public int getItemCount() { 139 | return getItemsList() != null ? getItemsList().size() : 0; 140 | } 141 | 142 | public T getItem(int index) { 143 | if (index >= getItemsList().size()) { 144 | return null; 145 | } 146 | 147 | return getItemsList().get(index); 148 | } 149 | 150 | public void resolveActionChange(Runnable action) { 151 | List before = getItemsList(); 152 | action.run(); 153 | resolveItemsListChange(before); 154 | } 155 | 156 | public void resolveItemsListChange(List oldList) { 157 | List newList = getItemsList(); 158 | if (oldList == null || newList == null) { 159 | return; 160 | } 161 | 162 | DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtilImpl(oldList, newList), true); 163 | result.dispatchUpdatesTo(this); 164 | } 165 | 166 | protected boolean isItemsContentEquals(T first, T second) { 167 | return first.equals(second); 168 | } 169 | 170 | private class DiffUtilImpl extends DiffUtil.Callback { 171 | 172 | private List mOldList; 173 | private List mNewList; 174 | 175 | public DiffUtilImpl(List oldList, List newList) { 176 | mOldList = oldList; 177 | mNewList = newList; 178 | } 179 | 180 | @Override 181 | public int getOldListSize() { 182 | return mOldList.size(); 183 | } 184 | 185 | @Override 186 | public int getNewListSize() { 187 | return mNewList.size(); 188 | } 189 | 190 | @Override 191 | public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 192 | return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition)); 193 | } 194 | 195 | @Override 196 | public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 197 | return isItemsContentEquals(mOldList.get(oldItemPosition), mNewList.get(newItemPosition)); 198 | } 199 | } 200 | 201 | @SuppressWarnings("unchecked") 202 | public Bundle packAllItems() { 203 | if (mInitialItemsBeforeFiltering.isEmpty()) { 204 | return null; 205 | } 206 | 207 | boolean itParcelable; 208 | boolean itSerializable; 209 | 210 | try { 211 | Parcelable parcelable = (Parcelable) mInitialItemsBeforeFiltering.get(0); 212 | itParcelable = true; 213 | } catch (ClassCastException e) { 214 | itParcelable = false; 215 | } 216 | 217 | try { 218 | Serializable serializable = (Serializable) mInitialItemsBeforeFiltering.get(0); 219 | itSerializable = true; 220 | } catch (ClassCastException e) { 221 | itSerializable = false; 222 | } 223 | 224 | Bundle itemsBox = new Bundle(); 225 | 226 | if (mFilterConditions.size() != 0) { 227 | int[] requestCodes = new int[mFilterConditions.size()]; 228 | FilterCondition[] filterConditions = new FilterCondition[mFilterConditions.size()]; 229 | for (int i = 0; i < mFilterConditions.size(); i++) { 230 | requestCodes[i] = mFilterConditions.keyAt(i); 231 | filterConditions[i] = mFilterConditions.valueAt(i); 232 | } 233 | itemsBox.putIntArray(FILTER_REQUEST_CODE_KEY, requestCodes); 234 | itemsBox.putSerializable(FILTER_CONDITION_KEY, filterConditions); 235 | } 236 | 237 | if (itParcelable) { 238 | List initialParcelableList = (List) mInitialItemsBeforeFiltering; 239 | Parcelable[] parcelables = initialParcelableList.toArray(new Parcelable[] {}); 240 | itemsBox.putParcelableArray(INITIAL_ITEMS_PARCELABLE_KEY, parcelables); 241 | 242 | if (isFiltered()) { 243 | List filteredParcelableList = (List) mFilteredItems; 244 | parcelables = filteredParcelableList.toArray(new Parcelable[]{}); 245 | itemsBox.putParcelableArray(FILTERED_ITEMS_PARCELABLE_KEY, parcelables); 246 | } 247 | 248 | return itemsBox; 249 | } 250 | if (itSerializable) { 251 | List initialSerializableList = (List) mInitialItemsBeforeFiltering; 252 | Serializable[] serializables = initialSerializableList.toArray(new Serializable[] {}); 253 | itemsBox.putSerializable(INITIAL_ITEMS_SERIALIZABLE_KEY, serializables); 254 | 255 | if (mFilteredItems != null) { 256 | List filteredSerializableList = (List) mFilteredItems; 257 | serializables = filteredSerializableList.toArray(new Serializable[]{}); 258 | itemsBox.putSerializable(FILTERED_ITEMS_SERIALIZABLE_KEY, serializables); 259 | } 260 | 261 | return itemsBox; 262 | } 263 | 264 | return null; 265 | } 266 | 267 | @SuppressWarnings("unchecked") 268 | public void unpackAllItems(Bundle itemsBox) { 269 | if (itemsBox == null) { 270 | return; 271 | } 272 | 273 | int[] requestCodes = itemsBox.getIntArray(FILTER_REQUEST_CODE_KEY); 274 | FilterCondition[] filterConditions = (FilterCondition[]) itemsBox.getSerializable(FILTER_CONDITION_KEY); 275 | if (requestCodes != null && filterConditions != null) { 276 | for (int i = 0; i < requestCodes.length && i < filterConditions.length; i++) { 277 | mFilterConditions.append(requestCodes[i], filterConditions[i]); 278 | } 279 | } 280 | 281 | Parcelable[] initialParcelables = itemsBox.getParcelableArray(INITIAL_ITEMS_PARCELABLE_KEY); 282 | Parcelable[] filteredParcelables = itemsBox.getParcelableArray(FILTERED_ITEMS_PARCELABLE_KEY); 283 | if (initialParcelables != null) { 284 | if (filteredParcelables != null && mFilterConditions.size() != 0) { 285 | /* избегаем излишней фильтрации */ 286 | List fixedSizeInitialList = (List) Arrays.asList(initialParcelables); 287 | List fixedSizeFilteredList = (List) Arrays.asList(filteredParcelables); 288 | mInitialItemsBeforeFiltering = new ArrayList<>(fixedSizeInitialList); 289 | mFilteredItems = new ArrayList<>(fixedSizeFilteredList); 290 | } else { 291 | setItemsList((List) Arrays.asList(initialParcelables)); 292 | } 293 | } 294 | 295 | Serializable[] initialSerializables = 296 | (Serializable[]) itemsBox.getSerializable(INITIAL_ITEMS_SERIALIZABLE_KEY); 297 | Serializable[] filteredSerializables = 298 | (Serializable[]) itemsBox.getSerializable(FILTERED_ITEMS_SERIALIZABLE_KEY); 299 | if (initialSerializables != null) { 300 | if (filteredSerializables != null && mFilterConditions.size() != 0) { 301 | /* избегаем излишней фильтрации */ 302 | List fixedSizeInitialList = (List) Arrays.asList(initialSerializables); 303 | List fixedSizeFilteredList = (List) Arrays.asList(filteredSerializables); 304 | mInitialItemsBeforeFiltering = new ArrayList<>(fixedSizeInitialList); 305 | mFilteredItems = new ArrayList<>(fixedSizeFilteredList); 306 | } else { 307 | setItemsList((List) Arrays.asList(initialSerializables)); 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/util/MemoryUtils.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.util; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | 6 | public class MemoryUtils { 7 | 8 | private static final double B_TO_GB = 1024d * 1024d * 1024d; 9 | 10 | public static ActivityManager.MemoryInfo getMemoryInfo(Context context) { 11 | ActivityManager actManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 12 | ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); 13 | 14 | actManager.getMemoryInfo(memInfo); 15 | return memInfo; 16 | } 17 | 18 | public static float getAvailableMemory(ActivityManager.MemoryInfo memoryInfo) { 19 | return ((int) (memoryInfo.availMem / B_TO_GB * 100)) / 100.0f; 20 | } 21 | 22 | public static float getTotalMemory(ActivityManager.MemoryInfo memoryInfo) { 23 | return ((int) (memoryInfo.totalMem / B_TO_GB * 100)) / 100.0f; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/simonk/projects/taskmanager/util/ProcessCompat.java: -------------------------------------------------------------------------------- 1 | package com.simonk.projects.taskmanager.util; 2 | 3 | public class ProcessCompat { 4 | 5 | public static boolean isAlive(Process process) { 6 | try { 7 | process.exitValue(); 8 | return false; 9 | } catch(IllegalThreadStateException e) { 10 | return true; 11 | } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonKab/Android-Process-Manager/e5c4ae3626beb2080bd49dc09c23d06bfdabb0c1/app/src/main/res/drawable-hdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonKab/Android-Process-Manager/e5c4ae3626beb2080bd49dc09c23d06bfdabb0c1/app/src/main/res/drawable-mdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonKab/Android-Process-Manager/e5c4ae3626beb2080bd49dc09c23d06bfdabb0c1/app/src/main/res/drawable-xhdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonKab/Android-Process-Manager/e5c4ae3626beb2080bd49dc09c23d06bfdabb0c1/app/src/main/res/drawable-xxhdpi/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 17 | 18 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/blacklist_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 17 | 20 | 21 | 30 | 31 | 42 | 43 | 51 | 52 | 58 | 59 |