├── .gitignore ├── .idea └── encodings.xml ├── README.md ├── app ├── build.gradle ├── proguard-project.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── tomclaw │ │ └── appsend_rb │ │ ├── App.kt │ │ ├── SettingsActivity.java │ │ ├── SettingsFragment.java │ │ ├── core │ │ ├── MainExecutor.java │ │ ├── PleaseWaitTask.java │ │ ├── Task.java │ │ ├── TaskExecutor.java │ │ └── WeakObjectTask.java │ │ ├── di │ │ ├── AppComponent.kt │ │ └── AppModule.kt │ │ ├── dto │ │ └── AppEntity.kt │ │ ├── screen │ │ ├── about │ │ │ ├── AboutActivity.kt │ │ │ ├── AboutPresenter.kt │ │ │ ├── AboutResourceProvider.kt │ │ │ ├── AboutView.kt │ │ │ └── di │ │ │ │ ├── AboutComponent.kt │ │ │ │ └── AboutModule.kt │ │ ├── apps │ │ │ ├── AppEntityConverter.kt │ │ │ ├── AppsActivity.kt │ │ │ ├── AppsInteractor.kt │ │ │ ├── AppsPresenter.kt │ │ │ ├── AppsView.kt │ │ │ ├── OutputWrapper.kt │ │ │ ├── PackageManagerWrapper.kt │ │ │ ├── PreferencesProvider.kt │ │ │ ├── ResourceProvider.kt │ │ │ ├── adapter │ │ │ │ ├── ItemClickListener.kt │ │ │ │ └── app │ │ │ │ │ ├── AppItem.kt │ │ │ │ │ ├── AppItemBlueprint.kt │ │ │ │ │ ├── AppItemPresenter.kt │ │ │ │ │ └── AppItemView.kt │ │ │ └── di │ │ │ │ ├── AppsComponent.kt │ │ │ │ └── AppsModule.kt │ │ └── permissions │ │ │ ├── PermissionInfoProvider.kt │ │ │ ├── PermissionsActivity.kt │ │ │ ├── PermissionsConverter.kt │ │ │ ├── PermissionsPresenter.kt │ │ │ ├── PermissionsResourceProvider.kt │ │ │ ├── PermissionsView.kt │ │ │ ├── adapter │ │ │ ├── safe │ │ │ │ ├── SafePermissionItem.kt │ │ │ │ ├── SafePermissionItemBlueprint.kt │ │ │ │ ├── SafePermissionItemPresenter.kt │ │ │ │ └── SafePermissionItemView.kt │ │ │ └── unsafe │ │ │ │ ├── UnsafePermissionItem.kt │ │ │ │ ├── UnsafePermissionItemBlueprint.kt │ │ │ │ ├── UnsafePermissionItemPresenter.kt │ │ │ │ └── UnsafePermissionItemView.kt │ │ │ └── di │ │ │ ├── PermissionsComponent.kt │ │ │ └── PermissionsModule.kt │ │ └── util │ │ ├── Activities.kt │ │ ├── Analytics.kt │ │ ├── AppIconLoader.kt │ │ ├── Arrays.kt │ │ ├── Colors.kt │ │ ├── Files.kt │ │ ├── Intents.kt │ │ ├── Metrics.kt │ │ ├── PackageHelper.kt │ │ ├── Parcels.kt │ │ ├── PerActivity.kt │ │ ├── PreferenceHelper.java │ │ ├── Preferences.kt │ │ ├── SchedulersFactory.kt │ │ ├── Streams.kt │ │ ├── ThemeHelper.kt │ │ ├── Views.kt │ │ └── ZipParcelable.kt │ └── res │ ├── drawable-nodpi │ └── app_placeholder.png │ ├── drawable-xxhdpi │ └── ic_logo_ab.png │ ├── drawable-xxxhdpi │ └── ic_logo_ab.png │ ├── drawable │ ├── delete.xml │ ├── floppy.xml │ ├── google_play.xml │ ├── ic_launcher_foreground.xml │ ├── lock_open.xml │ ├── magnify.xml │ ├── refresh.xml │ ├── run.xml │ ├── settings_box.xml │ └── share.xml │ ├── layout │ ├── about_activity.xml │ ├── app_item.xml │ ├── apps_activity.xml │ ├── permission_safe.xml │ ├── permission_unsafe.xml │ ├── permissions_activity.xml │ ├── progress_view.xml │ ├── settings_activity.xml │ └── toolbar.xml │ ├── menu │ └── main_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.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 │ ├── arrays.xml │ ├── attr.xml │ ├── defaults.xml │ ├── dimen.xml │ ├── ic_launcher_background.xml │ ├── pref_keys.xml │ ├── strings.xml │ ├── styles.xml │ └── values.xml │ └── xml │ ├── appcenter_backup_rule.xml │ ├── preferences.xml │ └── provider_paths.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── graphics ├── App Shortcut icons.sketch ├── AppSend.sketch ├── Appteka Colored.sketch ├── Appteka.sketch └── web_hi_res_512.png ├── preference-fragment ├── LICENSE ├── README.md ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── machinarius │ │ └── preferencefragment │ │ ├── PreferenceFragment.java │ │ └── PreferenceManagerCompat.java │ └── res │ ├── layout │ └── preference_list_fragment.xml │ └── values │ ├── dimens.xml │ └── strings.xml ├── settings.gradle ├── statusbar-util ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── library │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── jaeger │ │ └── library │ │ └── StatusBarUtil.java │ └── res │ └── values │ ├── ids.xml │ └── strings.xml └── tomclaw.keystore /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.zip 3 | *.apk 4 | *.iml 5 | .DS_Store 6 | 7 | # IDE folders # 8 | gen/ 9 | bin/ 10 | out/ 11 | local.properties 12 | proguard_logs/ 13 | projectFilesBackup/ 14 | .idea/* 15 | !.idea/codeStyleSettings.xml 16 | !.idea/encodings.xml 17 | !.idea/inspectionProfiles/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | *build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AppSend will extracts APK that are installed on your Android device and copies them to your SD card. 2 | 3 | * Fast and easy to use. 4 | * Extracts almost all application, including system applications. 5 | * No ROOT access required. 6 | * Let you check applications permissions. 7 | * Provided Search option to search applications. 8 | * Support features of Android 13 9 | * Can backup and install Apk's from your SD Card. 10 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'kotlin-parcelize' 5 | 6 | android { 7 | signingConfigs { 8 | release { 9 | if (project.hasProperty("storeFile")) storeFile file("$rootDir/" + project.storeFile) 10 | if (project.hasProperty("storePassword")) storePassword project.storePassword 11 | if (project.hasProperty("keyAlias")) keyAlias project.keyAlias 12 | if (project.hasProperty("keyPassword")) keyPassword project.keyPassword 13 | } 14 | } 15 | compileSdk 34 16 | defaultConfig { 17 | applicationId "com.tomclaw.appsend_rb" 18 | minSdkVersion 21 19 | targetSdkVersion 34 20 | versionCode = project.hasProperty("versionCode") ? Integer.parseInt(project.versionCode) : 83 21 | versionName = "3.7" 22 | multiDexEnabled true 23 | manifestPlaceholders = [ 24 | APPCENTER_APP_ID: "b23ac4e6-fe78-43b4-9ec1-a61c7ed7d495" 25 | ] 26 | } 27 | 28 | buildTypes { 29 | debug { 30 | minifyEnabled false 31 | shrinkResources false 32 | } 33 | release { 34 | minifyEnabled true 35 | shrinkResources true 36 | 37 | signingConfig signingConfigs.release 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' 39 | } 40 | } 41 | 42 | lint { 43 | abortOnError false 44 | } 45 | 46 | compileOptions { 47 | sourceCompatibility JavaVersion.VERSION_17 48 | targetCompatibility JavaVersion.VERSION_17 49 | } 50 | 51 | namespace 'com.tomclaw.appsend_rb' 52 | } 53 | 54 | dependencies { 55 | implementation project(path: ':preference-fragment') 56 | implementation project(path: ':statusbar-util') 57 | implementation 'androidx.annotation:annotation:1.7.0' 58 | implementation 'androidx.recyclerview:recyclerview:1.3.1' 59 | implementation 'com.google.android.material:material:1.10.0' 60 | implementation 'com.github.FarshadTahmasbi:EzPermission:0.1.4' 61 | implementation 'com.microsoft.appcenter:appcenter-analytics:4.4.5' 62 | implementation 'com.microsoft.appcenter:appcenter-crashes:4.4.5' 63 | implementation 'com.anjlab.android.iab.v3:library:1.0.44' 64 | implementation 'com.github.shts:TriangleLabelView:1.1.2' 65 | implementation 'com.github.solkin:simple-image-loader:v0.9.6' 66 | implementation 'com.github.solkin:disk-lru-cache:1.5' 67 | implementation 'com.github.b-yng:BottomSheetBuilder:1.6.1' 68 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' 69 | implementation "androidx.core:core-ktx:1.12.0" 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' 72 | implementation 'io.reactivex.rxjava3:rxjava:3.0.6' 73 | implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' 74 | implementation 'com.jakewharton.rxrelay3:rxrelay:3.0.1' 75 | implementation 'com.github.avito-tech:Konveyor:0.42.2' 76 | implementation 'com.google.dagger:dagger:2.42' 77 | kapt 'com.google.dagger:dagger-compiler:2.42' 78 | kapt 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0' 79 | } 80 | repositories { 81 | mavenCentral() 82 | } 83 | -------------------------------------------------------------------------------- /app/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -dontwarn android.support.v7.** 22 | -keep class android.support.v7.** { *; } 23 | -keep interface android.support.v7.** { *; } 24 | 25 | -keep class android.support.design.widget.** { *; } 26 | -keep interface android.support.design.widget.** { *; } 27 | -dontwarn android.support.design.** 28 | -keep public class * extends android.support.design.widget.CoordinatorLayout$Behavior { 29 | public (android.content.Context, android.util.AttributeSet); 30 | } 31 | 32 | # Glide 33 | -keep public class * implements com.bumptech.glide.module.GlideModule 34 | -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { 35 | **[] $VALUES; 36 | public *; 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 13 | 16 | 17 | 26 | 31 | 34 | 35 | 36 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 55 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solkin/appsend-android/23b283bf86062537879addbb7cdc3acfbd9d14ac/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/App.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb 2 | 3 | import android.app.Application 4 | import com.tomclaw.appsend_rb.di.AppComponent 5 | import com.tomclaw.appsend_rb.di.AppModule 6 | import com.tomclaw.appsend_rb.di.DaggerAppComponent 7 | import com.tomclaw.appsend_rb.util.AppIconLoader 8 | import com.tomclaw.cache.DiskLruCache 9 | import com.tomclaw.imageloader.SimpleImageLoader.initImageLoader 10 | import com.tomclaw.imageloader.core.DiskCacheImpl 11 | import com.tomclaw.imageloader.core.FileProvider 12 | import com.tomclaw.imageloader.core.FileProviderImpl 13 | import com.tomclaw.imageloader.core.MainExecutorImpl 14 | import com.tomclaw.imageloader.core.MemoryCacheImpl 15 | import com.tomclaw.imageloader.util.BitmapDecoder 16 | import com.tomclaw.imageloader.util.loader.ContentLoader 17 | import com.tomclaw.imageloader.util.loader.FileLoader 18 | import com.tomclaw.imageloader.util.loader.UrlLoader 19 | import java.io.IOException 20 | import java.util.concurrent.Executors 21 | 22 | class App : Application() { 23 | 24 | lateinit var component: AppComponent 25 | private set 26 | 27 | override fun onCreate() { 28 | super.onCreate() 29 | component = buildComponent() 30 | initImageLoader() 31 | } 32 | 33 | private fun buildComponent(): AppComponent { 34 | return DaggerAppComponent.builder() 35 | .appModule(AppModule(this)) 36 | .build() 37 | } 38 | 39 | private fun initImageLoader() { 40 | try { 41 | val fileProvider: FileProvider = FileProviderImpl( 42 | cacheDir, 43 | DiskCacheImpl(DiskLruCache.create(cacheDir, 15728640L)), 44 | UrlLoader(), 45 | FileLoader(assets), 46 | ContentLoader(contentResolver), 47 | AppIconLoader(packageManager), 48 | ) 49 | this.initImageLoader( 50 | listOf(BitmapDecoder()), 51 | fileProvider, MemoryCacheImpl(), MainExecutorImpl(), 52 | Executors.newFixedThreadPool(5) 53 | ) 54 | } catch (e: IOException) { 55 | e.printStackTrace() 56 | } 57 | } 58 | 59 | } 60 | 61 | fun Application.getComponent(): AppComponent { 62 | return (this as App).component 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb; 2 | 3 | import static com.tomclaw.appsend_rb.util.ThemeHelperKt.updateStatusBar; 4 | import static com.tomclaw.appsend_rb.util.ThemeHelperKt.updateTheme; 5 | 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.SharedPreferences; 9 | import android.os.Bundle; 10 | import android.preference.PreferenceManager; 11 | import android.text.TextUtils; 12 | import android.view.MenuItem; 13 | 14 | import androidx.appcompat.app.ActionBar; 15 | import androidx.appcompat.app.AlertDialog; 16 | import androidx.appcompat.app.AppCompatActivity; 17 | import androidx.appcompat.widget.Toolbar; 18 | 19 | /** 20 | * Created with IntelliJ IDEA. 21 | * User: solkin 22 | * Date: 9/30/13 23 | * Time: 7:37 PM 24 | */ 25 | public class SettingsActivity extends AppCompatActivity { 26 | 27 | public static final int RESULT_UPDATE = 5; 28 | private SharedPreferences preferences; 29 | private OnSettingsChangedListener listener; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | updateTheme(this); 34 | super.onCreate(savedInstanceState); 35 | 36 | setContentView(R.layout.settings_activity); 37 | updateStatusBar(this); 38 | 39 | Toolbar toolbar = findViewById(R.id.toolbar); 40 | setSupportActionBar(toolbar); 41 | 42 | listener = new OnSettingsChangedListener(); 43 | preferences = PreferenceManager.getDefaultSharedPreferences(this); 44 | preferences.registerOnSharedPreferenceChangeListener(listener); 45 | SettingsFragment settingsFragment = new SettingsFragment(); 46 | 47 | ActionBar actionBar = getSupportActionBar(); 48 | actionBar.setDisplayHomeAsUpEnabled(true); 49 | 50 | // Display the fragment as the main content. 51 | getSupportFragmentManager().beginTransaction() 52 | .replace(R.id.content, settingsFragment) 53 | .commit(); 54 | } 55 | 56 | @Override 57 | protected void onDestroy() { 58 | super.onDestroy(); 59 | preferences.unregisterOnSharedPreferenceChangeListener(listener); 60 | } 61 | 62 | @Override 63 | public boolean onOptionsItemSelected(MenuItem item) { 64 | switch (item.getItemId()) { 65 | case android.R.id.home: { 66 | finish(); 67 | } 68 | } 69 | return true; 70 | } 71 | 72 | public class OnSettingsChangedListener implements SharedPreferences.OnSharedPreferenceChangeListener { 73 | 74 | @Override 75 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 76 | Context context = SettingsActivity.this; 77 | // Checking for preference changed. 78 | if (TextUtils.equals(key, getString(R.string.pref_show_system))) { 79 | if (sharedPreferences.getBoolean(context.getString(R.string.pref_show_system), 80 | context.getResources().getBoolean(R.bool.pref_show_system_default))) { 81 | final AlertDialog alertDialog = new AlertDialog.Builder(context) 82 | .setTitle(R.string.system_apps_warning_title) 83 | .setMessage(R.string.system_apps_warning_message) 84 | .setNeutralButton(R.string.got_it, null).create(); 85 | alertDialog.show(); 86 | } 87 | setResult(RESULT_UPDATE); 88 | } else if (TextUtils.equals(key, getString(R.string.pref_dark_theme))) { 89 | Intent intent = getIntent().addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 90 | finish(); 91 | overridePendingTransition(0, 0); 92 | startActivity(intent); 93 | } else if (TextUtils.equals(key, getString(R.string.pref_sort_order))) { 94 | setResult(RESULT_UPDATE); 95 | } else if (TextUtils.equals(key, getString(R.string.pref_runnable))) { 96 | setResult(RESULT_UPDATE); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb; 2 | 3 | import static com.tomclaw.appsend_rb.util.FilesKt.getExternalDirectory; 4 | 5 | import android.content.Context; 6 | import android.os.Bundle; 7 | import android.preference.Preference; 8 | import android.widget.Toast; 9 | 10 | import com.github.machinarius.preferencefragment.PreferenceFragment; 11 | import com.tomclaw.appsend_rb.core.PleaseWaitTask; 12 | import com.tomclaw.appsend_rb.core.TaskExecutor; 13 | 14 | import java.io.File; 15 | 16 | /** 17 | * Created by Solkin on 12.01.2015. 18 | */ 19 | public class SettingsFragment extends PreferenceFragment { 20 | @Override 21 | public void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | addPreferencesFromResource(R.xml.preferences); 24 | Preference myPref = findPreference(getString(R.string.pref_clear_cache)); 25 | myPref.setOnPreferenceClickListener(preference -> { 26 | TaskExecutor.getInstance().execute(new PleaseWaitTask(getActivity()) { 27 | @Override 28 | public void executeBackground() { 29 | File directory = getExternalDirectory(); 30 | File[] files = directory.listFiles(pathname -> pathname.getName().endsWith(".apk")); 31 | if (files != null) { 32 | for (File file : files) { 33 | file.delete(); 34 | } 35 | } 36 | } 37 | 38 | @Override 39 | public void onSuccessMain() { 40 | Context context = getWeakObject(); 41 | if (context != null) { 42 | Toast.makeText(context, R.string.cache_cleared_successfully, Toast.LENGTH_SHORT).show(); 43 | } 44 | } 45 | 46 | @Override 47 | public void onFailMain(Throwable ex) { 48 | Context context = getWeakObject(); 49 | if (context != null) { 50 | Toast.makeText(context, R.string.cache_clearing_failed, Toast.LENGTH_SHORT).show(); 51 | } 52 | } 53 | }); 54 | return true; 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/core/MainExecutor.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.core; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | /** 7 | * Created with IntelliJ IDEA. 8 | * User: Solkin 9 | * Date: 09.11.13 10 | * Time: 13:33 11 | */ 12 | @SuppressWarnings("WeakerAccess") 13 | public class MainExecutor { 14 | 15 | private static Handler mainHandler = new Handler(Looper.getMainLooper()); 16 | 17 | public static boolean isMainThread() { 18 | return mainHandler.getLooper().getThread() == Thread.currentThread(); 19 | } 20 | 21 | /** 22 | * Performs a task on the main thread. If the current thread is main, execution immediately. 23 | * 24 | * @param runnable to execute 25 | */ 26 | public static void execute(Runnable runnable) { 27 | if (isMainThread()) { 28 | runnable.run(); 29 | } else { 30 | mainHandler.post(runnable); 31 | } 32 | } 33 | 34 | /** 35 | * Executes runnable on the main thread after specified delay. 36 | * 37 | * @param runnable to execute 38 | * @param delay delay in milliseconds until the code will be executed 39 | */ 40 | public static void executeLater(Runnable runnable, long delay) { 41 | mainHandler.postDelayed(runnable, delay); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/core/PleaseWaitTask.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.core; 2 | 3 | import android.app.ProgressDialog; 4 | import android.content.Context; 5 | 6 | import com.tomclaw.appsend_rb.R; 7 | 8 | import java.lang.ref.WeakReference; 9 | 10 | /** 11 | * Created with IntelliJ IDEA. 12 | * User: Solkin 13 | * Date: 09.11.13 14 | * Time: 14:19 15 | */ 16 | @SuppressWarnings("WeakerAccess") 17 | public abstract class PleaseWaitTask extends WeakObjectTask { 18 | 19 | private WeakReference weakProgressDialog; 20 | 21 | public PleaseWaitTask(Context context) { 22 | super(context); 23 | } 24 | 25 | @Override 26 | public boolean isPreExecuteRequired() { 27 | return true; 28 | } 29 | 30 | @Override 31 | public void onPreExecuteMain() { 32 | Context context = getWeakObject(); 33 | if (context != null) { 34 | try { 35 | ProgressDialog progressDialog = ProgressDialog.show(context, null, context.getString(getWaitStringId())); 36 | weakProgressDialog = new WeakReference<>(progressDialog); 37 | } catch (Throwable ignored) { 38 | } 39 | } 40 | } 41 | 42 | @Override 43 | public void onPostExecuteMain() { 44 | ProgressDialog progressDialog = weakProgressDialog.get(); 45 | if (progressDialog != null) { 46 | try { 47 | progressDialog.dismiss(); 48 | } catch (Throwable ignored) { 49 | } 50 | } 51 | } 52 | 53 | public int getWaitStringId() { 54 | return R.string.please_wait; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/core/Task.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.core; 2 | 3 | /** 4 | * Created with IntelliJ IDEA. 5 | * User: Solkin 6 | * Date: 31.10.13 7 | * Time: 11:08 8 | */ 9 | @SuppressWarnings("WeakerAccess") 10 | public abstract class Task implements Runnable { 11 | 12 | @Override 13 | public void run() { 14 | try { 15 | executeBackground(); 16 | onSuccessBackground(); 17 | MainExecutor.execute(() -> { 18 | onPostExecuteMain(); 19 | onSuccessMain(); 20 | }); 21 | } catch (final Throwable ex) { 22 | onFailBackground(); 23 | MainExecutor.execute(() -> { 24 | onPostExecuteMain(); 25 | onFailMain(ex); 26 | }); 27 | } 28 | } 29 | 30 | public boolean isPreExecuteRequired() { 31 | return false; 32 | } 33 | 34 | public void onPreExecuteMain() { 35 | } 36 | 37 | public abstract void executeBackground() throws Throwable; 38 | 39 | public void onPostExecuteMain() { 40 | } 41 | 42 | public void onSuccessBackground() { 43 | } 44 | 45 | public void onFailBackground() { 46 | } 47 | 48 | public void onSuccessMain() { 49 | } 50 | 51 | public void onFailMain(Throwable ex) { 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/core/TaskExecutor.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.core; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | 6 | /** 7 | * Created with IntelliJ IDEA. 8 | * User: Solkin 9 | * Date: 31.10.13 10 | * Time: 10:56 11 | */ 12 | public class TaskExecutor { 13 | 14 | private final ExecutorService threadExecutor = Executors.newSingleThreadExecutor(); 15 | 16 | private static class Holder { 17 | 18 | static TaskExecutor instance = new TaskExecutor(); 19 | } 20 | 21 | public static TaskExecutor getInstance() { 22 | return Holder.instance; 23 | } 24 | 25 | public void execute(final Task task) { 26 | if (task.isPreExecuteRequired()) { 27 | MainExecutor.execute(() -> { 28 | task.onPreExecuteMain(); 29 | threadExecutor.submit(task); 30 | }); 31 | } else { 32 | threadExecutor.submit(task); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/core/WeakObjectTask.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.core; 2 | 3 | import java.lang.ref.WeakReference; 4 | 5 | /** 6 | * Created by solkin on 16.05.14. 7 | */ 8 | @SuppressWarnings("WeakerAccess") 9 | public abstract class WeakObjectTask extends Task { 10 | 11 | private final WeakReference weakObject; 12 | 13 | public WeakObjectTask(W object) { 14 | this.weakObject = new WeakReference(object); 15 | } 16 | 17 | public W getWeakObject() { 18 | return weakObject.get(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.di 2 | 3 | import com.tomclaw.appsend_rb.screen.about.di.AboutComponent 4 | import com.tomclaw.appsend_rb.screen.about.di.AboutModule 5 | import com.tomclaw.appsend_rb.screen.apps.di.AppsComponent 6 | import com.tomclaw.appsend_rb.screen.apps.di.AppsModule 7 | import com.tomclaw.appsend_rb.screen.permissions.di.PermissionsComponent 8 | import com.tomclaw.appsend_rb.screen.permissions.di.PermissionsModule 9 | import dagger.Component 10 | import javax.inject.Singleton 11 | 12 | @Singleton 13 | @Component(modules = [AppModule::class]) 14 | interface AppComponent { 15 | 16 | fun appsComponent(module: AppsModule): AppsComponent 17 | 18 | fun permissionsComponent(module: PermissionsModule): PermissionsComponent 19 | 20 | fun aboutComponent(module: AboutModule): AboutComponent 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import com.tomclaw.appsend.util.Analytics 7 | import com.tomclaw.appsend.util.AnalyticsImpl 8 | import com.tomclaw.appsend_rb.util.SchedulersFactory 9 | import com.tomclaw.appsend_rb.util.SchedulersFactoryImpl 10 | import dagger.Module 11 | import dagger.Provides 12 | import java.util.Locale 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | class AppModule(private val app: Application) { 17 | 18 | @Provides 19 | @Singleton 20 | internal fun provideContext(): Context = app 21 | 22 | @Provides 23 | @Singleton 24 | internal fun provideSchedulersFactory(): SchedulersFactory = SchedulersFactoryImpl() 25 | 26 | @Provides 27 | @Singleton 28 | internal fun provideLocale(): Locale = Locale.getDefault() 29 | 30 | @Provides 31 | @Singleton 32 | internal fun provideAnalytics(): Analytics = AnalyticsImpl(app) 33 | 34 | @Provides 35 | @Singleton 36 | internal fun provideManager( 37 | context: Context 38 | ): PackageManager = context.packageManager 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/dto/AppEntity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.dto 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class AppEntity( 8 | val label: String, 9 | val packageName: String, 10 | val versionName: String, 11 | val versionCode: Long, 12 | val requestedPermissions: List?, 13 | val path: String, 14 | val size: Long, 15 | val firstInstallTime: Long, 16 | val lastUpdateTime: Long 17 | ) : Parcelable 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/about/AboutActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.about 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.Intent.ACTION_VIEW 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.tomclaw.appsend_rb.R 10 | import com.tomclaw.appsend_rb.getComponent 11 | import com.tomclaw.appsend_rb.screen.about.di.AboutModule 12 | import com.tomclaw.appsend_rb.screen.apps.PreferencesProvider 13 | import com.tomclaw.appsend_rb.util.updateStatusBar 14 | import com.tomclaw.appsend_rb.util.updateTheme 15 | import javax.inject.Inject 16 | 17 | class AboutActivity : AppCompatActivity(), AboutPresenter.AboutRouter { 18 | 19 | @Inject 20 | lateinit var presenter: AboutPresenter 21 | 22 | @Inject 23 | lateinit var preferences: PreferencesProvider 24 | 25 | private var isDarkTheme: Boolean = false 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | application.getComponent() 29 | .aboutComponent(AboutModule(this)) 30 | .inject(activity = this) 31 | 32 | isDarkTheme = updateTheme(preferences) 33 | updateStatusBar() 34 | super.onCreate(savedInstanceState) 35 | setContentView(R.layout.about_activity) 36 | 37 | val view = AboutViewImpl(window.decorView) 38 | 39 | presenter.attachView(view) 40 | } 41 | 42 | override fun onBackPressed() { 43 | super.onBackPressed() 44 | presenter.onBackPressed() 45 | } 46 | 47 | override fun onStart() { 48 | super.onStart() 49 | presenter.attachRouter(this) 50 | } 51 | 52 | override fun onStop() { 53 | presenter.detachRouter() 54 | super.onStop() 55 | } 56 | 57 | override fun onDestroy() { 58 | presenter.detachView() 59 | super.onDestroy() 60 | } 61 | 62 | override fun openRate() { 63 | openUriSafe( 64 | uri = MARKET_URI_RATE + packageName, 65 | fallback = WEB_URI_RATE + packageName 66 | ) 67 | } 68 | 69 | override fun openProjects() { 70 | openUriSafe( 71 | uri = MARKET_URI_PROJECTS, 72 | fallback = WEB_URI_PROJECTS 73 | ) 74 | } 75 | 76 | override fun leaveScreen() { 77 | finish() 78 | } 79 | 80 | private fun openUriSafe(uri: String, fallback: String) { 81 | try { 82 | startActivity(Intent(ACTION_VIEW, Uri.parse(uri))) 83 | } catch (ignored: android.content.ActivityNotFoundException) { 84 | startActivity(Intent(ACTION_VIEW, Uri.parse(fallback))) 85 | } 86 | } 87 | 88 | } 89 | 90 | fun createAboutActivityIntent( 91 | context: Context 92 | ): Intent = Intent(context, AboutActivity::class.java) 93 | 94 | private const val MARKET_URI_RATE = "market://details?id=" 95 | private const val MARKET_URI_PROJECTS = "market://search?q=pub:TomClaw+Software" 96 | private const val WEB_URI_RATE = "http://play.google.com/store/apps/details?id=" 97 | private const val WEB_URI_PROJECTS = "http://play.google.com/store/apps/developer?id=TomClaw+Software" -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/about/AboutPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.about 2 | 3 | import com.tomclaw.appsend_rb.util.SchedulersFactory 4 | import io.reactivex.rxjava3.disposables.CompositeDisposable 5 | import io.reactivex.rxjava3.kotlin.plusAssign 6 | 7 | interface AboutPresenter { 8 | 9 | fun attachView(view: AboutView) 10 | 11 | fun detachView() 12 | 13 | fun attachRouter(router: AboutRouter) 14 | 15 | fun detachRouter() 16 | 17 | fun onBackPressed() 18 | 19 | interface AboutRouter { 20 | 21 | fun openRate() 22 | 23 | fun openProjects() 24 | 25 | fun leaveScreen() 26 | 27 | } 28 | 29 | } 30 | 31 | class AboutPresenterImpl( 32 | private val resourceProvider: AboutResourceProvider, 33 | private val schedulers: SchedulersFactory 34 | ) : AboutPresenter { 35 | 36 | private var view: AboutView? = null 37 | private var router: AboutPresenter.AboutRouter? = null 38 | 39 | private val subscriptions = CompositeDisposable() 40 | 41 | override fun attachView(view: AboutView) { 42 | this.view = view 43 | 44 | subscriptions += view.navigationClicks().subscribe { router?.leaveScreen() } 45 | subscriptions += view.rateClicks().subscribe { router?.openRate() } 46 | subscriptions += view.projectsClicks().subscribe { router?.openProjects() } 47 | 48 | bindVersion() 49 | } 50 | 51 | override fun detachView() { 52 | subscriptions.clear() 53 | this.view = null 54 | } 55 | 56 | override fun attachRouter(router: AboutPresenter.AboutRouter) { 57 | this.router = router 58 | } 59 | 60 | override fun detachRouter() { 61 | this.router = null 62 | } 63 | 64 | override fun onBackPressed() { 65 | router?.leaveScreen() 66 | } 67 | 68 | private fun bindVersion() { 69 | view?.setVersion(resourceProvider.provideVersion()) 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/about/AboutResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.about 2 | 3 | import android.content.pm.PackageManager 4 | import android.content.res.Resources 5 | import com.tomclaw.appsend_rb.R 6 | 7 | interface AboutResourceProvider { 8 | 9 | fun provideVersion(): String 10 | 11 | } 12 | 13 | class AboutResourceProviderImpl( 14 | private val packageName: String, 15 | private val packageManager: PackageManager, 16 | private val resources: Resources 17 | ) : AboutResourceProvider { 18 | 19 | override fun provideVersion(): String { 20 | try { 21 | val info = packageManager.getPackageInfo(packageName, 0) 22 | return resources.getString(R.string.app_version, info.versionName, info.versionCode) 23 | } catch (ignored: PackageManager.NameNotFoundException) { 24 | } 25 | return "" 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/about/AboutView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.about 2 | 3 | import android.view.View 4 | import android.widget.TextView 5 | import androidx.appcompat.widget.Toolbar 6 | import com.jakewharton.rxrelay3.PublishRelay 7 | import com.tomclaw.appsend_rb.R 8 | import io.reactivex.rxjava3.core.Observable 9 | 10 | interface AboutView { 11 | 12 | fun navigationClicks(): Observable 13 | 14 | fun rateClicks(): Observable 15 | 16 | fun projectsClicks(): Observable 17 | 18 | fun setVersion(version: String) 19 | 20 | } 21 | 22 | class AboutViewImpl( 23 | view: View 24 | ) : AboutView { 25 | 26 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 27 | private val rateButton: View = view.findViewById(R.id.rate_button) 28 | private val projectsButton: View = view.findViewById(R.id.projects_button) 29 | private val versionText: TextView = view.findViewById(R.id.app_version) 30 | 31 | private val navigationRelay = PublishRelay.create() 32 | private val rateRelay = PublishRelay.create() 33 | private val projectsRelay = PublishRelay.create() 34 | 35 | init { 36 | toolbar.setTitle(R.string.info) 37 | toolbar.setNavigationOnClickListener { navigationRelay.accept(Unit) } 38 | rateButton.setOnClickListener { rateRelay.accept(Unit) } 39 | projectsButton.setOnClickListener { projectsRelay.accept(Unit) } 40 | } 41 | 42 | override fun navigationClicks(): Observable = navigationRelay 43 | 44 | override fun rateClicks(): Observable = rateRelay 45 | 46 | override fun projectsClicks(): Observable = projectsRelay 47 | 48 | override fun setVersion(version: String) { 49 | versionText.text = version 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/about/di/AboutComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.about.di 2 | 3 | import com.tomclaw.appsend_rb.screen.about.AboutActivity 4 | import com.tomclaw.appsend_rb.util.PerActivity 5 | import dagger.Subcomponent 6 | 7 | @PerActivity 8 | @Subcomponent(modules = [AboutModule::class]) 9 | interface AboutComponent { 10 | 11 | fun inject(activity: AboutActivity) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/about/di/AboutModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.about.di 2 | 3 | import android.content.Context 4 | import com.tomclaw.appsend_rb.screen.about.AboutPresenter 5 | import com.tomclaw.appsend_rb.screen.about.AboutPresenterImpl 6 | import com.tomclaw.appsend_rb.screen.about.AboutResourceProvider 7 | import com.tomclaw.appsend_rb.screen.about.AboutResourceProviderImpl 8 | import com.tomclaw.appsend_rb.screen.apps.PreferencesProvider 9 | import com.tomclaw.appsend_rb.screen.apps.PreferencesProviderImpl 10 | import com.tomclaw.appsend_rb.util.PerActivity 11 | import com.tomclaw.appsend_rb.util.SchedulersFactory 12 | import dagger.Module 13 | import dagger.Provides 14 | 15 | @Module 16 | class AboutModule( 17 | private val context: Context 18 | ) { 19 | 20 | @Provides 21 | @PerActivity 22 | internal fun providePresenter( 23 | resourceProvider: AboutResourceProvider, 24 | schedulers: SchedulersFactory 25 | ): AboutPresenter = AboutPresenterImpl(resourceProvider, schedulers) 26 | 27 | @Provides 28 | @PerActivity 29 | internal fun providePreferencesProvider(): PreferencesProvider { 30 | return PreferencesProviderImpl(context) 31 | } 32 | 33 | @Provides 34 | @PerActivity 35 | internal fun provideResourceProvider(): AboutResourceProvider { 36 | return AboutResourceProviderImpl( 37 | context.packageName, 38 | context.packageManager, 39 | context.resources 40 | ) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/AppEntityConverter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import com.avito.konveyor.blueprint.Item 4 | import com.tomclaw.appsend_rb.dto.AppEntity 5 | import com.tomclaw.appsend_rb.screen.apps.adapter.app.AppItem 6 | import com.tomclaw.appsend_rb.util.createAppIconURI 7 | import java.util.concurrent.TimeUnit 8 | 9 | interface AppEntityConverter { 10 | 11 | fun convert(id: Long, entity: AppEntity): Item 12 | 13 | } 14 | 15 | class AppEntityConverterImpl(private val resourceProvider: ResourceProvider) : AppEntityConverter { 16 | 17 | override fun convert(id: Long, entity: AppEntity): Item = AppItem( 18 | id = id, 19 | icon = createAppIconURI(entity.packageName), 20 | packageName = entity.packageName, 21 | name = entity.label, 22 | size = resourceProvider.formatBytes(entity.size), 23 | firstInstallTime = resourceProvider.formatTime(entity.firstInstallTime), 24 | lastUpdateTime = resourceProvider.formatTime(entity.lastUpdateTime), 25 | versionName = entity.versionName, 26 | versionCode = entity.versionCode, 27 | newApp = entity.lastUpdateTime > System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1) 28 | ) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/AppsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.Manifest 4 | import android.content.ActivityNotFoundException 5 | import android.content.Intent 6 | import android.content.Intent.ACTION_SEND 7 | import android.content.Intent.ACTION_VIEW 8 | import android.content.Intent.CATEGORY_DEFAULT 9 | import android.content.Intent.EXTRA_STREAM 10 | import android.content.Intent.FLAG_ACTIVITY_NEW_TASK 11 | import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION 12 | import android.content.Intent.createChooser 13 | import android.net.Uri 14 | import android.os.Build 15 | import android.os.Bundle 16 | import android.provider.Settings 17 | import androidx.appcompat.app.AppCompatActivity 18 | import com.androidisland.ezpermission.EzPermission 19 | import com.avito.konveyor.ItemBinder 20 | import com.avito.konveyor.adapter.AdapterPresenter 21 | import com.avito.konveyor.adapter.SimpleRecyclerAdapter 22 | import com.tomclaw.appsend_rb.R 23 | import com.tomclaw.appsend_rb.SettingsActivity 24 | import com.tomclaw.appsend_rb.getComponent 25 | import com.tomclaw.appsend_rb.screen.about.createAboutActivityIntent 26 | import com.tomclaw.appsend_rb.screen.apps.di.AppsModule 27 | import com.tomclaw.appsend_rb.screen.permissions.createPermissionsActivityIntent 28 | import com.tomclaw.appsend_rb.util.ZipParcelable 29 | import com.tomclaw.appsend_rb.util.grantProviderUriPermission 30 | import com.tomclaw.appsend_rb.util.registerAppCenter 31 | import com.tomclaw.appsend_rb.util.updateStatusBar 32 | import com.tomclaw.appsend_rb.util.updateTheme 33 | import javax.inject.Inject 34 | 35 | 36 | class AppsActivity : AppCompatActivity(), AppsPresenter.AppsRouter { 37 | 38 | @Inject 39 | lateinit var presenter: AppsPresenter 40 | 41 | @Inject 42 | lateinit var adapterPresenter: AdapterPresenter 43 | 44 | @Inject 45 | lateinit var preferences: PreferencesProvider 46 | 47 | @Inject 48 | lateinit var binder: ItemBinder 49 | 50 | private var isDarkTheme: Boolean = false 51 | 52 | override fun onCreate(savedInstanceState: Bundle?) { 53 | val compressedPresenterState: ZipParcelable? = savedInstanceState?.getParcelable(KEY_PRESENTER_STATE) 54 | val presenterState: Bundle? = compressedPresenterState?.restore() 55 | application.getComponent() 56 | .appsComponent(AppsModule(this, presenterState)) 57 | .inject(activity = this) 58 | 59 | isDarkTheme = updateTheme(preferences) 60 | updateStatusBar() 61 | super.onCreate(savedInstanceState) 62 | setContentView(R.layout.apps_activity) 63 | 64 | val adapter = SimpleRecyclerAdapter(adapterPresenter, binder) 65 | val view = AppsViewImpl(window.decorView, adapter, preferences) 66 | 67 | registerAppCenter(application) 68 | 69 | presenter.attachView(view) 70 | } 71 | 72 | override fun onBackPressed() { 73 | super.onBackPressed() 74 | presenter.onBackPressed() 75 | } 76 | 77 | override fun onStart() { 78 | super.onStart() 79 | presenter.attachRouter(this) 80 | } 81 | 82 | override fun onStop() { 83 | presenter.detachRouter() 84 | super.onStop() 85 | } 86 | 87 | override fun onResume() { 88 | super.onResume() 89 | if (isDarkTheme != preferences.isDarkTheme()) { 90 | val intent = intent.addFlags(FLAG_ACTIVITY_NO_ANIMATION) 91 | finish() 92 | startActivity(intent) 93 | } else { 94 | presenter.onResume() 95 | } 96 | } 97 | 98 | override fun onDestroy() { 99 | presenter.detachView() 100 | super.onDestroy() 101 | } 102 | 103 | override fun onSaveInstanceState(outState: Bundle) { 104 | super.onSaveInstanceState(outState) 105 | outState.putParcelable(KEY_PRESENTER_STATE, ZipParcelable(presenter.saveState())) 106 | } 107 | 108 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 109 | super.onActivityResult(requestCode, resultCode, data) 110 | if (requestCode == REQUEST_UPDATE_SETTINGS) { 111 | if (resultCode == SettingsActivity.RESULT_UPDATE) { 112 | presenter.invalidateAppsList() 113 | } 114 | } 115 | } 116 | 117 | override fun showPrefsScreen() { 118 | val intent = Intent( 119 | this, 120 | SettingsActivity::class.java 121 | ) 122 | startActivityForResult(intent, REQUEST_UPDATE_SETTINGS) 123 | } 124 | 125 | override fun showInfoScreen() { 126 | val intent = createAboutActivityIntent(this) 127 | startActivity(intent) 128 | } 129 | 130 | override fun leaveScreen() { 131 | finish() 132 | } 133 | 134 | override fun runApp(packageName: String): Boolean { 135 | val launchIntent = packageManager.getLaunchIntentForPackage(packageName) 136 | launchIntent?.run { startActivity(launchIntent) } 137 | return launchIntent != null 138 | } 139 | 140 | override fun openGooglePlay(packageName: String) { 141 | try { 142 | startActivity(Intent(ACTION_VIEW, Uri.parse("market://details?id=$packageName"))) 143 | } catch (ex: ActivityNotFoundException) { 144 | startActivity( 145 | Intent( 146 | ACTION_VIEW, 147 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 148 | ) 149 | ) 150 | } 151 | } 152 | 153 | override fun showAppDetails(packageName: String) { 154 | val intent = Intent() 155 | .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 156 | .addCategory(CATEGORY_DEFAULT) 157 | .setData(Uri.parse("package:$packageName")) 158 | .addFlags(FLAG_ACTIVITY_NEW_TASK) 159 | startActivity(intent) 160 | } 161 | 162 | override fun showRequestedPermissions(permissions: List) { 163 | val intent = createPermissionsActivityIntent(context = this, permissions) 164 | startActivity(intent) 165 | } 166 | 167 | override fun runAppUninstall(packageName: String) { 168 | val packageUri = Uri.parse("package:$packageName") 169 | val uninstallIntent = Intent(Intent.ACTION_DELETE, packageUri) 170 | startActivity(uninstallIntent) 171 | } 172 | 173 | override fun shareApk(uri: Uri) { 174 | val intent = Intent().apply { 175 | action = ACTION_SEND 176 | putExtra(EXTRA_STREAM, uri) 177 | type = "application/zip" 178 | } 179 | grantProviderUriPermission(this, uri, intent) 180 | startActivity(createChooser(intent, resources.getText(R.string.send_to))) 181 | } 182 | 183 | override fun requestPermissions(onGranted: () -> Unit, onDenied: () -> Unit) { 184 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 185 | onGranted() 186 | } else { 187 | EzPermission.with(this) 188 | .permissions( 189 | Manifest.permission.WRITE_EXTERNAL_STORAGE 190 | ) 191 | .request { granted, denied, permanentlyDenied -> 192 | if (granted.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { 193 | onGranted() 194 | } else { 195 | onDenied() 196 | } 197 | } 198 | } 199 | } 200 | 201 | } 202 | 203 | private const val KEY_PRESENTER_STATE = "presenter_state" 204 | private const val REQUEST_UPDATE_SETTINGS = 6 205 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/AppsInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.content.pm.PackageInfo 4 | import android.net.Uri 5 | import android.os.Build 6 | import com.tomclaw.appsend_rb.dto.AppEntity 7 | import com.tomclaw.appsend_rb.util.SchedulersFactory 8 | import com.tomclaw.appsend_rb.util.getApkName 9 | import com.tomclaw.appsend_rb.util.safeClose 10 | import io.reactivex.rxjava3.core.Observable 11 | import io.reactivex.rxjava3.core.Single 12 | import java.io.File 13 | import java.io.FileInputStream 14 | import java.io.IOException 15 | import java.io.InputStream 16 | import java.io.OutputStream 17 | import java.util.Locale 18 | 19 | interface AppsInteractor { 20 | 21 | fun loadApps( 22 | systemApps: Boolean, 23 | runnableOnly: Boolean, 24 | sortOrder: Int 25 | ): Observable> 26 | 27 | fun loadApp(packageName: String): Observable 28 | 29 | fun exportApp(entity: AppEntity): Observable 30 | 31 | } 32 | 33 | class AppsInteractorImpl( 34 | private val packageManager: PackageManagerWrapper, 35 | private val outputWrapper: OutputWrapper, 36 | private val locale: Locale, 37 | private val schedulers: SchedulersFactory 38 | ) : AppsInteractor { 39 | 40 | override fun loadApps( 41 | systemApps: Boolean, 42 | runnableOnly: Boolean, 43 | sortOrder: Int 44 | ): Observable> { 45 | return Single 46 | .create> { emitter -> 47 | val entities = loadEntities(systemApps, runnableOnly, sortOrder) 48 | emitter.onSuccess(entities) 49 | } 50 | .toObservable() 51 | .subscribeOn(schedulers.io()) 52 | } 53 | 54 | override fun loadApp(packageName: String): Observable { 55 | return Single 56 | .create { emitter -> 57 | try { 58 | val packageInfo = packageManager.getPackageInfo(packageName, GET_PERMISSIONS) 59 | createAppEntity(packageInfo)?.let { emitter.onSuccess(it) } 60 | ?: emitter.onError(IOException("unable to create app entity")) 61 | } catch (ex: Throwable) { 62 | emitter.onError(ex) 63 | } 64 | } 65 | .toObservable() 66 | .subscribeOn(schedulers.io()) 67 | } 68 | 69 | private fun loadEntities( 70 | systemApps: Boolean, 71 | runnableOnly: Boolean, 72 | sortOrder: Int 73 | ): List { 74 | val entities = ArrayList() 75 | val packages = packageManager.getInstalledApplications(GET_META_DATA) 76 | for (info in packages) { 77 | try { 78 | val packageInfo = packageManager.getPackageInfo(info.packageName, GET_PERMISSIONS) 79 | createAppEntity(packageInfo)?.let { entity -> 80 | val isUserApp = info.flags and FLAG_SYSTEM != FLAG_SYSTEM && 81 | info.flags and FLAG_UPDATED_SYSTEM_APP != FLAG_UPDATED_SYSTEM_APP 82 | if (isUserApp || systemApps) { 83 | val launchIntent = 84 | packageManager.getLaunchIntentForPackage(info.packageName) 85 | if (launchIntent != null || !runnableOnly) { 86 | entities += entity 87 | } 88 | } 89 | } 90 | } catch (ignored: Throwable) { 91 | // Bad package. 92 | } 93 | } 94 | when (sortOrder) { 95 | NAME_ASCENDING -> entities.sortWith { lhs: AppEntity, rhs: AppEntity -> 96 | lhs.label.uppercase( 97 | locale 98 | ).compareTo(rhs.label.uppercase(locale)) 99 | } 100 | 101 | NAME_DESCENDING -> entities.sortWith { lhs: AppEntity, rhs: AppEntity -> 102 | rhs.label.uppercase( 103 | locale 104 | ).compareTo(lhs.label.uppercase(locale)) 105 | } 106 | 107 | APP_SIZE -> entities.sortWith { lhs: AppEntity, rhs: AppEntity -> rhs.size.compareTo(lhs.size) } 108 | INSTALL_TIME -> entities.sortWith { lhs: AppEntity, rhs: AppEntity -> 109 | rhs.firstInstallTime.compareTo( 110 | lhs.firstInstallTime 111 | ) 112 | } 113 | 114 | UPDATE_TIME -> entities.sortWith { lhs: AppEntity, rhs: AppEntity -> 115 | rhs.lastUpdateTime.compareTo( 116 | lhs.lastUpdateTime 117 | ) 118 | } 119 | } 120 | 121 | return entities 122 | } 123 | 124 | private fun createAppEntity(packageInfo: PackageInfo): AppEntity? { 125 | val file = File(packageInfo.applicationInfo.publicSourceDir) 126 | if (file.exists()) { 127 | val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 128 | packageInfo.longVersionCode 129 | } else { 130 | @Suppress("DEPRECATION") 131 | packageInfo.versionCode.toLong() 132 | } 133 | return AppEntity( 134 | label = packageManager.getApplicationLabel(packageInfo.applicationInfo), 135 | packageName = packageInfo.packageName, 136 | versionName = packageInfo.versionName, 137 | versionCode = versionCode, 138 | requestedPermissions = packageInfo.requestedPermissions.asList(), 139 | path = file.path, 140 | size = file.length(), 141 | firstInstallTime = packageInfo.firstInstallTime, 142 | lastUpdateTime = packageInfo.lastUpdateTime 143 | ) 144 | } 145 | return null 146 | } 147 | 148 | override fun exportApp(entity: AppEntity): Observable { 149 | return Single 150 | .create { emitter -> 151 | val buffer = ByteArray(524288) 152 | var inputStream: InputStream? = null 153 | var outputStream: OutputStream? = null 154 | val uri = outputWrapper.getOutputUri( 155 | getApkName(entity), 156 | "application/vnd.android.package-archive" 157 | ) 158 | val file = File(entity.path) 159 | try { 160 | inputStream = FileInputStream(file) 161 | outputStream = outputWrapper.openStream(uri) 162 | var read: Int 163 | while (inputStream.read(buffer).also { read = it } != -1) { 164 | outputStream.write(buffer, 0, read) 165 | } 166 | outputStream.flush() 167 | } catch (ex: Throwable) { 168 | emitter.onError(ex) 169 | return@create 170 | } finally { 171 | outputStream.safeClose() 172 | inputStream.safeClose() 173 | } 174 | emitter.onSuccess(uri) 175 | } 176 | .toObservable() 177 | .subscribeOn(schedulers.io()) 178 | } 179 | 180 | } 181 | 182 | const val NAME_ASCENDING = 1 183 | const val NAME_DESCENDING = 2 184 | const val APP_SIZE = 3 185 | const val INSTALL_TIME = 4 186 | const val UPDATE_TIME = 5 187 | 188 | const val APPS_DIR_NAME = "Apps" 189 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/AppsPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.net.Uri 4 | import android.os.Build 5 | import android.os.Bundle 6 | import com.avito.konveyor.adapter.AdapterPresenter 7 | import com.avito.konveyor.blueprint.Item 8 | import com.avito.konveyor.data_source.ListDataSource 9 | import com.tomclaw.appsend_rb.dto.AppEntity 10 | import com.tomclaw.appsend_rb.screen.apps.adapter.ItemClickListener 11 | import com.tomclaw.appsend_rb.screen.apps.adapter.app.AppItem 12 | import com.tomclaw.appsend_rb.util.SchedulersFactory 13 | import dagger.Lazy 14 | import io.reactivex.rxjava3.disposables.CompositeDisposable 15 | import io.reactivex.rxjava3.kotlin.plusAssign 16 | 17 | interface AppsPresenter : ItemClickListener { 18 | 19 | fun attachView(view: AppsView) 20 | 21 | fun detachView() 22 | 23 | fun attachRouter(router: AppsRouter) 24 | 25 | fun detachRouter() 26 | 27 | fun saveState(): Bundle 28 | 29 | fun onBackPressed() 30 | 31 | fun onResume() 32 | 33 | fun invalidateAppsList() 34 | 35 | interface AppsRouter { 36 | 37 | fun showPrefsScreen() 38 | 39 | fun showInfoScreen() 40 | 41 | fun leaveScreen() 42 | 43 | fun runApp(packageName: String): Boolean 44 | 45 | fun openGooglePlay(packageName: String) 46 | 47 | fun showRequestedPermissions(permissions: List) 48 | 49 | fun showAppDetails(packageName: String) 50 | 51 | fun runAppUninstall(packageName: String) 52 | 53 | fun shareApk(uri: Uri) 54 | 55 | fun requestPermissions(onGranted: () -> Unit, onDenied: () -> Unit) 56 | 57 | } 58 | 59 | } 60 | 61 | class AppsPresenterImpl( 62 | private val interactor: AppsInteractor, 63 | private val adapterPresenter: Lazy, 64 | private val appEntityConverter: AppEntityConverter, 65 | private val preferences: PreferencesProvider, 66 | private val schedulers: SchedulersFactory, 67 | state: Bundle? 68 | ) : AppsPresenter { 69 | 70 | private var view: AppsView? = null 71 | private var router: AppsPresenter.AppsRouter? = null 72 | 73 | private var entities: List? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 74 | state?.getParcelableArrayList(KEY_ENTITIES, AppEntity::class.java) 75 | } else { 76 | @Suppress("DEPRECATION") 77 | state?.getParcelableArrayList(KEY_ENTITIES) 78 | } 79 | 80 | private var packageMayBeDeleted: String? = state?.getString(KEY_PACKAGE_MAY_BE_DELETED) 81 | 82 | private val subscriptions = CompositeDisposable() 83 | 84 | override fun attachView(view: AppsView) { 85 | this.view = view 86 | subscriptions += view.refreshClicks().subscribe { loadAppItems() } 87 | subscriptions += view.prefsClicks().subscribe { onPrefsClicked() } 88 | subscriptions += view.infoClicks().subscribe { onInfoClicked() } 89 | subscriptions += view.appMenuClicks().subscribe { onAppMenuClicked(it) } 90 | subscriptions += view.searchTextChanged().subscribe { text -> filterApps(text) } 91 | subscriptions += view.searchCloseChanged().subscribe { filterApps("") } 92 | 93 | entities.takeIf { it != null } 94 | ?.run { applyAppEntities(this) } 95 | ?: loadAppItems() 96 | } 97 | 98 | private fun onPrefsClicked() { 99 | router?.showPrefsScreen() 100 | } 101 | 102 | private fun onInfoClicked() { 103 | router?.showInfoScreen() 104 | } 105 | 106 | private fun onAppMenuClicked(pair: Pair) { 107 | val item = pair.second 108 | when (pair.first) { 109 | ACTION_RUN_APP -> router?.runApp(item.packageName) 110 | ?.let { result -> 111 | if (!result) view?.showAppLaunchError() 112 | } 113 | ACTION_FIND_IN_GP -> { 114 | packageMayBeDeleted = item.packageName 115 | router?.openGooglePlay(item.packageName) 116 | } 117 | ACTION_SHARE_APP -> router?.requestPermissions( 118 | onGranted = { shareApp(item) }, 119 | onDenied = { view?.showWritePermissionsRequiredError() } 120 | ) 121 | ACTION_EXTRACT_APP -> router?.requestPermissions( 122 | onGranted = { extractApp(item) }, 123 | onDenied = { view?.showWritePermissionsRequiredError() } 124 | ) 125 | ACTION_SHOW_PERMISSIONS -> showPermissions(item) 126 | ACTION_SHOW_DETAILS -> { 127 | packageMayBeDeleted = item.packageName 128 | router?.showAppDetails(item.packageName) 129 | } 130 | ACTION_REMOVE_APP -> { 131 | packageMayBeDeleted = item.packageName 132 | router?.runAppUninstall(item.packageName) 133 | } 134 | } 135 | } 136 | 137 | override fun detachView() { 138 | subscriptions.clear() 139 | this.view = null 140 | } 141 | 142 | override fun attachRouter(router: AppsPresenter.AppsRouter) { 143 | this.router = router 144 | } 145 | 146 | override fun detachRouter() { 147 | this.router = null 148 | } 149 | 150 | override fun invalidateAppsList() { 151 | loadAppItems() 152 | } 153 | 154 | private fun loadAppItems() { 155 | subscriptions += interactor.loadApps( 156 | systemApps = preferences.isShowSystemApps(), 157 | runnableOnly = preferences.isRunnableOnly(), 158 | sortOrder = preferences.getSortOrder() 159 | ) 160 | .observeOn(schedulers.mainThread()) 161 | .doOnSubscribe { view?.showProgress() } 162 | .doAfterTerminate { view?.showContent() } 163 | .subscribe({ entities -> 164 | applyAppEntities(entities) 165 | }, {}) 166 | } 167 | 168 | private fun filterApps(query: String) { 169 | entities.takeIf { it != null } 170 | ?.filter { it.label.contains(query, true) } 171 | ?.run { bindAppEntities(this) } 172 | ?: loadAppItems() 173 | } 174 | 175 | private fun applyAppEntities(entities: List) { 176 | this.entities = entities 177 | bindAppEntities(entities) 178 | } 179 | 180 | private fun bindAppEntities(entities: List) { 181 | var id: Long = 0 182 | val items = entities 183 | .map { appEntityConverter.convert(id++, it) } 184 | val dataSource = ListDataSource(items) 185 | adapterPresenter.get().onDataSourceChanged(dataSource) 186 | view?.contentUpdated() 187 | } 188 | 189 | private fun shareApp(item: AppItem) { 190 | val entity = entities?.find { it.packageName == item.packageName } ?: return 191 | subscriptions += interactor.exportApp(entity) 192 | .observeOn(schedulers.mainThread()) 193 | .doOnSubscribe { view?.showProgress() } 194 | .doAfterTerminate { view?.showContent() } 195 | .subscribe({ file -> 196 | router?.shareApk(file) 197 | }, { 198 | view?.showAppExportError() 199 | }) 200 | } 201 | 202 | private fun extractApp(item: AppItem) { 203 | val entity = entities?.find { it.packageName == item.packageName } ?: return 204 | subscriptions += interactor.exportApp(entity) 205 | .observeOn(schedulers.mainThread()) 206 | .doOnSubscribe { view?.showProgress() } 207 | .doAfterTerminate { view?.showContent() } 208 | .subscribe({ 209 | view?.showExtractSuccess() 210 | }, { 211 | view?.showAppExportError() 212 | }) 213 | } 214 | 215 | private fun showPermissions(item: AppItem) { 216 | subscriptions += interactor.loadApp(item.packageName) 217 | .observeOn(schedulers.mainThread()) 218 | .subscribe({ entity -> 219 | entity.requestedPermissions?.let { 220 | router?.showRequestedPermissions(entity.requestedPermissions) 221 | } ?: view?.showNoRequestedPermissionsMessage() 222 | }, { 223 | view?.showUnableToGetPermissionsError() 224 | }) 225 | } 226 | 227 | override fun saveState() = Bundle().apply { 228 | entities?.let { putParcelableArrayList(KEY_ENTITIES, ArrayList(it)) } 229 | packageMayBeDeleted?.let { putString(KEY_PACKAGE_MAY_BE_DELETED, packageMayBeDeleted) } 230 | } 231 | 232 | override fun onBackPressed() { 233 | router?.leaveScreen() 234 | } 235 | 236 | override fun onResume() { 237 | packageMayBeDeleted?.let { pkg -> 238 | checkAppExist(pkg) 239 | packageMayBeDeleted = null 240 | } 241 | } 242 | 243 | private fun checkAppExist(packageName: String) { 244 | subscriptions += interactor.loadApp(packageName) 245 | .observeOn(schedulers.mainThread()) 246 | .subscribe({}, { 247 | entities?.let { actual -> 248 | val entity = actual.find { it.packageName == packageName } 249 | ?: return@subscribe 250 | applyAppEntities(actual - entity) 251 | } 252 | }) 253 | } 254 | 255 | override fun onItemClick(item: Item) { 256 | when (item) { 257 | is AppItem -> view?.showAppMenu(item) 258 | } 259 | } 260 | 261 | } 262 | 263 | private const val KEY_ENTITIES = "entities" 264 | private const val KEY_PACKAGE_MAY_BE_DELETED = "package_may_be_deleted" 265 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/AppsView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.os.Build 4 | import android.text.Html 5 | import android.text.Html.FROM_HTML_MODE_COMPACT 6 | import android.view.MenuItem 7 | import android.view.View 8 | import androidx.appcompat.widget.SearchView 9 | import androidx.appcompat.widget.Toolbar 10 | import androidx.recyclerview.widget.DefaultItemAnimator 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.avito.konveyor.adapter.SimpleRecyclerAdapter 14 | import com.github.rubensousa.bottomsheetbuilder.BottomSheetBuilder 15 | import com.google.android.material.snackbar.Snackbar 16 | import com.jakewharton.rxrelay3.PublishRelay 17 | import com.tomclaw.appsend_rb.R 18 | import com.tomclaw.appsend_rb.screen.apps.adapter.app.AppItem 19 | import com.tomclaw.appsend_rb.util.getAttributedColor 20 | import com.tomclaw.appsend_rb.util.hideWithAlphaAnimation 21 | import com.tomclaw.appsend_rb.util.showWithAlphaAnimation 22 | import io.reactivex.rxjava3.core.Observable 23 | 24 | interface AppsView { 25 | 26 | fun showProgress() 27 | 28 | fun showContent() 29 | 30 | fun contentUpdated() 31 | 32 | fun refreshClicks(): Observable 33 | 34 | fun prefsClicks(): Observable 35 | 36 | fun infoClicks(): Observable 37 | 38 | fun appMenuClicks(): Observable> 39 | 40 | fun searchTextChanged(): Observable 41 | 42 | fun searchCloseChanged(): Observable 43 | 44 | fun showAppMenu(item: AppItem) 45 | 46 | fun showExtractSuccess() 47 | 48 | fun showAppLaunchError() 49 | 50 | fun showAppExportError() 51 | 52 | fun showNoRequestedPermissionsMessage() 53 | 54 | fun showUnableToGetPermissionsError() 55 | 56 | fun showWritePermissionsRequiredError() 57 | 58 | } 59 | 60 | class AppsViewImpl( 61 | private val view: View, 62 | private val adapter: SimpleRecyclerAdapter, 63 | private val preferences: PreferencesProvider 64 | ) : AppsView { 65 | 66 | private val context = view.context 67 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 68 | private val recycler: RecyclerView = view.findViewById(R.id.recycler) 69 | private val overlayProgress: View = view.findViewById(R.id.overlay_progress) 70 | 71 | private val refreshRelay = PublishRelay.create() 72 | private val prefsRelay = PublishRelay.create() 73 | private val infoRelay = PublishRelay.create() 74 | private val appMenuRelay = PublishRelay.create>() 75 | private val searchTextRelay = PublishRelay.create() 76 | private val searchCloseRelay = PublishRelay.create() 77 | 78 | init { 79 | toolbar.setTitle(R.string.app_name) 80 | toolbar.inflateMenu(R.menu.main_menu) 81 | toolbar.setOnMenuItemClickListener { item -> 82 | when (item.itemId) { 83 | R.id.refresh -> refreshRelay.accept(Unit) 84 | R.id.settings -> prefsRelay.accept(Unit) 85 | R.id.info -> infoRelay.accept(Unit) 86 | } 87 | true 88 | } 89 | val searchMenu: MenuItem = toolbar.menu.findItem(R.id.menu_search) 90 | val searchView = searchMenu.actionView as SearchView 91 | searchView.queryHint = searchMenu.title 92 | searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 93 | override fun onQueryTextSubmit(query: String): Boolean { 94 | return true 95 | } 96 | 97 | override fun onQueryTextChange(newText: String): Boolean { 98 | searchTextRelay.accept(newText) 99 | return true 100 | } 101 | }) 102 | searchView.setOnCloseListener { searchCloseRelay.accept(Unit); false } 103 | searchView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { 104 | override fun onViewAttachedToWindow(v: View) {} 105 | override fun onViewDetachedFromWindow(v: View) { 106 | searchCloseRelay.accept(Unit) 107 | } 108 | }) 109 | adapter.setHasStableIds(true) 110 | recycler.adapter = adapter 111 | recycler.layoutManager = LinearLayoutManager( 112 | context, 113 | RecyclerView.VERTICAL, 114 | false 115 | ) 116 | recycler.itemAnimator = DefaultItemAnimator() 117 | } 118 | 119 | override fun showProgress() { 120 | overlayProgress.showWithAlphaAnimation(animateFully = true) 121 | } 122 | 123 | override fun showContent() { 124 | overlayProgress.hideWithAlphaAnimation(animateFully = false) 125 | } 126 | 127 | override fun contentUpdated() { 128 | adapter.notifyDataSetChanged() 129 | } 130 | 131 | override fun refreshClicks(): Observable = refreshRelay 132 | 133 | override fun prefsClicks(): Observable = prefsRelay 134 | 135 | override fun infoClicks(): Observable = infoRelay 136 | 137 | override fun appMenuClicks(): Observable> = appMenuRelay 138 | 139 | override fun searchTextChanged(): Observable = searchTextRelay 140 | 141 | override fun searchCloseChanged(): Observable = searchCloseRelay 142 | 143 | override fun showAppMenu(item: AppItem) { 144 | val theme = R.style.AppTheme_BottomSheetDialog_Dark.takeIf { preferences.isDarkTheme() } 145 | ?: R.style.AppTheme_BottomSheetDialog_Light 146 | BottomSheetBuilder(view.context, theme) 147 | .setMode(BottomSheetBuilder.MODE_LIST) 148 | .setIconTintColor(getAttributedColor(context, R.attr.menu_icons_tint)) 149 | .setItemTextColor(getAttributedColor(context, R.attr.text_primary_color)) 150 | .apply { 151 | addItem(ACTION_RUN_APP, R.string.run_app, R.drawable.run) 152 | addItem(ACTION_FIND_IN_GP, R.string.find_on_gp, R.drawable.google_play) 153 | addItem(ACTION_SHARE_APP, R.string.share_apk, R.drawable.share) 154 | addItem(ACTION_EXTRACT_APP, R.string.extract_apk, R.drawable.floppy) 155 | addItem( 156 | ACTION_SHOW_PERMISSIONS, 157 | R.string.required_permissions, 158 | R.drawable.lock_open 159 | ) 160 | addItem(ACTION_SHOW_DETAILS, R.string.app_details, R.drawable.settings_box) 161 | addItem(ACTION_REMOVE_APP, R.string.remove, R.drawable.delete) 162 | } 163 | .setItemClickListener { 164 | appMenuRelay.accept(Pair(it.itemId, item)) 165 | } 166 | .createDialog() 167 | .show() 168 | } 169 | 170 | override fun showExtractSuccess() { 171 | val text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 172 | Html.fromHtml( 173 | context.getString(R.string.app_extract_success), 174 | FROM_HTML_MODE_COMPACT 175 | ) 176 | } else { 177 | @Suppress("DEPRECATION") 178 | Html.fromHtml(context.getString(R.string.app_extract_success)) 179 | } 180 | Snackbar.make(recycler, text, Snackbar.LENGTH_LONG).show() 181 | } 182 | 183 | override fun showAppLaunchError() { 184 | Snackbar.make(recycler, R.string.non_launchable_package, Snackbar.LENGTH_LONG).show() 185 | } 186 | 187 | override fun showAppExportError() { 188 | Snackbar.make(recycler, R.string.app_extract_failed, Snackbar.LENGTH_LONG).show() 189 | } 190 | 191 | override fun showNoRequestedPermissionsMessage() { 192 | Snackbar.make(recycler, R.string.no_requested_permissions, Snackbar.LENGTH_LONG).show() 193 | } 194 | 195 | override fun showUnableToGetPermissionsError() { 196 | Snackbar.make(recycler, R.string.unable_to_get_permissions, Snackbar.LENGTH_LONG).show() 197 | } 198 | 199 | override fun showWritePermissionsRequiredError() { 200 | Snackbar.make(recycler, R.string.write_permission_extract, Snackbar.LENGTH_LONG).show() 201 | } 202 | 203 | } 204 | 205 | const val ACTION_RUN_APP = 0 206 | const val ACTION_FIND_IN_GP = 1 207 | const val ACTION_SHARE_APP = 2 208 | const val ACTION_EXTRACT_APP = 3 209 | const val ACTION_SHOW_PERMISSIONS = 4 210 | const val ACTION_SHOW_DETAILS = 5 211 | const val ACTION_REMOVE_APP = 6 212 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/OutputWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.content.ContentResolver 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Environment 9 | import android.provider.MediaStore 10 | import androidx.core.content.FileProvider 11 | import java.io.File 12 | import java.io.IOException 13 | import java.io.OutputStream 14 | 15 | interface OutputWrapper { 16 | 17 | fun getOutputUri(fileName: String, mimeType: String): Uri 18 | 19 | fun openStream(uri: Uri): OutputStream 20 | 21 | } 22 | 23 | class OutputWrapperImpl( 24 | private val context: Context, 25 | private val resolver: ContentResolver 26 | ) : OutputWrapper { 27 | 28 | override fun getOutputUri(fileName: String, mimeType: String): Uri { 29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 30 | val collections = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) 31 | val fileDetails = ContentValues() 32 | fileDetails.put(MediaStore.Images.Media.DISPLAY_NAME, fileName) 33 | fileDetails.put(MediaStore.Images.Media.MIME_TYPE, mimeType) 34 | return resolver.insert(collections, fileDetails) 35 | ?: throw IOException("unable to create URI") 36 | } else { 37 | val directory = File( 38 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), 39 | APPS_DIR_NAME 40 | ) 41 | if (!(directory.exists() || directory.mkdirs())) { 42 | throw IOException("unable to create directory") 43 | } 44 | val destination = File(directory, fileName) 45 | if (destination.exists() && !destination.delete()) { 46 | throw IOException("unable to delete destination file") 47 | } 48 | return FileProvider.getUriForFile(context, "${context.packageName}.provider", destination) 49 | } 50 | } 51 | 52 | override fun openStream(uri: Uri): OutputStream { 53 | return resolver.openOutputStream(uri) ?: throw IOException("unable to open stream") 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/PackageManagerWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.content.Intent 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageInfo 6 | import android.content.pm.PackageManager 7 | 8 | interface PackageManagerWrapper { 9 | 10 | fun getInstalledApplications(flags: Int): List 11 | 12 | fun getPackageInfo(packageName: String, flags: Int): PackageInfo 13 | 14 | fun getApplicationLabel(info: ApplicationInfo): String 15 | 16 | fun getLaunchIntentForPackage(packageName: String): Intent? 17 | 18 | } 19 | 20 | class PackageManagerWrapperImpl( 21 | private val packageManager: PackageManager 22 | ) : PackageManagerWrapper { 23 | 24 | override fun getInstalledApplications(flags: Int): List = 25 | packageManager.getInstalledApplications(flags) 26 | 27 | override fun getPackageInfo(packageName: String, flags: Int): PackageInfo = 28 | packageManager.getPackageInfo(packageName, flags) 29 | 30 | override fun getApplicationLabel(info: ApplicationInfo) = 31 | packageManager.getApplicationLabel(info).toString() 32 | 33 | override fun getLaunchIntentForPackage(packageName: String): Intent? = 34 | packageManager.getLaunchIntentForPackage(packageName) 35 | 36 | } 37 | 38 | const val FLAG_SYSTEM = ApplicationInfo.FLAG_SYSTEM 39 | const val FLAG_UPDATED_SYSTEM_APP = ApplicationInfo.FLAG_UPDATED_SYSTEM_APP 40 | const val GET_META_DATA = PackageManager.GET_META_DATA 41 | const val GET_PERMISSIONS = PackageManager.GET_PERMISSIONS 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/PreferencesProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.content.Context 4 | import com.tomclaw.appsend_rb.R 5 | import com.tomclaw.appsend_rb.util.getBooleanPreference 6 | import com.tomclaw.appsend_rb.util.getStringPreference 7 | 8 | interface PreferencesProvider { 9 | 10 | fun isDarkTheme(): Boolean 11 | 12 | fun isShowSystemApps(): Boolean 13 | 14 | fun isRunnableOnly(): Boolean 15 | 16 | fun getSortOrder(): Int 17 | 18 | } 19 | 20 | class PreferencesProviderImpl(private val context: Context) : PreferencesProvider { 21 | 22 | override fun isDarkTheme(): Boolean { 23 | return context.getBooleanPreference( 24 | R.string.pref_dark_theme, 25 | R.bool.pref_dark_theme_default 26 | ) 27 | } 28 | 29 | override fun isShowSystemApps(): Boolean { 30 | return context.getBooleanPreference( 31 | R.string.pref_show_system, 32 | R.bool.pref_show_system_default 33 | ) 34 | } 35 | 36 | override fun isRunnableOnly(): Boolean { 37 | return context.getBooleanPreference( 38 | R.string.pref_runnable, 39 | R.bool.pref_runnable_default 40 | ) 41 | } 42 | 43 | override fun getSortOrder(): Int { 44 | return context.getStringPreference( 45 | R.string.pref_sort_order, 46 | R.string.pref_sort_order_default 47 | ).run { 48 | when (this) { 49 | context.getString(R.string.sort_order_ascending_value) -> NAME_ASCENDING 50 | context.getString(R.string.sort_order_descending_value) -> NAME_DESCENDING 51 | context.getString(R.string.sort_order_app_size_value) -> APP_SIZE 52 | context.getString(R.string.sort_order_install_time_value) -> INSTALL_TIME 53 | context.getString(R.string.sort_order_update_time_value) -> UPDATE_TIME 54 | else -> NAME_ASCENDING 55 | } 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/ResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps 2 | 3 | import android.content.Context 4 | import com.tomclaw.appsend_rb.util.formatBytesToString 5 | import java.text.DateFormat 6 | 7 | interface ResourceProvider { 8 | 9 | fun formatBytes(bytes: Long): String 10 | 11 | fun formatTime(time: Long): String 12 | 13 | } 14 | 15 | class ResourceProviderImpl( 16 | private val context: Context, 17 | private val dateFormat: DateFormat 18 | ) : ResourceProvider { 19 | 20 | override fun formatBytes(bytes: Long): String { 21 | return formatBytesToString(context.resources, bytes) 22 | } 23 | 24 | override fun formatTime(time: Long): String { 25 | return dateFormat.format(time) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/adapter/ItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps.adapter 2 | 3 | import com.avito.konveyor.blueprint.Item 4 | 5 | interface ItemClickListener { 6 | 7 | fun onItemClick(item: Item) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/adapter/app/AppItem.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps.adapter.app 2 | 3 | import android.os.Parcelable 4 | import com.avito.konveyor.blueprint.Item 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class AppItem( 9 | override val id: Long, 10 | val icon: String?, 11 | val packageName: String, 12 | val name: String, 13 | val size: String, 14 | val firstInstallTime: String, 15 | val lastUpdateTime: String, 16 | val versionName: String, 17 | val versionCode: Long, 18 | val newApp: Boolean 19 | ) : Item, Parcelable -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/adapter/app/AppItemBlueprint.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps.adapter.app 2 | 3 | import com.avito.konveyor.blueprint.Item 4 | import com.avito.konveyor.blueprint.ItemBlueprint 5 | import com.avito.konveyor.blueprint.ItemPresenter 6 | import com.avito.konveyor.blueprint.ViewHolderBuilder 7 | import com.tomclaw.appsend_rb.R 8 | 9 | class AppItemBlueprint(override val presenter: ItemPresenter) : 10 | ItemBlueprint { 11 | 12 | override val viewHolderProvider = ViewHolderBuilder.ViewHolderProvider( 13 | layoutId = R.layout.app_item, 14 | creator = { _, view -> AppItemViewHolder(view) } 15 | ) 16 | 17 | override fun isRelevantItem(item: Item) = item is AppItem 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/adapter/app/AppItemPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps.adapter.app 2 | 3 | import com.avito.konveyor.blueprint.ItemPresenter 4 | import com.tomclaw.appsend_rb.screen.apps.adapter.ItemClickListener 5 | 6 | class AppItemPresenter( 7 | private val listener: ItemClickListener 8 | ) : ItemPresenter { 9 | 10 | override fun bindView(view: AppItemView, item: AppItem, position: Int) { 11 | view.setIcon(item.icon) 12 | view.setName(item.name) 13 | view.setVersion(item.versionName) 14 | view.setSize(item.size) 15 | view.setTime(item.lastUpdateTime) 16 | view.setBadgeVisible(item.newApp) 17 | view.setOnClickListener { listener.onItemClick(item) } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/adapter/app/AppItemView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps.adapter.app 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.ImageView 6 | import android.widget.TextView 7 | import com.avito.konveyor.adapter.BaseViewHolder 8 | import com.avito.konveyor.blueprint.ItemView 9 | import com.tomclaw.appsend_rb.R 10 | import com.tomclaw.appsend_rb.util.bind 11 | import com.tomclaw.appsend_rb.util.hide 12 | import com.tomclaw.appsend_rb.util.show 13 | import com.tomclaw.imageloader.util.centerCrop 14 | import com.tomclaw.imageloader.util.fetch 15 | import com.tomclaw.imageloader.util.withPlaceholder 16 | 17 | interface AppItemView : ItemView { 18 | 19 | fun setIcon(url: String?) 20 | 21 | fun setName(name: String) 22 | 23 | fun setVersion(version: String?) 24 | 25 | fun setSize(size: String?) 26 | 27 | fun setTime(time: String?) 28 | 29 | fun setBadgeVisible(visible: Boolean) 30 | 31 | fun setOnClickListener(listener: (() -> Unit)?) 32 | 33 | } 34 | 35 | class AppItemViewHolder(view: View) : BaseViewHolder(view), AppItemView { 36 | 37 | private val icon: ImageView = view.findViewById(R.id.app_icon) 38 | private val name: TextView = view.findViewById(R.id.app_name) 39 | private val version: TextView = view.findViewById(R.id.app_version) 40 | private val size: TextView = view.findViewById(R.id.app_size) 41 | private val time: TextView = view.findViewById(R.id.app_time) 42 | private val badge: View = view.findViewById(R.id.badge_new) 43 | 44 | private var listener: (() -> Unit)? = null 45 | 46 | init { 47 | view.setOnClickListener { listener?.invoke() } 48 | } 49 | 50 | override fun setIcon(url: String?) { 51 | icon.fetch(url.orEmpty()) { 52 | centerCrop() 53 | withPlaceholder(R.drawable.app_placeholder) 54 | placeholder = { 55 | with(it.get()) { 56 | scaleType = android.widget.ImageView.ScaleType.CENTER_CROP 57 | setImageResource(R.drawable.app_placeholder) 58 | } 59 | } 60 | } 61 | } 62 | 63 | override fun setName(name: String) { 64 | this.name.bind(name) 65 | } 66 | 67 | override fun setVersion(version: String?) { 68 | this.version.bind(version) 69 | } 70 | 71 | override fun setSize(size: String?) { 72 | this.size.bind(size) 73 | } 74 | 75 | override fun setTime(time: String?) { 76 | this.time.bind(time) 77 | } 78 | 79 | override fun setBadgeVisible(visible: Boolean) { 80 | if (visible) { 81 | badge.show() 82 | } else { 83 | badge.hide() 84 | } 85 | } 86 | 87 | override fun setOnClickListener(listener: (() -> Unit)?) { 88 | this.listener = listener 89 | } 90 | 91 | override fun onUnbind() { 92 | this.listener = null 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/di/AppsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps.di 2 | 3 | import com.tomclaw.appsend_rb.screen.apps.AppsActivity 4 | import com.tomclaw.appsend_rb.util.PerActivity 5 | import dagger.Subcomponent 6 | 7 | @PerActivity 8 | @Subcomponent(modules = [AppsModule::class]) 9 | interface AppsComponent { 10 | 11 | fun inject(activity: AppsActivity) 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/apps/di/AppsModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.apps.di 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Bundle 6 | import com.avito.konveyor.ItemBinder 7 | import com.avito.konveyor.adapter.AdapterPresenter 8 | import com.avito.konveyor.adapter.SimpleAdapterPresenter 9 | import com.avito.konveyor.blueprint.ItemBlueprint 10 | import com.tomclaw.appsend_rb.screen.apps.AppEntityConverter 11 | import com.tomclaw.appsend_rb.screen.apps.AppEntityConverterImpl 12 | import com.tomclaw.appsend_rb.screen.apps.AppsInteractor 13 | import com.tomclaw.appsend_rb.screen.apps.AppsInteractorImpl 14 | import com.tomclaw.appsend_rb.screen.apps.AppsPresenter 15 | import com.tomclaw.appsend_rb.screen.apps.AppsPresenterImpl 16 | import com.tomclaw.appsend_rb.screen.apps.OutputWrapper 17 | import com.tomclaw.appsend_rb.screen.apps.OutputWrapperImpl 18 | import com.tomclaw.appsend_rb.screen.apps.PackageManagerWrapper 19 | import com.tomclaw.appsend_rb.screen.apps.PackageManagerWrapperImpl 20 | import com.tomclaw.appsend_rb.screen.apps.PreferencesProvider 21 | import com.tomclaw.appsend_rb.screen.apps.PreferencesProviderImpl 22 | import com.tomclaw.appsend_rb.screen.apps.ResourceProvider 23 | import com.tomclaw.appsend_rb.screen.apps.ResourceProviderImpl 24 | import com.tomclaw.appsend_rb.screen.apps.adapter.app.AppItemBlueprint 25 | import com.tomclaw.appsend_rb.screen.apps.adapter.app.AppItemPresenter 26 | import com.tomclaw.appsend_rb.util.PerActivity 27 | import com.tomclaw.appsend_rb.util.SchedulersFactory 28 | import dagger.Lazy 29 | import dagger.Module 30 | import dagger.Provides 31 | import dagger.multibindings.IntoSet 32 | import java.text.DateFormat 33 | import java.text.SimpleDateFormat 34 | import java.util.Locale 35 | 36 | @Module 37 | class AppsModule( 38 | private val context: Context, 39 | private val state: Bundle? 40 | ) { 41 | 42 | @Provides 43 | @PerActivity 44 | internal fun provideAdapterPresenter(binder: ItemBinder): AdapterPresenter { 45 | return SimpleAdapterPresenter(binder, binder) 46 | } 47 | 48 | @Provides 49 | @PerActivity 50 | internal fun providePresenter( 51 | interactor: AppsInteractor, 52 | adapterPresenter: Lazy, 53 | appEntityConverter: AppEntityConverter, 54 | preferences: PreferencesProvider, 55 | schedulers: SchedulersFactory 56 | ): AppsPresenter = AppsPresenterImpl( 57 | interactor, 58 | adapterPresenter, 59 | appEntityConverter, 60 | preferences, 61 | schedulers, 62 | state 63 | ) 64 | 65 | @Provides 66 | @PerActivity 67 | internal fun provideInteractor( 68 | packageManager: PackageManagerWrapper, 69 | outputWrapper: OutputWrapper, 70 | locale: Locale, 71 | schedulers: SchedulersFactory 72 | ): AppsInteractor = AppsInteractorImpl(packageManager, outputWrapper, locale, schedulers) 73 | 74 | @Provides 75 | @PerActivity 76 | internal fun provideAppInfoConverter(resourceProvider: ResourceProvider): AppEntityConverter { 77 | return AppEntityConverterImpl(resourceProvider) 78 | } 79 | 80 | @Provides 81 | @PerActivity 82 | internal fun provideResourceProvider(dateFormat: DateFormat): ResourceProvider { 83 | return ResourceProviderImpl(context, dateFormat) 84 | } 85 | 86 | @SuppressLint("SimpleDateFormat") 87 | @Provides 88 | @PerActivity 89 | internal fun provideDateFormat(): DateFormat { 90 | return SimpleDateFormat("dd.MM.yy") 91 | } 92 | 93 | @Provides 94 | @PerActivity 95 | internal fun providePreferencesProvider(): PreferencesProvider { 96 | return PreferencesProviderImpl(context) 97 | } 98 | 99 | @Provides 100 | @PerActivity 101 | internal fun providePackageManagerWrapper(): PackageManagerWrapper { 102 | return PackageManagerWrapperImpl(context.packageManager) 103 | } 104 | 105 | @Provides 106 | @PerActivity 107 | internal fun provideOutputWrapper(): OutputWrapper { 108 | return OutputWrapperImpl(context, context.contentResolver) 109 | } 110 | 111 | @Provides 112 | @PerActivity 113 | internal fun provideItemBinder( 114 | blueprintSet: Set<@JvmSuppressWildcards ItemBlueprint<*, *>> 115 | ): ItemBinder { 116 | return ItemBinder.Builder().apply { 117 | blueprintSet.forEach { registerItem(it) } 118 | }.build() 119 | } 120 | 121 | @Provides 122 | @IntoSet 123 | @PerActivity 124 | internal fun provideAppItemBlueprint( 125 | presenter: AppItemPresenter 126 | ): ItemBlueprint<*, *> = AppItemBlueprint(presenter) 127 | 128 | @Provides 129 | @PerActivity 130 | internal fun provideAppItemPresenter(presenter: AppsPresenter) = 131 | AppItemPresenter(presenter) 132 | 133 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/PermissionInfoProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions 2 | 3 | import android.content.pm.PackageManager 4 | import android.content.pm.PackageManager.GET_META_DATA 5 | import android.content.pm.PermissionInfo 6 | import android.os.Build 7 | import android.os.Parcelable 8 | import kotlinx.parcelize.Parcelize 9 | import java.util.Locale 10 | 11 | interface PermissionInfoProvider { 12 | 13 | fun getPermissionBrief(permission: String): PermissionInfoProviderImpl.PermissionBrief 14 | 15 | } 16 | 17 | class PermissionInfoProviderImpl( 18 | private val packageManager: PackageManager, 19 | private val locale: Locale 20 | ) : PermissionInfoProvider { 21 | 22 | override fun getPermissionBrief(permission: String): PermissionBrief { 23 | var description: String? 24 | var dangerous: Boolean 25 | try { 26 | val permissionInfo = packageManager.getPermissionInfo(permission, GET_META_DATA) 27 | description = permissionInfo.loadLabel(packageManager).toString().capitalize(locale) 28 | dangerous = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 29 | permissionInfo.protection == PermissionInfo.PROTECTION_DANGEROUS 30 | } else { 31 | @Suppress("DEPRECATION") 32 | permissionInfo.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS 33 | } 34 | } catch (ignored: Throwable) { 35 | description = null 36 | dangerous = false 37 | } 38 | return PermissionBrief(permission, description, dangerous) 39 | } 40 | 41 | @Parcelize 42 | data class PermissionBrief( 43 | val permission: String, 44 | val description: String?, 45 | val dangerous: Boolean 46 | ) : Parcelable 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/PermissionsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.avito.konveyor.ItemBinder 8 | import com.avito.konveyor.adapter.AdapterPresenter 9 | import com.avito.konveyor.adapter.SimpleRecyclerAdapter 10 | import com.tomclaw.appsend.util.Analytics 11 | import com.tomclaw.appsend_rb.R 12 | import com.tomclaw.appsend_rb.getComponent 13 | import com.tomclaw.appsend_rb.screen.permissions.di.PermissionsModule 14 | import com.tomclaw.appsend_rb.util.updateTheme 15 | import javax.inject.Inject 16 | 17 | class PermissionsActivity : AppCompatActivity(), PermissionsPresenter.PermissionsRouter { 18 | 19 | @Inject 20 | lateinit var presenter: PermissionsPresenter 21 | 22 | @Inject 23 | lateinit var adapterPresenter: AdapterPresenter 24 | 25 | @Inject 26 | lateinit var binder: ItemBinder 27 | 28 | @Inject 29 | lateinit var analytics: Analytics 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | val permissions = intent?.getStringArrayListExtra(EXTRA_PERMISSIONS) 33 | ?: throw IllegalArgumentException("Permissions list is required") 34 | val presenterState = savedInstanceState?.getBundle(KEY_PRESENTER_STATE) 35 | application.getComponent() 36 | .permissionsComponent(PermissionsModule(this, permissions, presenterState)) 37 | .inject(activity = this) 38 | updateTheme(this) 39 | 40 | super.onCreate(savedInstanceState) 41 | setContentView(R.layout.permissions_activity) 42 | 43 | val adapter = SimpleRecyclerAdapter(adapterPresenter, binder) 44 | val view = PermissionsViewImpl(window.decorView, adapter) 45 | 46 | presenter.attachView(view) 47 | 48 | if (savedInstanceState == null) { 49 | analytics.trackEvent("open-permissions-screen") 50 | } 51 | } 52 | 53 | @Deprecated("Deprecated in Java") 54 | override fun onBackPressed() { 55 | super.onBackPressed() 56 | presenter.onBackPressed() 57 | } 58 | 59 | override fun onStart() { 60 | super.onStart() 61 | presenter.attachRouter(this) 62 | } 63 | 64 | override fun onStop() { 65 | presenter.detachRouter() 66 | super.onStop() 67 | } 68 | 69 | override fun onDestroy() { 70 | presenter.detachView() 71 | super.onDestroy() 72 | } 73 | 74 | override fun onSaveInstanceState(outState: Bundle) { 75 | super.onSaveInstanceState(outState) 76 | outState.putBundle(KEY_PRESENTER_STATE, presenter.saveState()) 77 | } 78 | 79 | override fun leaveScreen() { 80 | finish() 81 | } 82 | 83 | } 84 | 85 | fun createPermissionsActivityIntent( 86 | context: Context, 87 | permissions: List 88 | ): Intent = Intent(context, PermissionsActivity::class.java) 89 | .putStringArrayListExtra(EXTRA_PERMISSIONS, ArrayList(permissions)) 90 | 91 | private const val EXTRA_PERMISSIONS = "permissions" 92 | private const val KEY_PRESENTER_STATE = "presenter_state" 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/PermissionsConverter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions 2 | 3 | import com.avito.konveyor.blueprint.Item 4 | import com.tomclaw.appsend_rb.screen.permissions.adapter.safe.SafePermissionItem 5 | import com.tomclaw.appsend_rb.screen.permissions.adapter.unsafe.UnsafePermissionItem 6 | 7 | interface PermissionsConverter { 8 | 9 | fun convert(permission: String): Item 10 | 11 | } 12 | 13 | class PermissionsConverterImpl( 14 | private val permissionInfoProvider: PermissionInfoProvider 15 | ) : PermissionsConverter { 16 | 17 | private var id: Long = 1 18 | 19 | override fun convert(permission: String): Item { 20 | val permissionBrief = permissionInfoProvider.getPermissionBrief(permission) 21 | return when (permissionBrief.dangerous) { 22 | true -> UnsafePermissionItem( 23 | id = id++, 24 | permission = permissionBrief.permission, 25 | description = permissionBrief.description 26 | ) 27 | 28 | false -> SafePermissionItem( 29 | id = id++, 30 | permission = permissionBrief.permission, 31 | description = permissionBrief.description 32 | ) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/PermissionsPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions 2 | 3 | import android.os.Bundle 4 | import com.avito.konveyor.adapter.AdapterPresenter 5 | import com.avito.konveyor.data_source.ListDataSource 6 | import com.tomclaw.appsend_rb.util.SchedulersFactory 7 | import dagger.Lazy 8 | import io.reactivex.rxjava3.disposables.CompositeDisposable 9 | import io.reactivex.rxjava3.kotlin.plusAssign 10 | 11 | interface PermissionsPresenter { 12 | 13 | fun attachView(view: PermissionsView) 14 | 15 | fun detachView() 16 | 17 | fun attachRouter(router: PermissionsRouter) 18 | 19 | fun detachRouter() 20 | 21 | fun saveState(): Bundle 22 | 23 | fun onBackPressed() 24 | 25 | interface PermissionsRouter { 26 | 27 | fun leaveScreen() 28 | 29 | } 30 | 31 | } 32 | 33 | class PermissionsPresenterImpl( 34 | private val permissions: List, 35 | private val adapterPresenter: Lazy, 36 | private val converter: PermissionsConverter, 37 | private val schedulers: SchedulersFactory, 38 | state: Bundle? 39 | ) : PermissionsPresenter { 40 | 41 | private var view: PermissionsView? = null 42 | private var router: PermissionsPresenter.PermissionsRouter? = null 43 | 44 | private val subscriptions = CompositeDisposable() 45 | 46 | override fun attachView(view: PermissionsView) { 47 | this.view = view 48 | 49 | subscriptions += view.navigationClicks().subscribe { 50 | onBackPressed() 51 | } 52 | 53 | bindPermissions() 54 | } 55 | 56 | override fun detachView() { 57 | subscriptions.clear() 58 | this.view = null 59 | } 60 | 61 | override fun attachRouter(router: PermissionsPresenter.PermissionsRouter) { 62 | this.router = router 63 | } 64 | 65 | override fun detachRouter() { 66 | this.router = null 67 | } 68 | 69 | override fun saveState() = Bundle().apply { 70 | } 71 | 72 | override fun onBackPressed() { 73 | router?.leaveScreen() 74 | } 75 | 76 | private fun bindPermissions() { 77 | val items = permissions.map { converter.convert(it) } 78 | val dataSource = ListDataSource(items) 79 | adapterPresenter.get().onDataSourceChanged(dataSource) 80 | view?.contentUpdated() 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/PermissionsResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions 2 | 3 | import android.content.res.Resources 4 | import com.tomclaw.appsend_rb.R 5 | 6 | interface PermissionsResourceProvider { 7 | 8 | fun getUnknownPermissionString(): String 9 | } 10 | 11 | class PermissionsResourceProviderImpl( 12 | private val resources: Resources 13 | ) : PermissionsResourceProvider { 14 | 15 | override fun getUnknownPermissionString(): String { 16 | return resources.getString(R.string.unknown_permission_description) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/PermissionsView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.View 5 | import androidx.appcompat.widget.Toolbar 6 | import androidx.recyclerview.widget.DefaultItemAnimator 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.avito.konveyor.adapter.SimpleRecyclerAdapter 10 | import com.jakewharton.rxrelay3.PublishRelay 11 | import com.tomclaw.appsend_rb.R 12 | import io.reactivex.rxjava3.core.Observable 13 | 14 | interface PermissionsView { 15 | 16 | fun contentUpdated() 17 | 18 | fun navigationClicks(): Observable 19 | 20 | } 21 | 22 | class PermissionsViewImpl( 23 | private val view: View, 24 | private val adapter: SimpleRecyclerAdapter 25 | ) : PermissionsView { 26 | 27 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 28 | private val recycler: RecyclerView = view.findViewById(R.id.recycler) 29 | 30 | private val navigationRelay = PublishRelay.create() 31 | 32 | init { 33 | toolbar.setTitle(R.string.required_permissions) 34 | toolbar.setNavigationOnClickListener { navigationRelay.accept(Unit) } 35 | 36 | val orientation = RecyclerView.VERTICAL 37 | val layoutManager = LinearLayoutManager(view.context, orientation, false) 38 | adapter.setHasStableIds(true) 39 | recycler.adapter = adapter 40 | recycler.layoutManager = layoutManager 41 | recycler.itemAnimator = DefaultItemAnimator() 42 | recycler.itemAnimator?.changeDuration = DURATION_MEDIUM 43 | } 44 | 45 | @SuppressLint("NotifyDataSetChanged") 46 | override fun contentUpdated() { 47 | adapter.notifyDataSetChanged() 48 | } 49 | 50 | override fun navigationClicks(): Observable = navigationRelay 51 | 52 | } 53 | 54 | private const val DURATION_MEDIUM = 300L 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/safe/SafePermissionItem.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.safe 2 | 3 | import android.os.Parcelable 4 | import com.avito.konveyor.blueprint.Item 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class SafePermissionItem( 9 | override val id: Long, 10 | val description: String?, 11 | val permission: String, 12 | ) : Item, Parcelable 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/safe/SafePermissionItemBlueprint.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.safe 2 | 3 | import com.avito.konveyor.blueprint.Item 4 | import com.avito.konveyor.blueprint.ItemBlueprint 5 | import com.avito.konveyor.blueprint.ItemPresenter 6 | import com.avito.konveyor.blueprint.ViewHolderBuilder 7 | import com.tomclaw.appsend_rb.R 8 | 9 | class SafePermissionItemBlueprint(override val presenter: ItemPresenter) : 10 | ItemBlueprint { 11 | 12 | override val viewHolderProvider = 13 | ViewHolderBuilder.ViewHolderProvider( 14 | layoutId = R.layout.permission_safe, 15 | creator = { _, view -> SafePermissionItemViewHolder(view) } 16 | ) 17 | 18 | override fun isRelevantItem(item: Item) = item is SafePermissionItem 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/safe/SafePermissionItemPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.safe 2 | 3 | import com.avito.konveyor.blueprint.ItemPresenter 4 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsResourceProvider 5 | 6 | class SafePermissionItemPresenter( 7 | private val resourceProvider: PermissionsResourceProvider 8 | ) : ItemPresenter { 9 | 10 | override fun bindView(view: SafePermissionItemView, item: SafePermissionItem, position: Int) { 11 | val description = item.description ?: resourceProvider.getUnknownPermissionString() 12 | view.setDescription(description) 13 | view.setPermission(item.permission) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/safe/SafePermissionItemView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.safe 2 | 3 | import android.view.View 4 | import android.widget.TextView 5 | import com.avito.konveyor.adapter.BaseViewHolder 6 | import com.avito.konveyor.blueprint.ItemView 7 | import com.tomclaw.appsend_rb.R 8 | import com.tomclaw.appsend_rb.util.bind 9 | 10 | interface SafePermissionItemView : ItemView { 11 | 12 | fun setDescription(value: String?) 13 | 14 | fun setPermission(value: String) 15 | 16 | } 17 | 18 | class SafePermissionItemViewHolder(view: View) : BaseViewHolder(view), SafePermissionItemView { 19 | 20 | private val description: TextView = view.findViewById(R.id.description) 21 | private val permission: TextView = view.findViewById(R.id.permission) 22 | 23 | override fun setDescription(value: String?) { 24 | description.bind(value) 25 | } 26 | 27 | override fun setPermission(value: String) { 28 | permission.bind(value) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/unsafe/UnsafePermissionItem.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.unsafe 2 | 3 | import android.os.Parcelable 4 | import com.avito.konveyor.blueprint.Item 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class UnsafePermissionItem( 9 | override val id: Long, 10 | val description: String?, 11 | val permission: String, 12 | ) : Item, Parcelable 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/unsafe/UnsafePermissionItemBlueprint.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.unsafe 2 | 3 | import com.avito.konveyor.blueprint.Item 4 | import com.avito.konveyor.blueprint.ItemBlueprint 5 | import com.avito.konveyor.blueprint.ItemPresenter 6 | import com.avito.konveyor.blueprint.ViewHolderBuilder 7 | import com.tomclaw.appsend_rb.R 8 | 9 | class UnsafePermissionItemBlueprint(override val presenter: ItemPresenter) : 10 | ItemBlueprint { 11 | 12 | override val viewHolderProvider = ViewHolderBuilder.ViewHolderProvider( 13 | layoutId = R.layout.permission_unsafe, 14 | creator = { _, view -> UnsafePermissionItemViewHolder(view) } 15 | ) 16 | 17 | override fun isRelevantItem(item: Item) = item is UnsafePermissionItem 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/unsafe/UnsafePermissionItemPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.unsafe 2 | 3 | import com.avito.konveyor.blueprint.ItemPresenter 4 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsResourceProvider 5 | 6 | class UnsafePermissionItemPresenter( 7 | private val resourceProvider: PermissionsResourceProvider 8 | ) : ItemPresenter { 9 | 10 | override fun bindView(view: UnsafePermissionItemView, item: UnsafePermissionItem, position: Int) { 11 | val description = item.description ?: resourceProvider.getUnknownPermissionString() 12 | view.setDescription(description) 13 | view.setPermission(item.permission) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/adapter/unsafe/UnsafePermissionItemView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.adapter.unsafe 2 | 3 | import android.view.View 4 | import android.widget.TextView 5 | import com.avito.konveyor.adapter.BaseViewHolder 6 | import com.avito.konveyor.blueprint.ItemView 7 | import com.tomclaw.appsend_rb.R 8 | import com.tomclaw.appsend_rb.util.bind 9 | 10 | interface UnsafePermissionItemView : ItemView { 11 | 12 | fun setDescription(value: String?) 13 | 14 | fun setPermission(value: String) 15 | 16 | } 17 | 18 | class UnsafePermissionItemViewHolder(view: View) : BaseViewHolder(view), UnsafePermissionItemView { 19 | 20 | private val description: TextView = view.findViewById(R.id.description) 21 | private val permission: TextView = view.findViewById(R.id.permission) 22 | 23 | override fun setDescription(value: String?) { 24 | description.bind(value) 25 | } 26 | 27 | override fun setPermission(value: String) { 28 | permission.bind(value) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/di/PermissionsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.di 2 | 3 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsActivity 4 | import com.tomclaw.appsend_rb.util.PerActivity 5 | import dagger.Subcomponent 6 | 7 | @PerActivity 8 | @Subcomponent(modules = [PermissionsModule::class]) 9 | interface PermissionsComponent { 10 | 11 | fun inject(activity: PermissionsActivity) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/screen/permissions/di/PermissionsModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.screen.permissions.di 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.os.Bundle 6 | import com.avito.konveyor.ItemBinder 7 | import com.avito.konveyor.adapter.AdapterPresenter 8 | import com.avito.konveyor.adapter.SimpleAdapterPresenter 9 | import com.avito.konveyor.blueprint.ItemBlueprint 10 | import com.tomclaw.appsend_rb.screen.permissions.PermissionInfoProvider 11 | import com.tomclaw.appsend_rb.screen.permissions.PermissionInfoProviderImpl 12 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsConverter 13 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsConverterImpl 14 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsPresenter 15 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsPresenterImpl 16 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsResourceProvider 17 | import com.tomclaw.appsend_rb.screen.permissions.PermissionsResourceProviderImpl 18 | import com.tomclaw.appsend_rb.screen.permissions.adapter.safe.SafePermissionItemBlueprint 19 | import com.tomclaw.appsend_rb.screen.permissions.adapter.safe.SafePermissionItemPresenter 20 | import com.tomclaw.appsend_rb.screen.permissions.adapter.unsafe.UnsafePermissionItemBlueprint 21 | import com.tomclaw.appsend_rb.screen.permissions.adapter.unsafe.UnsafePermissionItemPresenter 22 | import com.tomclaw.appsend_rb.util.PerActivity 23 | import com.tomclaw.appsend_rb.util.SchedulersFactory 24 | import dagger.Lazy 25 | import dagger.Module 26 | import dagger.Provides 27 | import dagger.multibindings.IntoSet 28 | import java.util.Locale 29 | 30 | @Module 31 | class PermissionsModule( 32 | private val context: Context, 33 | private val permissions: List, 34 | private val state: Bundle? 35 | ) { 36 | 37 | @Provides 38 | @PerActivity 39 | internal fun providePresenter( 40 | adapterPresenter: Lazy, 41 | converter: PermissionsConverter, 42 | schedulers: SchedulersFactory 43 | ): PermissionsPresenter = 44 | PermissionsPresenterImpl(permissions, adapterPresenter, converter, schedulers, state) 45 | 46 | @Provides 47 | @PerActivity 48 | internal fun providePermissionInfoProvider( 49 | packageManager: PackageManager, 50 | locale: Locale 51 | ): PermissionInfoProvider { 52 | return PermissionInfoProviderImpl(packageManager, locale) 53 | } 54 | 55 | @Provides 56 | @PerActivity 57 | internal fun providePermissionsResourceProvider(): PermissionsResourceProvider { 58 | return PermissionsResourceProviderImpl(context.resources) 59 | } 60 | 61 | @Provides 62 | @PerActivity 63 | internal fun providePermissionsConverter( 64 | permissionInfoProvider: PermissionInfoProvider 65 | ): PermissionsConverter { 66 | return PermissionsConverterImpl(permissionInfoProvider) 67 | } 68 | 69 | @Provides 70 | @PerActivity 71 | internal fun provideAdapterPresenter(binder: ItemBinder): AdapterPresenter { 72 | return SimpleAdapterPresenter(binder, binder) 73 | } 74 | 75 | @Provides 76 | @PerActivity 77 | internal fun provideItemBinder( 78 | blueprintSet: Set<@JvmSuppressWildcards ItemBlueprint<*, *>> 79 | ): ItemBinder { 80 | return ItemBinder.Builder().apply { 81 | blueprintSet.forEach { registerItem(it) } 82 | }.build() 83 | } 84 | 85 | @Provides 86 | @IntoSet 87 | @PerActivity 88 | internal fun provideSafeItemBlueprint( 89 | presenter: SafePermissionItemPresenter 90 | ): ItemBlueprint<*, *> = SafePermissionItemBlueprint(presenter) 91 | 92 | @Provides 93 | @PerActivity 94 | internal fun provideSafePermissionItemPresenter( 95 | resourceProvider: PermissionsResourceProvider 96 | ) = SafePermissionItemPresenter(resourceProvider) 97 | 98 | @Provides 99 | @IntoSet 100 | @PerActivity 101 | internal fun provideUnsafePermissionItemBlueprint( 102 | presenter: UnsafePermissionItemPresenter 103 | ): ItemBlueprint<*, *> = UnsafePermissionItemBlueprint(presenter) 104 | 105 | @Provides 106 | @PerActivity 107 | internal fun provideUnsafePermissionItemPresenter( 108 | resourceProvider: PermissionsResourceProvider 109 | ) = UnsafePermissionItemPresenter(resourceProvider) 110 | 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Activities.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.app.Activity 4 | import com.jaeger.library.StatusBarUtil 5 | import com.tomclaw.appsend_rb.R 6 | import com.tomclaw.appsend_rb.screen.apps.PreferencesProvider 7 | 8 | fun Activity.updateTheme(preferences: PreferencesProvider): Boolean { 9 | val isDarkTheme = preferences.isDarkTheme() 10 | setTheme(if (isDarkTheme) R.style.AppThemeBlack else R.style.AppTheme) 11 | return isDarkTheme 12 | } 13 | 14 | fun Activity.updateStatusBar() { 15 | val color = getAttributedColor(this, R.attr.toolbar_background) 16 | StatusBarUtil.setColor(this, color) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Analytics.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend.util 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.text.TextUtils 8 | import com.microsoft.appcenter.AppCenter 9 | 10 | interface Analytics { 11 | 12 | fun register() 13 | 14 | fun trackEvent(name: String) 15 | 16 | } 17 | 18 | class AnalyticsImpl( 19 | private val app: Application, 20 | ) : Analytics { 21 | 22 | override fun register() { 23 | val appIdentifier = getAppIdentifier(app.applicationContext) 24 | AppCenter.start( 25 | app, appIdentifier, 26 | com.microsoft.appcenter.analytics.Analytics::class.java, 27 | com.microsoft.appcenter.crashes.Crashes::class.java 28 | ) 29 | } 30 | 31 | override fun trackEvent(name: String) { 32 | com.microsoft.appcenter.analytics.Analytics.trackEvent(name) 33 | } 34 | 35 | private fun getAppIdentifier(context: Context): String? { 36 | val appID = getManifestString(context, APP_IDENTIFIER_KEY) 37 | if (TextUtils.isEmpty(appID)) { 38 | throw RuntimeException("AppCenter app identifier was not configured correctly in manifest or build configuration.") 39 | } 40 | return appID 41 | } 42 | 43 | private fun getManifestString(context: Context, key: String): String? { 44 | return getManifestBundle(context).getString(key) 45 | } 46 | 47 | private fun getManifestBundle(context: Context): Bundle { 48 | return try { 49 | context.packageManager.getApplicationInfo( 50 | context.packageName, 51 | PackageManager.GET_META_DATA 52 | ).metaData 53 | } catch (e: PackageManager.NameNotFoundException) { 54 | throw RuntimeException(e) 55 | } 56 | } 57 | 58 | } 59 | 60 | const val APP_IDENTIFIER_KEY = "appcenter.app_identifier" 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/AppIconLoader.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.content.pm.PackageManager 4 | import com.tomclaw.imageloader.core.Loader 5 | import java.io.File 6 | import java.io.FileOutputStream 7 | import java.net.URI 8 | 9 | class AppIconLoader(private val packageManager: PackageManager) : Loader { 10 | 11 | override val schemes: List 12 | get() = listOf("app") 13 | 14 | override fun load(uriString: String, file: File): Boolean { 15 | try { 16 | val packageName = parseUri(uriString) 17 | val packageInfo = packageManager.getPackageInfo(packageName, 0) 18 | val data = getPackageIconPng( 19 | packageInfo.applicationInfo, packageManager 20 | ) 21 | FileOutputStream(file).use { output -> 22 | output.write(data) 23 | output.flush() 24 | } 25 | return true 26 | } catch (ignored: Throwable) { 27 | } 28 | return false 29 | } 30 | 31 | private fun parseUri(s: String?): String { 32 | val uri = URI.create(s) 33 | return uri.authority 34 | } 35 | 36 | } 37 | 38 | fun createAppIconURI(packageName: String): String { 39 | return "app://$packageName" 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Arrays.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.os.Parcel 4 | import java.io.ByteArrayOutputStream 5 | import java.util.zip.Deflater 6 | import java.util.zip.Inflater 7 | 8 | fun ByteArray.unmarshallToParcel(): Parcel { 9 | val parcel = Parcel.obtain() 10 | parcel.unmarshall(this, 0, size) 11 | parcel.setDataPosition(0) 12 | return parcel 13 | } 14 | 15 | fun ByteArray.zip(): ByteArray { 16 | val deflater = Deflater() 17 | deflater.setInput(this) 18 | 19 | val estimatedCompressedSize = (size / 2) 20 | val output = ByteArrayOutputStream(estimatedCompressedSize) 21 | deflater.finish() 22 | val buffer = ByteArray(BUFFER_SIZE) 23 | while (!deflater.finished()) { 24 | val count = deflater.deflate(buffer) 25 | output.write(buffer, 0, count) 26 | } 27 | output.close() 28 | deflater.end() 29 | return output.toByteArray() 30 | } 31 | 32 | fun ByteArray.unzip(): ByteArray { 33 | val inflater = Inflater() 34 | inflater.setInput(this) 35 | 36 | val output = ByteArrayOutputStream(size) 37 | val buffer = ByteArray(BUFFER_SIZE) 38 | while (!inflater.finished()) { 39 | val count = inflater.inflate(buffer) 40 | output.write(buffer, 0, count) 41 | } 42 | output.close() 43 | inflater.end() 44 | return output.toByteArray() 45 | } 46 | 47 | private const val BUFFER_SIZE = 1024 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Colors.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.util.TypedValue 6 | 7 | fun getAttributedColor(context: Context, attr: Int): Int { 8 | val set = intArrayOf(attr) 9 | val typedValue = TypedValue() 10 | val a = context.obtainStyledAttributes(typedValue.data, set) 11 | val color = a.getColor(0, Color.WHITE) 12 | a.recycle() 13 | return color 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Files.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.content.res.Resources 4 | import android.os.Environment 5 | import com.tomclaw.appsend_rb.R 6 | import com.tomclaw.appsend_rb.dto.AppEntity 7 | import java.io.File 8 | 9 | fun getApkPrefix(item: AppEntity): String { 10 | return escapeFileSymbols(item.packageName + "_" + item.versionName) 11 | } 12 | 13 | fun getApkSuffix(): String { 14 | return ".apk" 15 | } 16 | 17 | fun getApkName(item: AppEntity): String { 18 | return getApkPrefix(item) + getApkSuffix() 19 | } 20 | 21 | fun escapeFileSymbols(name: String): String { 22 | var escaped = name 23 | for (symbol in RESERVED_CHARS) { 24 | escaped = escaped.replace(symbol[0], '_') 25 | } 26 | return escaped 27 | } 28 | 29 | fun formatBytesToString(resources: Resources, bytes: Long): String { 30 | return when { 31 | bytes < 1024 -> { 32 | resources.getString(R.string.bytes, bytes) 33 | } 34 | bytes < 1024 * 1024 -> { 35 | resources.getString(R.string.kibibytes, bytes / 1024.0f) 36 | } 37 | bytes < 1024 * 1024 * 1024 -> { 38 | resources.getString(R.string.mibibytes, bytes / 1024.0f / 1024.0f) 39 | } 40 | else -> { 41 | resources.getString(R.string.gigibytes, bytes / 1024.0f / 1024.0f / 1024.0f) 42 | } 43 | } 44 | } 45 | 46 | @Suppress("DEPRECATION") 47 | fun getExternalDirectory(): File { 48 | val externalDirectory = 49 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) 50 | val directory = File(externalDirectory, "Apps") 51 | directory.mkdirs() 52 | return directory 53 | } 54 | 55 | val RESERVED_CHARS = arrayOf("|", "\\", "?", "*", "<", "\"", ":", ">") 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Intents.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.net.Uri 7 | import android.os.Build 8 | 9 | fun grantProviderUriPermission(context: Context, uri: Uri, intent: Intent) { 10 | if (isFileProviderUri()) { 11 | val resInfoList = 12 | context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) 13 | for (resolveInfo in resInfoList) { 14 | val packageName = resolveInfo.activityInfo.packageName 15 | context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) 16 | } 17 | } 18 | } 19 | 20 | fun isFileProviderUri(): Boolean { 21 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Metrics.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.text.TextUtils 8 | import com.microsoft.appcenter.AppCenter 9 | import com.microsoft.appcenter.analytics.Analytics 10 | import com.microsoft.appcenter.crashes.Crashes 11 | 12 | fun registerAppCenter(application: Application) { 13 | getAppIdentifier(application)?.let { appIdentifier -> 14 | AppCenter.start(application, appIdentifier, Analytics::class.java, Crashes::class.java) 15 | } 16 | } 17 | 18 | fun getAppIdentifier(context: Context): String? { 19 | val appIdentifier = getManifestString(context, APP_IDENTIFIER_KEY) 20 | require(!TextUtils.isEmpty(appIdentifier)) { "AppCenter app identifier was not configured correctly in manifest or build configuration." } 21 | return appIdentifier 22 | } 23 | 24 | fun getManifestString(context: Context, key: String?): String? { 25 | return getBundle(context).getString(key) 26 | } 27 | 28 | private fun getBundle(context: Context): Bundle { 29 | return try { 30 | context.packageManager.getApplicationInfo( 31 | context.packageName, 32 | PackageManager.GET_META_DATA 33 | ).metaData 34 | } catch (e: PackageManager.NameNotFoundException) { 35 | throw RuntimeException(e) 36 | } 37 | } 38 | 39 | private const val APP_IDENTIFIER_KEY = "com.microsoft.appcenter.android.appIdentifier" 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/PackageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.content.pm.PackageManager 5 | import android.graphics.Bitmap 6 | import android.graphics.Canvas 7 | import android.graphics.drawable.BitmapDrawable 8 | import android.graphics.drawable.Drawable 9 | import java.io.ByteArrayOutputStream 10 | import java.io.IOException 11 | 12 | 13 | fun getPackageIconPng( 14 | info: ApplicationInfo, 15 | packageManager: PackageManager? 16 | ): ByteArray { 17 | val icon = info.loadIcon(packageManager) 18 | val bitmap = drawableToBitmap(icon) 19 | val baos = ByteArrayOutputStream() 20 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) 21 | val data = baos.toByteArray() 22 | try { 23 | baos.close() 24 | } catch (ignored: IOException) { 25 | } 26 | return data 27 | } 28 | 29 | private fun drawableToBitmap(drawable: Drawable): Bitmap { 30 | if (drawable is BitmapDrawable) { 31 | if (drawable.bitmap != null) { 32 | return drawable.bitmap 33 | } 34 | } 35 | 36 | val bitmap = if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) { 37 | Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) 38 | } else { 39 | Bitmap.createBitmap( 40 | drawable.intrinsicWidth, 41 | drawable.intrinsicHeight, 42 | Bitmap.Config.ARGB_8888 43 | ) 44 | } 45 | 46 | val canvas = Canvas(bitmap) 47 | drawable.setBounds(0, 0, canvas.width, canvas.height) 48 | drawable.draw(canvas) 49 | return bitmap 50 | } 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Parcels.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | object Parcels { 7 | @JvmStatic 8 | fun creator(body: Parcel.() -> T) = object : Parcelable.Creator { 9 | override fun createFromParcel(source: Parcel) = body(source) 10 | 11 | override fun newArray(size: Int) = arrayOfNulls(size) as Array 12 | } 13 | 14 | @JvmStatic 15 | fun Parcel.writeBool(value: Boolean) = writeInt(if (value) 1 else 0) 16 | 17 | @JvmStatic 18 | fun Parcel.readBool(): Boolean = readInt() == 1 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/PerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class PerActivity -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/PreferenceHelper.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.tomclaw.appsend_rb.R; 7 | 8 | /** 9 | * Created with IntelliJ IDEA. 10 | * User: solkin 11 | * Date: 11/15/13 12 | * Time: 1:56 PM 13 | */ 14 | public class PreferenceHelper { 15 | 16 | public static boolean isDarkTheme(Context context) { 17 | return getBooleanPreference(context, R.string.pref_dark_theme, R.bool.pref_dark_theme_default); 18 | } 19 | 20 | private static boolean getBooleanPreference(Context context, int preferenceKey, int defaultValueKey) { 21 | return getSharedPreferences(context).getBoolean(context.getResources().getString(preferenceKey), 22 | context.getResources().getBoolean(defaultValueKey)); 23 | } 24 | 25 | private static SharedPreferences getSharedPreferences(Context context) { 26 | return context.getSharedPreferences(getDefaultSharedPreferencesName(context), 27 | getSharedPreferencesMode()); 28 | } 29 | 30 | private static String getDefaultSharedPreferencesName(Context context) { 31 | return context.getPackageName() + "_preferences"; 32 | } 33 | 34 | private static int getSharedPreferencesMode() { 35 | return Context.MODE_PRIVATE; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Preferences.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.content.Context 4 | import android.content.Context.MODE_PRIVATE 5 | import android.content.SharedPreferences 6 | import androidx.annotation.BoolRes 7 | import androidx.annotation.StringRes 8 | 9 | fun Context.getBooleanPreference( 10 | @StringRes preferenceKey: Int, 11 | @BoolRes defaultValueKey: Int 12 | ): Boolean { 13 | return getSharedPreferences().getBoolean( 14 | resources.getString(preferenceKey), 15 | resources.getBoolean(defaultValueKey) 16 | ) 17 | } 18 | 19 | fun Context.setBooleanPreference(@StringRes preferenceKey: Int, value: Boolean) { 20 | getSharedPreferences() 21 | .edit() 22 | .putBoolean(resources.getString(preferenceKey), value) 23 | .apply() 24 | } 25 | 26 | fun Context.getStringPreference( 27 | @StringRes preferenceKey: Int, 28 | @StringRes defaultValueKey: Int 29 | ): String? { 30 | return getSharedPreferences().getString( 31 | resources.getString(preferenceKey), 32 | resources.getString(defaultValueKey) 33 | ) 34 | } 35 | 36 | fun Context.setStringPreference(@StringRes preferenceKey: Int, value: String) { 37 | getSharedPreferences() 38 | .edit() 39 | .putString(resources.getString(preferenceKey), value) 40 | .apply() 41 | } 42 | 43 | fun Context.getSharedPreferences(): SharedPreferences { 44 | return getSharedPreferences(getDefaultSharedPreferencesName(this), MODE_PRIVATE) 45 | } 46 | 47 | private fun getDefaultSharedPreferencesName(context: Context): String? { 48 | return context.packageName + "_preferences" 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/SchedulersFactory.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 4 | import io.reactivex.rxjava3.core.Scheduler 5 | import io.reactivex.rxjava3.schedulers.Schedulers 6 | 7 | 8 | interface SchedulersFactory { 9 | 10 | fun io(): Scheduler 11 | 12 | fun single(): Scheduler 13 | 14 | fun mainThread(): Scheduler 15 | 16 | } 17 | 18 | class SchedulersFactoryImpl : SchedulersFactory { 19 | 20 | override fun io(): Scheduler { 21 | return Schedulers.io() 22 | } 23 | 24 | override fun single(): Scheduler { 25 | return Schedulers.single() 26 | } 27 | 28 | override fun mainThread(): Scheduler { 29 | return AndroidSchedulers.mainThread() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Streams.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import java.io.Closeable 4 | import java.io.IOException 5 | 6 | fun Closeable?.safeClose() { 7 | try { 8 | this?.close() 9 | } catch (ignored: IOException) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/ThemeHelper.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.app.Activity 4 | import com.jaeger.library.StatusBarUtil 5 | import com.tomclaw.appsend_rb.R 6 | 7 | fun updateTheme(activity: Activity): Boolean { 8 | val isDarkTheme = PreferenceHelper.isDarkTheme(activity) 9 | activity.setTheme(if (isDarkTheme) R.style.AppThemeBlack else R.style.AppTheme) 10 | return isDarkTheme 11 | } 12 | 13 | fun updateStatusBar(activity: Activity) { 14 | val color = getAttributedColor(activity, R.attr.toolbar_background) 15 | StatusBarUtil.setColor(activity, color) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/Views.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.text.Editable 6 | import android.text.TextUtils 7 | import android.text.TextWatcher 8 | import android.view.View 9 | import android.view.View.GONE 10 | import android.view.View.VISIBLE 11 | import android.view.ViewPropertyAnimator 12 | import android.view.animation.AccelerateDecelerateInterpolator 13 | import android.widget.EditText 14 | import android.widget.TextView 15 | import com.jakewharton.rxrelay3.Relay 16 | 17 | fun View?.toggle() { 18 | if (this?.visibility == VISIBLE) hide() else show() 19 | } 20 | 21 | fun View?.isVisible(): Boolean = this?.visibility == VISIBLE 22 | 23 | fun View?.show() { 24 | this?.visibility = VISIBLE 25 | } 26 | 27 | fun View?.hide() { 28 | this?.visibility = GONE 29 | } 30 | 31 | fun TextView.bind(value: String?) { 32 | if (TextUtils.isEmpty(value)) { 33 | visibility = GONE 34 | text = "" 35 | } else { 36 | visibility = VISIBLE 37 | text = value 38 | } 39 | } 40 | 41 | fun View.clicks(relay: Relay) { 42 | setOnClickListener { relay.accept(Unit) } 43 | } 44 | 45 | fun EditText.changes(handler: (String) -> Unit) { 46 | addTextChangedListener(object : TextWatcher { 47 | override fun afterTextChanged(s: Editable) {} 48 | 49 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} 50 | 51 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { 52 | handler.invoke(s.toString()) 53 | } 54 | }) 55 | } 56 | 57 | fun View.showWithAlphaAnimation( 58 | duration: Long = ANIMATION_DURATION, 59 | animateFully: Boolean = true, 60 | endCallback: (() -> Unit)? = null 61 | ): ViewPropertyAnimator { 62 | if (animateFully) { 63 | alpha = 0.0f 64 | } 65 | show() 66 | return animate() 67 | .setDuration(duration) 68 | .alpha(1.0f) 69 | .setInterpolator(AccelerateDecelerateInterpolator()) 70 | .setListener(object : AnimatorListenerAdapter() { 71 | override fun onAnimationEnd(animation: Animator) { 72 | alpha = 1.0f 73 | show() 74 | endCallback?.invoke() 75 | } 76 | }) 77 | } 78 | 79 | fun View.hideWithAlphaAnimation( 80 | duration: Long = ANIMATION_DURATION, 81 | animateFully: Boolean = true, 82 | endCallback: (() -> Unit)? = null 83 | ): ViewPropertyAnimator { 84 | if (animateFully) { 85 | alpha = 1.0f 86 | } 87 | return animate() 88 | .setDuration(duration) 89 | .alpha(0.0f) 90 | .setInterpolator(AccelerateDecelerateInterpolator()) 91 | .setListener(object : AnimatorListenerAdapter() { 92 | override fun onAnimationEnd(animation: Animator) { 93 | hide() 94 | alpha = 1.0f 95 | endCallback?.invoke() 96 | } 97 | }) 98 | } 99 | 100 | const val ANIMATION_DURATION: Long = 250 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/appsend_rb/util/ZipParcelable.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.appsend_rb.util 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import com.tomclaw.appsend_rb.util.Parcels.readBool 6 | import com.tomclaw.appsend_rb.util.Parcels.writeBool 7 | 8 | class ZipParcelable : Parcelable { 9 | 10 | private var raw: ByteArray? = null 11 | private var zip: Boolean? = null 12 | private var nested: Parcelable? = null 13 | 14 | constructor(nested: Parcelable?) { 15 | this.nested = nested 16 | } 17 | 18 | private constructor(raw: ByteArray, zip: Boolean) { 19 | this.raw = raw 20 | this.zip = zip 21 | } 22 | 23 | private fun writeNestedParcel(nested: Parcelable, parcel: Parcel, flags: Int) { 24 | parcel.writeParcelable(nested, flags) 25 | } 26 | 27 | private fun

