diff --git a/.eslintrc.js b/.eslintrc.js index 2dbffb7..f78a4ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { root: true, - extends: ['@react-native-community', 'prettier'], + extends: ['@react-native', 'prettier'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], overrides: [ diff --git a/.gitignore b/.gitignore index b99afd1..9d03b55 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ coverage/ # vscode .vscode/ + +# testing +/coverage diff --git a/.node-version b/.node-version deleted file mode 100644 index 3c03207..0000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -18 diff --git a/Gemfile b/Gemfile index 1142b1b..8d72c37 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,9 @@ source 'https://rubygems.org' # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version -ruby '>= 2.6.10' +ruby ">= 2.6.10" -gem 'cocoapods', '>= 1.11.3' +# Cocoapods 1.15 introduced a bug which break the build. We will remove the upper +# bound in the template on Cocoapods with next React Native release. +gem 'cocoapods', '>= 1.13', '< 1.15' +gem 'activesupport', '>= 6.1.7.5', '< 7.1.0' diff --git a/README.md b/README.md index 405fa5b..4e69648 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ cd android && ./gradlew build Install and launch app in an android device/emulator: ```shell -npm run android +npm run start:all ``` ## Build diff --git a/android/app/build.gradle b/android/app/build.gradle index 22f3e40..9851d3e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,8 +1,7 @@ apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" -import com.android.build.OutputFile - /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. @@ -13,15 +12,17 @@ react { // root = file("../") // The folder where the react-native NPM package is. Default is ../node_modules/react-native // reactNativeDir = file("../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen - // codegenDir = file("../node_modules/react-native-codegen") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + // codegenDir = file("../node_modules/@react-native/codegen") // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js // cliFile = file("../node_modules/react-native/cli.js") + /* Variants */ // The list of variants to that are debuggable. For those we're going to // skip the bundling of the JS bundle and the assets. By default is just 'debug'. // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. // debuggableVariants = ["liteDebug", "prodDebug"] + /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. // nodeExecutableAndArgs = ["node"] @@ -41,6 +42,7 @@ react { // A list of extra flags to pass to the 'bundle' commands. // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle // extraPackagerArgs = [] + /* Hermes Commands */ // The hermes compiler command to run. By default it is 'hermesc' // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" @@ -49,14 +51,6 @@ react { // hermesFlags = ["-O", "-output-source-map"] } -/** - * Set this to true to create four separate APKs instead of one, - * one for each native architecture. This is useful if you don't - * use App Bundles (https://developer.android.com/guide/app-bundle/) - * and want to have separate APKs to upload to the Play Store. - */ -def enableSeparateBuildPerCPUArchitecture = false - /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ @@ -75,49 +69,34 @@ def enableProguardInReleaseBuilds = true */ def jscFlavor = 'org.webkit:android-jsc:+' -/** - * Private function to get the list of Native Architectures you want to build. - * This reads the value from reactNativeArchitectures in your gradle.properties - * file and works together with the --active-arch-only flag of react-native run-android. - */ -def reactNativeArchitectures() { - def value = project.getProperties().get("reactNativeArchitectures") - return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] -} - android { + kotlinOptions { + jvmTarget = '17' + } ndkVersion rootProject.ext.ndkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion - compileSdkVersion rootProject.ext.compileSdkVersion namespace "com.razinj.context_launcher" - defaultConfig { applicationId "com.razinj.context_launcher" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 16 - versionName "2.0.2" + versionCode 17 + versionName "2.0.3" archivesBaseName = "context-launcher-v$versionName-$versionCode" } - splits { - abi { - reset() - enable enableSeparateBuildPerCPUArchitecture - universalApk false // If true, also generate a universal APK - include(*reactNativeArchitectures()) - } - } signingConfigs { release { if (project.hasProperty('CONTEXT_LAUNCHER_UPLOAD_STORE_FILE')) { - project.logger.lifecycle('Release upload keystore file found.') + project.logger.lifecycle('[!] Release upload keystore file found.') storeFile file(CONTEXT_LAUNCHER_UPLOAD_STORE_FILE) storePassword CONTEXT_LAUNCHER_UPLOAD_STORE_PASSWORD keyAlias CONTEXT_LAUNCHER_UPLOAD_KEY_ALIAS keyPassword CONTEXT_LAUNCHER_UPLOAD_KEY_PASSWORD } else { - project.logger.lifecycle('Release upload keystore file not found.') + project.logger.lifecycle('[X] Release upload keystore file not found.') } } debug { @@ -139,33 +118,13 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } - - // applicationVariants are e.g. debug, release - applicationVariants.all { variant -> - variant.outputs.each { output -> - // For each separate APK per architecture, set a unique version code as described here: - // https://developer.android.com/studio/build/configure-apk-splits.html - // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. - def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] - def abi = output.getFilter(OutputFile.ABI) - if (abi != null) { // null for the universal-debug, universal-release variants - output.versionCodeOverride = - defaultConfig.versionCode * 1000 + versionCodes.get(abi) - } - - } - } } dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") - debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") - debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { - exclude group: 'com.squareup.okhttp3', module: 'okhttp' - } - debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") + implementation 'androidx.core:core-ktx:1.13.1' + if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") } else { @@ -179,4 +138,26 @@ project.ext.vectoricons = [ iconFontNames: ['MaterialCommunityIcons.ttf'] ] -apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" +apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") + +/* +Below code is a fix for this issue: + +* What went wrong: +A problem was found with the configuration of task ':app:lintAnalyzeDebug' (type 'AndroidLintAnalysisTask'). + - Gradle detected a problem with the following location: '/Users/nizar/dev/context_launcher/android/app/build/intermediates/ReactNativeVectorIcons'. + + Reason: Task ':app:lintAnalyzeDebug' uses this output of task ':app:copyReactNativeVectorIconFonts' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. + + Possible solutions: + 1. Declare task ':app:copyReactNativeVectorIconFonts' as an input of ':app:lintAnalyzeDebug'. + 2. Declare an explicit dependency on ':app:copyReactNativeVectorIconFonts' from ':app:lintAnalyzeDebug' using Task#dependsOn. + 3. Declare an explicit dependency on ':app:copyReactNativeVectorIconFonts' from ':app:lintAnalyzeDebug' using Task#mustRunAfter. + +Inspired from: https://github.com/oblador/react-native-vector-icons/issues/1584 +*/ +tasks.configureEach { + if (name == 'lintAnalyzeDebug') { + dependsOn('copyReactNativeVectorIconFonts') + } +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 4b185bc..eb98c01 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -2,12 +2,8 @@ - - - - + tools:ignore="GoogleAppIndexingWarning"/> diff --git a/android/app/src/debug/java/com/razinj/context_launcher/ReactNativeFlipper.java b/android/app/src/debug/java/com/razinj/context_launcher/ReactNativeFlipper.java deleted file mode 100644 index 0b506cc..0000000 --- a/android/app/src/debug/java/com/razinj/context_launcher/ReactNativeFlipper.java +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - *

This source code is licensed under the MIT license found in the LICENSE file in the root - * directory of this source tree. - */ -package com.razinj.context_launcher; - -import android.content.Context; - -import com.facebook.flipper.android.AndroidFlipperClient; -import com.facebook.flipper.android.utils.FlipperUtils; -import com.facebook.flipper.core.FlipperClient; -import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; -import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; -import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; -import com.facebook.flipper.plugins.inspector.DescriptorMapping; -import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; -import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; -import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; -import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; -import com.facebook.react.ReactInstanceEventListener; -import com.facebook.react.ReactInstanceManager; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.modules.network.NetworkingModule; - -/** - * Class responsible of loading Flipper inside your React Native application. This is the debug - * flavor of it. Here you can add your own plugins and customize the Flipper setup. - */ -public class ReactNativeFlipper { - - public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { - if (FlipperUtils.shouldEnableFlipper(context)) { - final FlipperClient client = AndroidFlipperClient.getInstance(context); - - client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); - client.addPlugin(new DatabasesFlipperPlugin(context)); - client.addPlugin(new SharedPreferencesFlipperPlugin(context)); - client.addPlugin(CrashReporterPlugin.getInstance()); - - NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); - NetworkingModule.setCustomClientBuilder(builder -> builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin))); - client.addPlugin(networkFlipperPlugin); - client.start(); - - // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized - // Hence we run if after all native modules have been initialized - ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); - if (reactContext == null) { - reactInstanceManager.addReactInstanceEventListener( - new ReactInstanceEventListener() { - @Override - public void onReactContextInitialized(ReactContext reactContext) { - reactInstanceManager.removeReactInstanceEventListener(this); - reactContext.runOnNativeModulesQueueThread(() -> client.addPlugin(new FrescoFlipperPlugin())); - } - }); - } else { - client.addPlugin(new FrescoFlipperPlugin()); - } - } - } -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 884b413..a2a81b6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + - + + + diff --git a/android/app/src/main/java/com/razinj/context_launcher/AppDetails.java b/android/app/src/main/java/com/razinj/context_launcher/AppDetails.java deleted file mode 100644 index 66ce9f8..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/AppDetails.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.razinj.context_launcher; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.json.JSONException; -import org.json.JSONObject; - -public class AppDetails { - String packageName; - String name; - String icon; - - AppDetails(String packageName, String name, String icon) { - this.packageName = packageName; - this.name = name; - this.icon = icon; - } - - @NonNull - public String toString() { - try { - JSONObject appDetails = new JSONObject(); - - appDetails.put("packageName", this.packageName); - appDetails.put("name", this.name); - appDetails.put("icon", this.icon); - - return appDetails.toString(); - } catch (JSONException e) { - Log.e("AppsModule", "Couldn't construct app details JSON: " + e.getMessage()); - throw new RuntimeException(e); - } - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/AppDetails.kt b/android/app/src/main/java/com/razinj/context_launcher/AppDetails.kt new file mode 100644 index 0000000..fa773cf --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/AppDetails.kt @@ -0,0 +1,26 @@ +package com.razinj.context_launcher + +import android.util.Log +import org.json.JSONException +import org.json.JSONObject + +class AppDetails internal constructor( + private var packageName: String, + private var name: String, + private var icon: String?, +) { + override fun toString(): String { + try { + val appDetails = JSONObject() + + appDetails.put("packageName", this.packageName) + appDetails.put("name", this.name) + appDetails.put("icon", this.icon) + + return appDetails.toString() + } catch (e: JSONException) { + Log.e("AppsModule", "Couldn't construct app details JSON: " + e.message) + throw RuntimeException(e) + } + } +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/AppProvider.java b/android/app/src/main/java/com/razinj/context_launcher/AppProvider.java deleted file mode 100644 index d492cf8..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/AppProvider.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.razinj.context_launcher; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.LauncherApps; -import android.os.Build; -import android.os.IBinder; -import android.os.Process; -import android.os.UserHandle; - -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; - -public class AppProvider extends Service { - private PackageChangeReceiver packageChangeReceiver; - - @Override - public void onCreate() { - final LauncherApps launcherApps = (LauncherApps) getSystemService(Context.LAUNCHER_APPS_SERVICE); - assert launcherApps != null; - - launcherApps.registerCallback(new LauncherAppsCallback() { - @Override - public void onPackageAdded(String packageName, UserHandle user) { - if (user.equals(Process.myUserHandle())) return; - - PackageChangeReceiver.handleEvent(AppProvider.this, Intent.ACTION_PACKAGE_ADDED, packageName, false); - } - - @Override - public void onPackageChanged(String packageName, UserHandle user) { - if (user.equals(Process.myUserHandle())) return; - - PackageChangeReceiver.handleEvent(AppProvider.this, Intent.ACTION_PACKAGE_CHANGED, packageName, true); - } - - @Override - public void onPackageRemoved(String packageName, UserHandle user) { - if (user.equals(Process.myUserHandle())) return; - - PackageChangeReceiver.handleEvent(AppProvider.this, Intent.ACTION_PACKAGE_REMOVED, packageName, false); - } - }); - - this.packageChangeReceiver = new PackageChangeReceiver(); - - IntentFilter appChangedIntentFilter = new IntentFilter(); - appChangedIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - appChangedIntentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); - appChangedIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); - appChangedIntentFilter.addDataScheme("package"); - appChangedIntentFilter.addDataScheme("file"); - - registerReceiver(packageChangeReceiver, appChangedIntentFilter); - - super.onCreate(); - - startForegroundCustom(); - } - - private void startForegroundCustom() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - startForeground(1, new Notification()); - return; - } - - // Notification channel - String channelId = BuildConfig.APPLICATION_ID; - String channelName = "AppProvider Channel"; - NotificationChannel notificationChannel = new NotificationChannel( - channelId, - channelName, - NotificationManager.IMPORTANCE_NONE - ); - notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); - - // Notification manager - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - assert notificationManager != null; - notificationManager.createNotificationChannel(notificationChannel); - - // Notification builder - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, channelId); - Notification notification = notificationBuilder - .setPriority(NotificationManager.IMPORTANCE_NONE) - .setCategory(Notification.CATEGORY_SERVICE) - .setAutoCancel(false) - .setOngoing(true) - .setSilent(true) - .build(); - startForeground(1, notification); - } - - @Override - public void onDestroy() { - this.unregisterReceiver(packageChangeReceiver); - super.onDestroy(); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/AppProvider.kt b/android/app/src/main/java/com/razinj/context_launcher/AppProvider.kt new file mode 100644 index 0000000..3d9a9a0 --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/AppProvider.kt @@ -0,0 +1,121 @@ +package com.razinj.context_launcher + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.LauncherApps +import android.os.Build +import android.os.IBinder +import android.os.Process +import android.os.UserHandle +import androidx.core.app.NotificationCompat + +class AppProvider : Service() { + private var packageChangeReceiver: PackageChangeReceiver? = null + + override fun onCreate() { + val launcherApps = (getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps) + launcherApps.registerCallback( + object : LauncherAppsCallback() { + override fun onPackageAdded( + packageName: String, + user: UserHandle, + ) { + if (user == Process.myUserHandle()) return + + PackageChangeReceiver.handleEvent( + this@AppProvider, + Intent.ACTION_PACKAGE_ADDED, + packageName, + ) + } + + override fun onPackageChanged( + packageName: String, + user: UserHandle, + ) { + if (user == Process.myUserHandle()) return + + PackageChangeReceiver.handleEvent( + this@AppProvider, + Intent.ACTION_PACKAGE_CHANGED, + packageName, + ) + } + + override fun onPackageRemoved( + packageName: String, + user: UserHandle, + ) { + if (user == Process.myUserHandle()) return + + PackageChangeReceiver.handleEvent( + this@AppProvider, + Intent.ACTION_PACKAGE_REMOVED, + packageName, + ) + } + }, + ) + + this.packageChangeReceiver = PackageChangeReceiver() + + val appChangedIntentFilter = IntentFilter() + appChangedIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED) + appChangedIntentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED) + appChangedIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED) + appChangedIntentFilter.addDataScheme("package") + appChangedIntentFilter.addDataScheme("file") + + registerReceiver(packageChangeReceiver, appChangedIntentFilter) + + super.onCreate() + + startForegroundCustom() + } + + private fun startForegroundCustom() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + startForeground(1, Notification()) + return + } + + // Notification channel + val channelId = BuildConfig.APPLICATION_ID + val channelName = "AppProvider Channel" + val notificationChannel = + NotificationChannel( + channelId, + channelName, + NotificationManager.IMPORTANCE_NONE, + ) + notificationChannel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + + // Notification manager + val notificationManager = (getSystemService(NOTIFICATION_SERVICE) as NotificationManager) + notificationManager.createNotificationChannel(notificationChannel) + + // Notification builder + val notificationBuilder = NotificationCompat.Builder(this, channelId) + val notification = + notificationBuilder + .setPriority(NotificationManager.IMPORTANCE_NONE) + .setCategory(Notification.CATEGORY_SERVICE) + .setAutoCancel(false) + .setOngoing(true) + .setSilent(true) + .build() + + startForeground(1, notification) + } + + override fun onDestroy() { + this.unregisterReceiver(packageChangeReceiver) + super.onDestroy() + } + + override fun onBind(intent: Intent): IBinder? = null +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/AppsModule.java b/android/app/src/main/java/com/razinj/context_launcher/AppsModule.java deleted file mode 100644 index 2ba174a..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/AppsModule.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.razinj.context_launcher; - -import static com.razinj.context_launcher.Constants.PACKAGE_CHANGE_EVENT; -import static com.razinj.context_launcher.Constants.PACKAGE_CHANGE_IS_REMOVED; -import static com.razinj.context_launcher.Constants.PACKAGE_CHANGE_NAME; -import static com.razinj.context_launcher.Constants.PACKAGE_UPDATE_ACTION; -import static com.razinj.context_launcher.Constants.SHORT_NOT_AVAILABLE; -import static com.razinj.context_launcher.Utils.getPackageInfo; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; - -import androidx.annotation.NonNull; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -public class AppsModule extends ReactContextBaseJavaModule { - private final ReactApplicationContext reactContext; - private static BroadcastReceiver packageChangeBroadcastReceiver; - - AppsModule(ReactApplicationContext reactContext) { - super(reactContext); - this.reactContext = reactContext; - initializePackageChangeBroadcastReceiver(); - } - - @NonNull - @Override - public String getName() { - return "AppsModule"; - } - - @Override - public void onCatalystInstanceDestroy() { - reactContext.unregisterReceiver(packageChangeBroadcastReceiver); - } - - private void initializePackageChangeBroadcastReceiver() { - packageChangeBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (!reactContext.hasActiveReactInstance()) return; - - Bundle extras = intent.getExtras(); - WritableMap map = Arguments.createMap(); - - map.putString(PACKAGE_CHANGE_NAME, extras.getString(PACKAGE_CHANGE_NAME)); - map.putBoolean(PACKAGE_CHANGE_IS_REMOVED, extras.getBoolean(PACKAGE_CHANGE_IS_REMOVED)); - - reactContext.getJSModule((DeviceEventManagerModule.RCTDeviceEventEmitter.class)).emit(PACKAGE_CHANGE_EVENT, map); - } - }; - - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(PACKAGE_UPDATE_ACTION); - - reactContext.registerReceiver(packageChangeBroadcastReceiver, intentFilter); - } - - @ReactMethod - public void getApplications(Promise promise) { - new Thread(() -> { - PackageManager pm = reactContext.getPackageManager(); - List apps = new ArrayList<>(); - List packages; - - // Get installed packages - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packages = pm.getInstalledPackages(PackageManager.PackageInfoFlags.of(0)); - } else { - packages = pm.getInstalledPackages(0); - } - - // Filter and map to AppDetails - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - apps = packages.stream().filter(packageInfo -> Objects.nonNull(pm.getLaunchIntentForPackage(packageInfo.packageName))).map(packageInfo -> new AppDetails(packageInfo.packageName, packageInfo.applicationInfo.loadLabel(pm).toString(), Utils.getEncodedIcon(pm, packageInfo.packageName))).collect(Collectors.toList()); - } else { - for (PackageInfo packageInfo : packages) { - if (Objects.isNull(pm.getLaunchIntentForPackage(packageInfo.packageName))) { - continue; - } - - apps.add(new AppDetails(packageInfo.packageName, packageInfo.applicationInfo.loadLabel(pm).toString(), Utils.getEncodedIcon(pm, packageInfo.packageName))); - } - } - - promise.resolve(apps.toString()); - }).start(); - } - - @ReactMethod - private void launchApplication(String packageName) { - Intent intent = reactContext.getPackageManager().getLaunchIntentForPackage(packageName); - - reactContext.startActivity(intent); - } - - @ReactMethod - public void showApplicationDetails(String packageName) { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setData(Uri.fromParts("package", packageName, null)); - - reactContext.startActivity(intent); - } - - @ReactMethod - public void requestApplicationUninstall(String packageName) { - Intent intent = new Intent(Intent.ACTION_DELETE).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setData(Uri.fromParts("package", packageName, null)); - - reactContext.startActivity(intent); - } - - @ReactMethod - public void addListener(String eventName) { - // Required for NativeEventEmitter - } - - @ReactMethod - public void removeListeners(Integer count) { - // Required for NativeEventEmitter - } - - @Override - public Map getConstants() { - String appVersion; - String buildNumber; - String packageName; - - try { - appVersion = getPackageInfo(reactContext).versionName; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - buildNumber = Long.toString(getPackageInfo(reactContext).getLongVersionCode()); - } else { - buildNumber = Long.toString(getPackageInfo(reactContext).versionCode); - } - packageName = getReactApplicationContext().getPackageName(); - } catch (PackageManager.NameNotFoundException e) { - appVersion = SHORT_NOT_AVAILABLE; - buildNumber = SHORT_NOT_AVAILABLE; - packageName = SHORT_NOT_AVAILABLE; - } - - Map constants = new HashMap<>(); - - constants.put("appVersion", appVersion); - constants.put("buildNumber", buildNumber); - constants.put("packageName", packageName); - - return constants; - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/AppsModule.kt b/android/app/src/main/java/com/razinj/context_launcher/AppsModule.kt new file mode 100644 index 0000000..b064151 --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/AppsModule.kt @@ -0,0 +1,177 @@ +package com.razinj.context_launcher + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.modules.core.DeviceEventManagerModule + +class AppsModule internal constructor( + private val reactContext: ReactApplicationContext, +) : ReactContextBaseJavaModule(reactContext) { + private var packageChangeBroadcastReceiver: BroadcastReceiver? = null + + init { + initializePackageChangeBroadcastReceiver() + } + + override fun getName(): String = "AppsModule" + + override fun invalidate() { + reactContext.unregisterReceiver(packageChangeBroadcastReceiver) + } + + private fun initializePackageChangeBroadcastReceiver() { + packageChangeBroadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (!reactContext.hasActiveReactInstance()) return + + val extras = intent.extras ?: return + val map = Arguments.createMap() + + map.putString( + Constants.PACKAGE_CHANGE_NAME, + extras.getString(Constants.PACKAGE_CHANGE_NAME), + ) + map.putBoolean( + Constants.PACKAGE_CHANGE_IS_REMOVED, + extras.getBoolean(Constants.PACKAGE_CHANGE_IS_REMOVED), + ) + + reactContext + .getJSModule((DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)) + .emit( + Constants.PACKAGE_CHANGE_EVENT, + map, + ) + } + } + + val intentFilter = IntentFilter() + intentFilter.addAction(Constants.PACKAGE_UPDATE_ACTION) + + @SuppressLint("UnspecifiedRegisterReceiverFlag") // Suppress lint warning in the else statement + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + reactContext.registerReceiver( + packageChangeBroadcastReceiver, + intentFilter, + Context.RECEIVER_EXPORTED, + ) + } else { + reactContext.registerReceiver(packageChangeBroadcastReceiver, intentFilter) + } + } + + @ReactMethod + fun getApplications(promise: Promise) { + Thread { + val pm = reactContext.packageManager + val apps = mutableListOf() + + val packages = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getInstalledPackages(PackageManager.PackageInfoFlags.of(0)) + } else { + pm.getInstalledPackages(0) + } + + for (packageInfo in packages) { + // Only use packages that are launch-able + pm.getLaunchIntentForPackage(packageInfo.packageName) ?: continue + + apps.add( + AppDetails( + packageInfo.packageName, + packageInfo.applicationInfo.loadLabel(pm).toString(), + Utils.getEncodedIcon(pm, packageInfo.packageName), + ), + ) + } + + promise.resolve(apps.toString()) + }.start() + } + + @ReactMethod + fun launchApplication(packageName: String) { + val intent = reactContext.packageManager.getLaunchIntentForPackage(packageName) + + reactContext.startActivity(intent) + } + + @ReactMethod + fun showApplicationDetails(packageName: String) { + val intent = + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .setData(Uri.fromParts("package", packageName, null)) + + reactContext.startActivity(intent) + } + + @ReactMethod + fun requestApplicationUninstall(packageName: String) { + val intent = + Intent(Intent.ACTION_DELETE) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .setData(Uri.fromParts("package", packageName, null)) + + reactContext.startActivity(intent) + } + + @ReactMethod + fun addListener(eventName: String) { + // Required for NativeEventEmitter + // See: https://github.com/facebook/react-native/commit/114be1d2170bae2d29da749c07b45acf931e51e2 + } + + @ReactMethod + fun removeListeners(count: Int) { + // Required for NativeEventEmitter + // See: https://github.com/facebook/react-native/commit/114be1d2170bae2d29da749c07b45acf931e51e2 + } + + override fun getConstants(): MutableMap { + val constants: MutableMap = mutableMapOf() + + try { + val packageInfo = Utils.getPackageInfo(reactContext) + val appVersion = packageInfo.versionName + val buildNumber = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode.toString() + } else { + packageInfo.versionCode.toString() + } + val packageName = reactApplicationContext.packageName + + constants.apply { + put("appVersion", appVersion ?: Constants.SHORT_NOT_AVAILABLE) + put("buildNumber", buildNumber) + put("packageName", packageName ?: Constants.SHORT_NOT_AVAILABLE) + } + } catch (e: PackageManager.NameNotFoundException) { + constants.apply { + put("appVersion", Constants.SHORT_NOT_AVAILABLE) + put("buildNumber", Constants.SHORT_NOT_AVAILABLE) + put("packageName", Constants.SHORT_NOT_AVAILABLE) + } + } + + return constants + } +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/Constants.java b/android/app/src/main/java/com/razinj/context_launcher/Constants.java deleted file mode 100644 index 36a3dea..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/Constants.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.razinj.context_launcher; - -public class Constants { - - private Constants() { - super(); - } - - // Package change intent action - public static final String PACKAGE_UPDATE_ACTION = "packageUpdateAction"; - // Package change event - public static final String PACKAGE_CHANGE_EVENT = "packageChange"; - public static final String PACKAGE_CHANGE_NAME = "packageName"; - public static final String PACKAGE_CHANGE_IS_REMOVED = "isRemoved"; - // Misc - public static final String SHORT_NOT_AVAILABLE = "N/A"; -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/Constants.kt b/android/app/src/main/java/com/razinj/context_launcher/Constants.kt new file mode 100644 index 0000000..6ced04a --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/Constants.kt @@ -0,0 +1,14 @@ +package com.razinj.context_launcher + +object Constants { + // Package change intent action + const val PACKAGE_UPDATE_ACTION: String = "packageUpdateAction" + + // Package change event + const val PACKAGE_CHANGE_EVENT: String = "packageChange" + const val PACKAGE_CHANGE_NAME: String = "packageName" + const val PACKAGE_CHANGE_IS_REMOVED: String = "isRemoved" + + // Misc + const val SHORT_NOT_AVAILABLE: String = "N/A" +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/CustomAppPackage.java b/android/app/src/main/java/com/razinj/context_launcher/CustomAppPackage.java deleted file mode 100644 index 20d8e5f..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/CustomAppPackage.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.razinj.context_launcher; - -import androidx.annotation.NonNull; - -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class CustomAppPackage implements ReactPackage { - - @NonNull - @Override - public List createViewManagers(@NonNull ReactApplicationContext reactContext) { - return Collections.emptyList(); - } - - @NonNull - @Override - public List createNativeModules(@NonNull ReactApplicationContext reactContext) { - List modules = new ArrayList<>(); - modules.add(new AppsModule(reactContext)); - return modules; - } - -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/CustomAppPackage.kt b/android/app/src/main/java/com/razinj/context_launcher/CustomAppPackage.kt new file mode 100644 index 0000000..c1314ca --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/CustomAppPackage.kt @@ -0,0 +1,16 @@ +package com.razinj.context_launcher + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager + +class CustomAppPackage : ReactPackage { + override fun createViewManagers(reactContext: ReactApplicationContext): MutableList>> = + mutableListOf() + + override fun createNativeModules(reactContext: ReactApplicationContext): MutableList = + listOf(AppsModule(reactContext)).toMutableList() +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/LauncherAppsCallback.java b/android/app/src/main/java/com/razinj/context_launcher/LauncherAppsCallback.java deleted file mode 100644 index 10c9fd8..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/LauncherAppsCallback.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.razinj.context_launcher; - -import android.content.pm.LauncherApps; -import android.os.UserHandle; - -/** - * Empty implementation of LauncherApps.Callback so we do not need to override all methods when - * only parts of LauncherApps.Callback are needed. - */ -public class LauncherAppsCallback extends LauncherApps.Callback { - @Override - public void onPackageRemoved(String packageName, UserHandle user) { - } - - @Override - public void onPackageAdded(String packageName, UserHandle user) { - } - - @Override - public void onPackageChanged(String packageName, UserHandle user) { - } - - @Override - public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { - } - - @Override - public void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing) { - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/LauncherAppsCallback.kt b/android/app/src/main/java/com/razinj/context_launcher/LauncherAppsCallback.kt new file mode 100644 index 0000000..2170dd6 --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/LauncherAppsCallback.kt @@ -0,0 +1,42 @@ +package com.razinj.context_launcher + +import android.content.pm.LauncherApps +import android.os.UserHandle + +/** + * Empty implementation of LauncherApps.Callback so we do not need to override all methods when + * only parts of LauncherApps.Callback are needed. + */ +open class LauncherAppsCallback : LauncherApps.Callback() { + override fun onPackageRemoved( + packageName: String, + user: UserHandle, + ) { + } + + override fun onPackageAdded( + packageName: String, + user: UserHandle, + ) { + } + + override fun onPackageChanged( + packageName: String, + user: UserHandle, + ) { + } + + override fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean, + ) { + } + + override fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean, + ) { + } +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/MainActivity.java b/android/app/src/main/java/com/razinj/context_launcher/MainActivity.java deleted file mode 100644 index 604d31a..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/MainActivity.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.razinj.context_launcher; - -import com.facebook.react.ReactActivity; -import com.facebook.react.ReactActivityDelegate; -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; -import com.facebook.react.defaults.DefaultReactActivityDelegate; - -import java.util.Objects; - -public class MainActivity extends ReactActivity { - - /** - * Returns the name of the main component registered from JavaScript. This is used to schedule - * rendering of the component. - */ - @Override - protected String getMainComponentName() { - return "context_launcher"; - } - - /** - * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link - * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React - * (aka React 18) with two boolean flags. - */ - @Override - protected ReactActivityDelegate createReactActivityDelegate() { - return new DefaultReactActivityDelegate( - this, - Objects.requireNonNull(getMainComponentName()), - // If you opted-in for the New Architecture, we enable the Fabric Renderer. - DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled - // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18). - DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled - ); - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/MainActivity.kt b/android/app/src/main/java/com/razinj/context_launcher/MainActivity.kt new file mode 100644 index 0000000..75abbe5 --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/MainActivity.kt @@ -0,0 +1,20 @@ +package com.razinj.context_launcher + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +class MainActivity : ReactActivity() { + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "context_launcher" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/MainApplication.java b/android/app/src/main/java/com/razinj/context_launcher/MainApplication.java deleted file mode 100644 index 30cefa0..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/MainApplication.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.razinj.context_launcher; - -import android.app.Application; -import android.content.Intent; -import android.os.Build; - -import androidx.annotation.NonNull; - -import com.facebook.react.PackageList; -import com.facebook.react.ReactApplication; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactPackage; -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; -import com.facebook.react.defaults.DefaultReactNativeHost; -import com.facebook.soloader.SoLoader; - -import org.jetbrains.annotations.Contract; - -import java.util.List; - -public class MainApplication extends Application implements ReactApplication { - - private final ReactNativeHost mReactNativeHost = new DefaultReactNativeHost(this) { - @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - @NonNull - @Override - protected List getPackages() { - List packages = new PackageList(this).getPackages(); - packages.add(new CustomAppPackage()); - return packages; - } - - @NonNull - @Contract(pure = true) - @Override - protected String getJSMainModuleName() { - return "index"; - } - - @Override - protected boolean isNewArchEnabled() { - return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; - } - - @Override - protected Boolean isHermesEnabled() { - return BuildConfig.IS_HERMES_ENABLED; - } - }; - - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; - } - - @Override - public void onCreate() { - super.onCreate(); - SoLoader.init(this, false); - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - DefaultNewArchitectureEntryPoint.load(); - } - ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(new Intent(this, AppProvider.class)); - } else { - startService(new Intent(this, AppProvider.class)); - } - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/MainApplication.kt b/android/app/src/main/java/com/razinj/context_launcher/MainApplication.kt new file mode 100644 index 0000000..4385336 --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/MainApplication.kt @@ -0,0 +1,52 @@ +package com.razinj.context_launcher + +import android.app.Application +import android.content.Intent +import android.os.Build +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.soloader.SoLoader + +class MainApplication : + Application(), + ReactApplication { + override val reactNativeHost: ReactNativeHost = + object : DefaultReactNativeHost(this) { + override fun getPackages(): List = + PackageList(this).packages.apply { + add(CustomAppPackage()) + } + + override fun getJSMainModuleName(): String = "index" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + + override val reactHost: ReactHost + get() = getDefaultReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + SoLoader.init(this, false) + + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(Intent(this, AppProvider::class.java)) + } else { + startService(Intent(this, AppProvider::class.java)) + } + } +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/PackageChangeReceiver.java b/android/app/src/main/java/com/razinj/context_launcher/PackageChangeReceiver.java deleted file mode 100644 index 6e61cd1..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/PackageChangeReceiver.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.razinj.context_launcher; - -import static com.razinj.context_launcher.Constants.PACKAGE_CHANGE_IS_REMOVED; -import static com.razinj.context_launcher.Constants.PACKAGE_CHANGE_NAME; -import static com.razinj.context_launcher.Constants.PACKAGE_UPDATE_ACTION; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import androidx.annotation.NonNull; - -public class PackageChangeReceiver extends BroadcastReceiver { - - @Override - public void onReceive(@NonNull Context context, @NonNull Intent intent) { - String packageName = intent.getData().getSchemeSpecificPart(); - - if (packageName.equalsIgnoreCase(context.getPackageName())) return; - - handleEvent(context, intent.getAction(), packageName, intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)); - } - - public static void handleEvent(Context context, @NonNull String action, String packageName, boolean replacing) { - if (!action.equals(Intent.ACTION_PACKAGE_ADDED) && !action.equals(Intent.ACTION_PACKAGE_CHANGED) && !action.equals(Intent.ACTION_PACKAGE_REMOVED)) { - return; - } - - Intent intent = new Intent(); - intent.setAction(PACKAGE_UPDATE_ACTION); - intent.putExtra(PACKAGE_CHANGE_NAME, packageName); - - if (action.equals(Intent.ACTION_PACKAGE_ADDED) || action.equals(Intent.ACTION_PACKAGE_CHANGED)) { - Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); - // Ignore plugin apps - if (launchIntent == null) return; - - intent.putExtra(PACKAGE_CHANGE_IS_REMOVED, Boolean.FALSE); - } else if (replacing) { - intent.putExtra(PACKAGE_CHANGE_IS_REMOVED, Boolean.FALSE); - } else { - intent.putExtra(PACKAGE_CHANGE_IS_REMOVED, Boolean.TRUE); - } - - context.sendBroadcast(intent); - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/PackageChangeReceiver.kt b/android/app/src/main/java/com/razinj/context_launcher/PackageChangeReceiver.kt new file mode 100644 index 0000000..05ae714 --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/PackageChangeReceiver.kt @@ -0,0 +1,58 @@ +package com.razinj.context_launcher + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class PackageChangeReceiver : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + val packageName = intent.data?.schemeSpecificPart + val action = intent.action + + if (packageName == null || action == null) { + return + } + if (packageName.equals(context.packageName, ignoreCase = true)) { + return + } + + handleEvent( + context, + action, + packageName, + ) + } + + companion object { + fun handleEvent( + context: Context, + action: String, + packageName: String, + ) { + if (action != Intent.ACTION_PACKAGE_ADDED && + action != Intent.ACTION_PACKAGE_CHANGED && + action != Intent.ACTION_PACKAGE_REMOVED + ) { + return + } + + val intent = Intent() + intent.setAction(Constants.PACKAGE_UPDATE_ACTION) + intent.putExtra(Constants.PACKAGE_CHANGE_NAME, packageName) + + if (action == Intent.ACTION_PACKAGE_ADDED || action == Intent.ACTION_PACKAGE_CHANGED) { + // Ignore plugin apps + context.packageManager.getLaunchIntentForPackage(packageName) ?: return + + intent.putExtra(Constants.PACKAGE_CHANGE_IS_REMOVED, false) + } else { + intent.putExtra(Constants.PACKAGE_CHANGE_IS_REMOVED, true) + } + + context.sendBroadcast(intent) + } + } +} diff --git a/android/app/src/main/java/com/razinj/context_launcher/Utils.java b/android/app/src/main/java/com/razinj/context_launcher/Utils.java deleted file mode 100644 index bff0bcc..0000000 --- a/android/app/src/main/java/com/razinj/context_launcher/Utils.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.razinj.context_launcher; - -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.util.Base64; - -import androidx.annotation.NonNull; - -import com.facebook.react.bridge.ReactApplicationContext; - -import java.io.ByteArrayOutputStream; - -public class Utils { - - private Utils() { - super(); - } - - public static String getEncodedIcon(@NonNull PackageManager pm, String packageName) { - try { - return getEncodedIcon(pm.getApplicationIcon(packageName)); - } catch (PackageManager.NameNotFoundException nameNotFoundException) { - return "NOT_FOUND"; - } - } - - public static String getEncodedIcon(@NonNull Drawable drawable) { - Bitmap bitmap; - - // Single color bitmap will be created of 1x1 pixel - bitmap = Bitmap.createBitmap( - drawable.getIntrinsicWidth() > 0 ? drawable.getIntrinsicWidth() : 1, - drawable.getIntrinsicHeight() > 0 ? drawable.getIntrinsicHeight() : 1, - Bitmap.Config.ARGB_8888 - ); - - final Canvas canvas = new Canvas(bitmap); - - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, byteArrayOutputStream); - } else { - bitmap.compress(Bitmap.CompressFormat.WEBP, 50, byteArrayOutputStream); - } - - return Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP); - } - - public static PackageInfo getPackageInfo(ReactApplicationContext reactContext) throws PackageManager.NameNotFoundException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return reactContext.getPackageManager().getPackageInfo(reactContext.getPackageName(), PackageManager.PackageInfoFlags.of(0)); - } else { - return reactContext.getPackageManager().getPackageInfo(reactContext.getPackageName(), 0); - } - } -} diff --git a/android/app/src/main/java/com/razinj/context_launcher/Utils.kt b/android/app/src/main/java/com/razinj/context_launcher/Utils.kt new file mode 100644 index 0000000..9f961cb --- /dev/null +++ b/android/app/src/main/java/com/razinj/context_launcher/Utils.kt @@ -0,0 +1,60 @@ +package com.razinj.context_launcher + +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.Base64 +import com.facebook.react.bridge.ReactApplicationContext +import java.io.ByteArrayOutputStream + +object Utils { + fun getEncodedIcon( + pm: PackageManager, + packageName: String?, + ): String = + try { + getEncodedIcon(pm.getApplicationIcon(packageName!!)) + } catch (nameNotFoundException: PackageManager.NameNotFoundException) { + "NOT_FOUND" + } + + private fun getEncodedIcon(drawable: Drawable): String { + // Single color bitmap will be created of 1x1 pixel + val bitmap = + Bitmap.createBitmap( + if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 1, + if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 1, + Bitmap.Config.ARGB_8888, + ) + + val canvas = Canvas(bitmap) + + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + + val byteArrayOutputStream = ByteArrayOutputStream() + + // See: https://developer.android.com/reference/android/graphics/Bitmap.CompressFormat#summary + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, byteArrayOutputStream) + } else { + bitmap.compress(Bitmap.CompressFormat.WEBP, 50, byteArrayOutputStream) + } + + return Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP) + } + + @Throws(PackageManager.NameNotFoundException::class) + fun getPackageInfo(reactContext: ReactApplicationContext): PackageInfo = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + reactContext.packageManager.getPackageInfo( + reactContext.packageName, + PackageManager.PackageInfoFlags.of(0), + ) + } else { + reactContext.packageManager.getPackageInfo(reactContext.packageName, 0) + } +} diff --git a/android/app/src/main/res/drawable/rn_edit_text_material.xml b/android/app/src/main/res/drawable/rn_edit_text_material.xml index f35d996..73b37e4 100644 --- a/android/app/src/main/res/drawable/rn_edit_text_material.xml +++ b/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -20,7 +20,7 @@ android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"> -