readNestedParcel(clazz: Class

, parcel: Parcel): Parcelable? { 28 | @Suppress("DEPRECATION") 29 | return parcel.readParcelable(clazz.classLoader) 30 | } 31 | 32 | inline fun restore() = restore(T::class.java) as? T 33 | 34 | fun

restore(clazz: Class

): Parcelable? { 35 | if (nested != null) return nested as Parcelable 36 | 37 | val data = raw?.takeIf { it.isNotEmpty() } ?: return null 38 | 39 | val array = when (zip ?: false) { 40 | true -> try { 41 | data.unzip() 42 | } catch (throwable: Throwable) { 43 | return null 44 | } 45 | 46 | else -> data 47 | } 48 | return readNestedParcel(clazz, array.unmarshallToParcel()) 49 | } 50 | 51 | override fun writeToParcel(out: Parcel, flags: Int) = with(out) { 52 | when (val nested = nested) { 53 | null -> writeBool(false) 54 | else -> { 55 | writeBool(value = true) 56 | val originalArray = parcelableToByteArray { writeNestedParcel(nested, it, flags) } 57 | try { 58 | val zip = originalArray.zip() 59 | writeBool(value = true) 60 | writeByteArrayWithSize(zip) 61 | } catch (throwable: Throwable) { 62 | writeBool(value = false) 63 | writeByteArrayWithSize(originalArray) 64 | } 65 | } 66 | } 67 | } 68 | 69 | private fun Parcel.writeByteArrayWithSize(array: ByteArray) { 70 | writeInt(array.size) 71 | writeByteArray(array) 72 | } 73 | 74 | override fun describeContents() = 0 75 | 76 | companion object { 77 | 78 | @JvmField 79 | val CREATOR = Parcels.creator { 80 | create(this) { raw, zip -> 81 | ZipParcelable(raw, zip) 82 | } 83 | } 84 | 85 | @JvmStatic 86 | fun create( 87 | parcel: Parcel, 88 | creator: (ByteArray, Boolean) -> ZipParcelable 89 | ): ZipParcelable = with(parcel) { 90 | var zip = false 91 | val data = when (readBool()) { 92 | true -> { 93 | zip = readBool() 94 | val size = readInt() 95 | ByteArray(size).apply { 96 | readByteArray(this) 97 | } 98 | } 99 | 100 | else -> ByteArray(size = 0) 101 | } 102 | return creator(data, zip) 103 | } 104 | } 105 | } 106 | 107 | fun parcelableToByteArray(writer: (Parcel) -> Unit): ByteArray { 108 | val parcel = Parcel.obtain() 109 | writer(parcel) 110 | val bytes = parcel.marshall() 111 | parcel.recycle() 112 | return bytes 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/app_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solkin/appsend-android/23b283bf86062537879addbb7cdc3acfbd9d14ac/app/src/main/res/drawable-nodpi/app_placeholder.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_logo_ab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solkin/appsend-android/23b283bf86062537879addbb7cdc3acfbd9d14ac/app/src/main/res/drawable-xxhdpi/ic_logo_ab.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_logo_ab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solkin/appsend-android/23b283bf86062537879addbb7cdc3acfbd9d14ac/app/src/main/res/drawable-xxxhdpi/ic_logo_ab.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/floppy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/google_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 14 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/lock_open.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/magnify.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/refresh.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/run.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/settings_box.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/about_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 13 | 14 | 20 | 21 | 26 | 27 | 34 | 35 | 41 | 42 | 49 | 50 | 51 | 52 | 57 | 58 |