Update codebase
@@ -1,3 +0,0 @@
|
|||||||
# Tripz — تطبيق السائق (Flutter)
|
|
||||||
كود موحّد + flavor لكل مستأجر. **OffersBloc** لتدفق العروض. راجع docs/03.
|
|
||||||
الحالة: هيكل placeholder — يُنشأ فعلياً في P0/P1.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
|
||||||
# check for errors, warnings, and lints.
|
|
||||||
#
|
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
|
||||||
|
|
||||||
linter:
|
|
||||||
# The lint rules applied to this project can be customized in the
|
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
||||||
# included above or to enable additional rules. A list of all available lints
|
|
||||||
# and their documentation is published at
|
|
||||||
# https://dart-lang.github.io/linter/lints/index.html.
|
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
gradle-wrapper.jar
|
|
||||||
/.gradle
|
|
||||||
/captures/
|
|
||||||
/gradlew
|
|
||||||
/gradlew.bat
|
|
||||||
/local.properties
|
|
||||||
GeneratedPluginRegistrant.java
|
|
||||||
|
|
||||||
# Remember to never publicly share your keystore.
|
|
||||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
|
||||||
key.properties
|
|
||||||
gradle.properties
|
|
||||||
**/*.keystore
|
|
||||||
**/*.jks
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
arguments=--init-script /var/folders/cn/w9j2wl8d11517_kjx3xk_vc00000gn/T/db3b08fc4a9ef609cb16b96b200fa13e563f396e9bb1ed0905fdab7bc3bc513b.gradle --init-script /var/folders/cn/w9j2wl8d11517_kjx3xk_vc00000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle
|
|
||||||
auto.sync=true
|
|
||||||
build.scans.enabled=false
|
|
||||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
|
||||||
connection.project.dir=
|
|
||||||
eclipse.preferences.version=1
|
|
||||||
gradle.user.home=
|
|
||||||
java.home=/Users/hamzaaleghwairyeen/Library/Java/JavaVirtualMachines/openjdk-22.0.2/Contents/Home
|
|
||||||
jvm.arguments=
|
|
||||||
offline.mode=false
|
|
||||||
override.workspace.settings=true
|
|
||||||
show.console.view=true
|
|
||||||
show.executions.view=true
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id "com.android.application"
|
|
||||||
// START: FlutterFire Configuration
|
|
||||||
id 'com.google.gms.google-services'
|
|
||||||
// END: FlutterFire Configuration
|
|
||||||
id "org.jetbrains.kotlin.android"
|
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
def localProperties = new Properties()
|
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
|
||||||
if (localPropertiesFile.exists()) {
|
|
||||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
|
||||||
localProperties.load(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
|
||||||
if (keystorePropertiesFile.exists()) {
|
|
||||||
keystorePropertiesFile.withReader("UTF-8") { reader ->
|
|
||||||
keystoreProperties.load(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') ?: '12'
|
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.1.2'
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace "com.sefer_driver"
|
|
||||||
compileSdk = 36
|
|
||||||
ndkVersion "29.0.14033849"
|
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path "src/main/cpp/CMakeLists.txt"
|
|
||||||
version "3.22.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.sefer_driver"
|
|
||||||
minSdk 29
|
|
||||||
targetSdk 36
|
|
||||||
versionCode 152
|
|
||||||
versionName '2.0.152'
|
|
||||||
multiDexEnabled true
|
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a"
|
|
||||||
}
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// استدعاء المفتاح من ملف key.properties
|
|
||||||
manifestPlaceholders += [mapsApiKey: keystoreProperties['mapsApiKey'] ?: ""]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
release {
|
|
||||||
// Defensively check if properties exist to avoid NullPointerException in BundleTool
|
|
||||||
if (keystoreProperties.isEmpty()) {
|
|
||||||
println "BUILD_LOG: ERROR - No properties found in key.properties. Release build will fail."
|
|
||||||
} else {
|
|
||||||
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
|
||||||
storePassword = keystoreProperties['storePassword']
|
|
||||||
keyAlias = keystoreProperties['keyAlias']
|
|
||||||
keyPassword = keystoreProperties['keyPassword']
|
|
||||||
|
|
||||||
// Final check to ensure no crucial signing variable is null
|
|
||||||
if (!storeFile || !storePassword || !keyAlias || !keyPassword) {
|
|
||||||
println "BUILD_LOG: ERROR - One or more signing properties are NULL. Check key.properties."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
// Only assign signingConfig if the release config is valid
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
coreLibraryDesugaringEnabled true
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
java.srcDirs += 'src/main/kotlin'
|
|
||||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flutter {
|
|
||||||
source '../..'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
// تمت الترقية لتطابق تطبيق الراكب
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
|
||||||
|
|
||||||
// تمت الترقية لتطابق تطبيق الراكب (لتجنب ملفات 4KB القديمة)
|
|
||||||
implementation "com.stripe:stripe-android:21.4.2"
|
|
||||||
implementation 'com.stripe:paymentsheet:21.4.2'
|
|
||||||
|
|
||||||
implementation 'com.scottyab:rootbeer-lib:0.1.0'
|
|
||||||
|
|
||||||
// تمت الترقية لتطابق تطبيق الراكب
|
|
||||||
implementation 'com.google.android.gms:play-services-safetynet:18.1.0'
|
|
||||||
|
|
||||||
implementation "androidx.concurrent:concurrent-futures:1.2.0"
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Suppress warnings for specific Google ML Kit and Stripe classes
|
|
||||||
-dontwarn com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions$Builder
|
|
||||||
-dontwarn com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions
|
|
||||||
-dontwarn com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions$Builder
|
|
||||||
-dontwarn com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions
|
|
||||||
-dontwarn com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions$Builder
|
|
||||||
-dontwarn com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions
|
|
||||||
-dontwarn com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions$Builder
|
|
||||||
-dontwarn com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions
|
|
||||||
-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivity$g
|
|
||||||
-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Args
|
|
||||||
-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Error
|
|
||||||
-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter
|
|
||||||
-dontwarn com.stripe.android.pushProvisioning.PushProvisioningEphemeralKeyProvider
|
|
||||||
|
|
||||||
# Keep rules for Google ML Kit
|
|
||||||
-keep class com.google.mlkit.vision.** { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.** { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions$Builder { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions$Builder { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions$Builder { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions { *; }
|
|
||||||
-keep class com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions$Builder { *; }
|
|
||||||
|
|
||||||
# Keep rules for Stripe
|
|
||||||
-keep class com.stripe.android.** { *; }
|
|
||||||
-keep class com.stripe.android.pushProvisioning.** { *; }
|
|
||||||
-keep class com.yalantis.ucrop.** { *; }
|
|
||||||
-keep class com.yalantis.ucrop.util.** { *; }
|
|
||||||
|
|
||||||
# منع قراءة كود RootDetection
|
|
||||||
#-keep class com.sefer_driver.RootDetection { *; }
|
|
||||||
-keep class com.sefer_driver.RootDetection {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
|
||||||
the Flutter tool needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.sefer_driver"> <!-- تأكد من أن الباكيج هنا صحيح حسب مشروعك -->
|
|
||||||
|
|
||||||
<!-- الأذونات (Permissions) -->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
||||||
|
|
||||||
<!-- أذونات الخدمات الأمامية (ضرورية لأندرويد 14+) -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
|
||||||
<uses-permission android:name="android.permission.QUICKBOOT_POWERON" />
|
|
||||||
<uses-permission android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
|
||||||
|
|
||||||
<!-- أذونات إضافية لتطابق Intaleq -->
|
|
||||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
|
||||||
<uses-permission android:name="android.permission.PICTURE_IN_PICTURE" />
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
|
||||||
|
|
||||||
<!-- تم إضافة android:name لجعل التطبيق يعمل كـ Flutter Application بشكل صحيح مثل Intaleq -->
|
|
||||||
<application
|
|
||||||
android:name="${applicationName}"
|
|
||||||
android:label="Tripz Driver"
|
|
||||||
android:icon="@mipmap/launcher_icon"
|
|
||||||
android:allowBackup="false"
|
|
||||||
android:fullBackupContent="false"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:usesCleartextTraffic="false"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:enableOnBackInvokedCallback="true">
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:exported="true"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
|
||||||
android:resource="@style/NormalTheme" />
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<!-- Deep Link المضاف مثل Intaleq -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="tripzdriver" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="com.yalantis.ucrop.UCropActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
|
||||||
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.geo.API_KEY"
|
|
||||||
android:value="${mapsApiKey}" />
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
||||||
android:value="@string/default_notification_channel_id" />
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2" />
|
|
||||||
<!-- تم تعطيل Impeller مؤقتاً كما في Intaleq لتجنب مشاكل الريندر -->
|
|
||||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
|
|
||||||
|
|
||||||
<!-- ✅ الخدمة الأساسية للخلفية مع تحديد النوع (حل الكراش) -->
|
|
||||||
<service
|
|
||||||
android:name="id.flutter.flutter_background_service.BackgroundService"
|
|
||||||
android:foregroundServiceType="location"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true" />
|
|
||||||
|
|
||||||
<!-- الخدمات الأخرى -->
|
|
||||||
<service
|
|
||||||
android:name=".MyFirebaseMessagingService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".LocationUpdatesService"
|
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="location" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
|
||||||
android:exported="false"
|
|
||||||
tools:replace="android:exported">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.phan_tech.flutter_overlay_apps.OverlayService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="flutter.overlay.window.flutter_overlay_window.OverlayService"
|
|
||||||
android:foregroundServiceType="specialUse">
|
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="explanation_for_special_use" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="com.google.android.c2dm.permission.SEND">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
|
||||||
<category android:name="com.sefer_driver" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.dsaved.bubblehead.bubble.BubbleHeadService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="intent.bring.app.to.foreground" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
|
|
||||||
android:exported="false" />
|
|
||||||
<receiver
|
|
||||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
|
||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".YourBroadcastReceiver"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.10.2) # 3.10.2 is fine, but no need to go as high as 3.31.5
|
|
||||||
project(sefer_driver) # Good
|
|
||||||
|
|
||||||
# Add your C++ source file(s) to create a SHARED library.
|
|
||||||
add_library(native-lib SHARED native-lib.cpp)
|
|
||||||
|
|
||||||
# Find the Android log library.
|
|
||||||
find_library(log-lib log)
|
|
||||||
|
|
||||||
# Link your library against the log library. This is essential for debugging.
|
|
||||||
target_link_libraries(native-lib ${log-lib})
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
|
|
||||||
#include <jni.h>
|
|
||||||
#include <string>
|
|
||||||
#include <fstream>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <android/log.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <link.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <sys/ptrace.h> // Add this line
|
|
||||||
|
|
||||||
|
|
||||||
#define LOG_TAG "NativeLib"
|
|
||||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
|
||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
||||||
|
|
||||||
// Function to check for common root binaries
|
|
||||||
bool isRooted() {
|
|
||||||
std::string paths[] = {
|
|
||||||
"/system/app/Superuser.apk",
|
|
||||||
"/system/xbin/su",
|
|
||||||
"/system/bin/su",
|
|
||||||
"/system/bin/magisk",
|
|
||||||
"/system/xbin/magisk",
|
|
||||||
"/sbin/magisk"
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &path : paths) {
|
|
||||||
std::ifstream file(path);
|
|
||||||
if (file.good()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to check for the presence of files or directories commonly associated with Frida.
|
|
||||||
bool checkFridaFiles() {
|
|
||||||
std::string fridaFiles[] = {
|
|
||||||
"/data/local/tmp/re.frida.server", //Common Frida server path
|
|
||||||
"/data/local/tmp/frida-server",
|
|
||||||
"/usr/lib/libfrida-gadget.so", // Frida gadget (injected library)
|
|
||||||
"/usr/lib64/libfrida-gadget.so",
|
|
||||||
"/data/local/re.frida.server",
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &path : fridaFiles) {
|
|
||||||
if (access(path.c_str(), F_OK) != -1) {
|
|
||||||
LOGE("Frida file detected: %s", path.c_str());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks for open ports commonly used by Frida. This is less reliable, as ports can be changed.
|
|
||||||
bool checkFridaPorts() {
|
|
||||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
|
||||||
if (sock == -1) {
|
|
||||||
return false; // Couldn't create socket, not a strong indicator.
|
|
||||||
}
|
|
||||||
|
|
||||||
sockaddr_in sa;
|
|
||||||
memset(&sa, 0, sizeof(sa));
|
|
||||||
sa.sin_family = AF_INET;
|
|
||||||
sa.sin_port = htons(27042); // Default Frida port
|
|
||||||
inet_pton(AF_INET, "127.0.0.1", &sa.sin_addr);
|
|
||||||
|
|
||||||
if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) != -1) {
|
|
||||||
LOGE("Frida default port (27042) is open.");
|
|
||||||
close(sock);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
close(sock);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the maps file of the current process for any suspicious entries.
|
|
||||||
bool checkMaps() {
|
|
||||||
std::ifstream mapsFile("/proc/self/maps");
|
|
||||||
std::string line;
|
|
||||||
|
|
||||||
if (mapsFile.is_open()) {
|
|
||||||
while (std::getline(mapsFile, line)) {
|
|
||||||
// Look for lines that indicate injected libraries, especially Frida.
|
|
||||||
if (line.find("frida") != std::string::npos ||
|
|
||||||
line.find("gum-js-") != std::string::npos) { // Gum is Frida's JavaScript engine
|
|
||||||
LOGE("Suspicious entry in /proc/self/maps: %s", line.c_str());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapsFile.close();
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
LOGE("Could not open /proc/self/maps");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check loaded modules.
|
|
||||||
bool checkLoadedModules() {
|
|
||||||
bool found = false;
|
|
||||||
dl_iterate_phdr([](struct dl_phdr_info *info, size_t size, void *data) {
|
|
||||||
bool *found_ptr = static_cast<bool *>(data);
|
|
||||||
if (std::string(info->dlpi_name).find("frida") != std::string::npos) {
|
|
||||||
LOGE("Frida module detected: %s", info->dlpi_name);
|
|
||||||
*found_ptr = true;
|
|
||||||
return 1; // Stop iterating
|
|
||||||
}
|
|
||||||
return 0; // Continue iterating
|
|
||||||
}, &found);
|
|
||||||
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// This is a simple ptrace check. More sophisticated checks are possible (and necessary for robust detection).
|
|
||||||
//bool checkPtrace() {
|
|
||||||
// // Attempt to ptrace ourselves. If another process is already tracing us, this will fail.
|
|
||||||
// if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {
|
|
||||||
// LOGE("ptrace failed. Debugger or tracer detected.");
|
|
||||||
// return true; // Likely being traced
|
|
||||||
// }
|
|
||||||
// // Detach. If attached, need to detach to not interfere.
|
|
||||||
// ptrace(PTRACE_DETACH, 0, 0, 0);
|
|
||||||
// return false;
|
|
||||||
//}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sefer_1driver_RootDetection_isNativeRooted(JNIEnv *env, jobject /* this */) {
|
|
||||||
|
|
||||||
if (isRooted()) {
|
|
||||||
return JNI_TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkFridaFiles()) {
|
|
||||||
return JNI_TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkFridaPorts()) {
|
|
||||||
return JNI_TRUE;
|
|
||||||
}
|
|
||||||
if (checkMaps()) {
|
|
||||||
return JNI_TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkLoadedModules()) {
|
|
||||||
return JNI_TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (checkPtrace()) {
|
|
||||||
// return JNI_TRUE;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return JNI_FALSE;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.sefer_driver
|
|
||||||
|
|
||||||
import android.app.IntentService
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
// TODO: Rename actions, choose action names that describe tasks that this
|
|
||||||
// IntentService can perform, e.g. ACTION_FETCH_NEW_ITEMS
|
|
||||||
private const val ACTION_FOO = "com.sefer_driver.action.FOO"
|
|
||||||
private const val ACTION_BAZ = "com.sefer_driver.action.BAZ"
|
|
||||||
|
|
||||||
// TODO: Rename parameters
|
|
||||||
private const val EXTRA_PARAM1 = "com.sefer_driver.extra.PARAM1"
|
|
||||||
private const val EXTRA_PARAM2 = "com.sefer_driver.extra.PARAM2"
|
|
||||||
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
package com.sefer_driver
|
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.setPadding
|
|
||||||
import com.scottyab.rootbeer.RootBeer
|
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import java.io.File
|
|
||||||
import java.util.Timer
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class MainActivity : FlutterFragmentActivity() {
|
|
||||||
private val SECURITY_CHANNEL = "com.sefer_driver/security"
|
|
||||||
private val APP_CONTROL_CHANNEL = "com.sefer_driver/app_control"
|
|
||||||
|
|
||||||
// 🔴 1. تعريف القناة لتكون متاحة للوصول من الدوال الأخرى
|
|
||||||
private var appControlChannel: MethodChannel? = null
|
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
|
||||||
super.configureFlutterEngine(flutterEngine)
|
|
||||||
|
|
||||||
// 🔴 2. تهيئة القناة هنا
|
|
||||||
appControlChannel =
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, APP_CONTROL_CHANNEL)
|
|
||||||
|
|
||||||
// Channel for security checks (isRooted)
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SECURITY_CHANNEL)
|
|
||||||
.setMethodCallHandler { call, result ->
|
|
||||||
when (call.method) {
|
|
||||||
"isNativeRooted" -> result.success(isDeviceCompromised())
|
|
||||||
else -> result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel for app control (bringing to foreground & overlay events)
|
|
||||||
appControlChannel?.setMethodCallHandler { call, result ->
|
|
||||||
when (call.method) {
|
|
||||||
"bringToForeground" -> {
|
|
||||||
Log.d("MainActivity", "Received bringToForeground request")
|
|
||||||
val intent =
|
|
||||||
Intent(this, MainActivity::class.java).apply {
|
|
||||||
action = Intent.ACTION_MAIN
|
|
||||||
addCategory(Intent.CATEGORY_LAUNCHER)
|
|
||||||
addFlags(
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
||||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
||||||
)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
startActivity(intent)
|
|
||||||
Log.d("MainActivity", "App brought to foreground successfully")
|
|
||||||
result.success(true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "Error bringing app to foreground", e)
|
|
||||||
result.error("ACTIVITY_START_FAILED", e.message, e.stackTraceToString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
Log.d("MainActivity", "MainActivity onCreate")
|
|
||||||
if (isDeviceCompromised()) {
|
|
||||||
showSecurityWarningDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔴 3. فحص نية فتح التطبيق (هل جاء من زر قبول النافذة؟)
|
|
||||||
checkIntentForOverlayAccept(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
setIntent(intent)
|
|
||||||
Log.d("MainActivity", "Received new intent: ${intent.action}")
|
|
||||||
|
|
||||||
// 🔴 4. فحص نية التطبيق عند الفتح من الخلفية
|
|
||||||
checkIntentForOverlayAccept(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 🔴 5. الدالة السحرية لالتقاط زر القبول وإرساله للـ Flutter 🔴
|
|
||||||
// =========================================================================
|
|
||||||
private fun checkIntentForOverlayAccept(intent: Intent) {
|
|
||||||
val acceptedTripId = intent.getStringExtra("acceptedTripId")
|
|
||||||
if (acceptedTripId != null) {
|
|
||||||
Log.d("MainActivity", "✅ Trip accepted via Native Intent: $acceptedTripId")
|
|
||||||
// إرسال الإشارة عبر القناة لتطبيق فلاتر
|
|
||||||
appControlChannel?.invokeMethod("onOverlayTripAccepted", acceptedTripId)
|
|
||||||
// مسح البيانات حتى لا تتكرر العملية إذا أغلق السائق التطبيق وفتحه يدوياً
|
|
||||||
intent.removeExtra("acceptedTripId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// --- دوال الحماية الخاصة بك (لم يتم المساس بها) ---
|
|
||||||
// =========================================================================
|
|
||||||
private fun isDeviceCompromised(): Boolean {
|
|
||||||
return try {
|
|
||||||
val isRootedByRootBeer = RootBeer(this).isRooted
|
|
||||||
Log.d("MainActivity", "Root check result: $isRootedByRootBeer")
|
|
||||||
isRootedByRootBeer
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "Security check error: ${e.message}", e)
|
|
||||||
true // Fail-safe: assume compromised if check fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSecurityWarningDialog() {
|
|
||||||
var secondsRemaining = 10
|
|
||||||
val progressBar =
|
|
||||||
ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply {
|
|
||||||
max = 10
|
|
||||||
progress = 10
|
|
||||||
}
|
|
||||||
val textView =
|
|
||||||
TextView(this).apply {
|
|
||||||
text = getString(R.string.security_warning_message)
|
|
||||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
|
||||||
}
|
|
||||||
val layout =
|
|
||||||
LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(48)
|
|
||||||
addView(textView)
|
|
||||||
addView(progressBar)
|
|
||||||
}
|
|
||||||
val dialog =
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(getString(R.string.security_warning_title))
|
|
||||||
.setView(layout)
|
|
||||||
.setCancelable(false)
|
|
||||||
.create()
|
|
||||||
dialog.show()
|
|
||||||
val timer = Timer()
|
|
||||||
timer.schedule(0, 1000) {
|
|
||||||
secondsRemaining--
|
|
||||||
runOnUiThread {
|
|
||||||
progressBar.progress = secondsRemaining
|
|
||||||
if (secondsRemaining <= 0) {
|
|
||||||
timer.cancel()
|
|
||||||
dialog.dismiss()
|
|
||||||
clearAppDataAndExit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearAppDataAndExit() {
|
|
||||||
try {
|
|
||||||
Runtime.getRuntime().exec("pm clear $packageName")
|
|
||||||
Log.d("MainActivity", "Cleared app data via package manager")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "Error clearing app data: ${e.message}", e)
|
|
||||||
clearCache()
|
|
||||||
clearAppData()
|
|
||||||
}
|
|
||||||
finishAffinity()
|
|
||||||
System.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearCache() {
|
|
||||||
deleteDir(cacheDir)
|
|
||||||
deleteDir(externalCacheDir)
|
|
||||||
Log.d("MainActivity", "Cleared cache directories")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearAppData() {
|
|
||||||
Log.d("MainActivity", "App data clearing skipped (commented out)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteDir(dir: File?): Boolean {
|
|
||||||
if (dir != null && dir.isDirectory) {
|
|
||||||
dir.list()?.forEach { deleteDir(File(dir, it)) }
|
|
||||||
}
|
|
||||||
val deleted = dir?.delete() ?: false
|
|
||||||
Log.d("MainActivity", "Deleted directory ${dir?.path}: $deleted")
|
|
||||||
return deleted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.sefer_driver
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Intent
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
|
|
||||||
class MyApplication : Application() {
|
|
||||||
companion object {
|
|
||||||
lateinit var instance: MyApplication
|
|
||||||
private set
|
|
||||||
|
|
||||||
val flutterEngine: FlutterEngine by lazy {
|
|
||||||
FlutterEngine(instance).apply {
|
|
||||||
dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
instance = this
|
|
||||||
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.sefer_driver/app_lifecycle")
|
|
||||||
.setMethodCallHandler { call, result ->
|
|
||||||
if (call.method == "bringAppToForeground") {
|
|
||||||
bringAppToForeground()
|
|
||||||
result.success(null)
|
|
||||||
} else {
|
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bringAppToForeground() {
|
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
package com.sefer_driver
|
|
||||||
|
|
||||||
object RootDetection {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("native-lib") // Load the native library
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun isNativeRooted(): Boolean // Declare the external function
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import android.content.Context
|
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.android.gms.safetynet.SafetyNet
|
|
||||||
import com.google.android.gms.safetynet.SafetyNetApi
|
|
||||||
import com.google.android.gms.tasks.OnFailureListener
|
|
||||||
import com.google.android.gms.tasks.OnSuccessListener
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.IOException
|
|
||||||
import java.security.GeneralSecurityException
|
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.Arrays
|
|
||||||
|
|
||||||
object SafetyNetCheck {
|
|
||||||
|
|
||||||
private const val TAG = "SafetyNetCheck"
|
|
||||||
|
|
||||||
fun checkSafetyNet(context: Context, apiKey: String, callback: (Boolean) -> Unit) {
|
|
||||||
// Generate a nonce. A good nonce is large, random, and used only once.
|
|
||||||
val nonce = generateNonce()
|
|
||||||
|
|
||||||
SafetyNet.getClient(context).attest(nonce, apiKey)
|
|
||||||
.addOnSuccessListener { response ->
|
|
||||||
// Success! Now, *verify* the response.
|
|
||||||
val jwsResult = response.jwsResult
|
|
||||||
if (jwsResult != null) {
|
|
||||||
try {
|
|
||||||
val isSafe = SafetyNetResponseVerifier.verify(jwsResult)
|
|
||||||
Log.d(TAG, "SafetyNet verification result: $isSafe")
|
|
||||||
callback(isSafe) // Now passing a *verified* result.
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error verifying SafetyNet response: ${e.message}", e)
|
|
||||||
callback(false) // Treat verification errors as failures.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "SafetyNet jwsResult is null")
|
|
||||||
callback(false) // Null result is a failure.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.addOnFailureListener { e ->
|
|
||||||
Log.e(TAG, "SafetyNet attest API call failed: ${e.message}", e)
|
|
||||||
callback(false) // API call failure.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Helper function to generate a nonce.
|
|
||||||
private fun generateNonce(): ByteArray {
|
|
||||||
val byteGenerator = SecureRandom()
|
|
||||||
val nonce = ByteArray(32)
|
|
||||||
byteGenerator.nextBytes(nonce)
|
|
||||||
return nonce
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Helper class to verify the SafetyNet response.
|
|
||||||
object SafetyNetResponseVerifier {
|
|
||||||
|
|
||||||
private const val TAG = "SafetyNetVerifier"
|
|
||||||
|
|
||||||
// This method *must* be implemented on a *backend server* for real security.
|
|
||||||
// This is just a *simplified example* for demonstration purposes and is
|
|
||||||
// *not* suitable for production without a backend check.
|
|
||||||
@Throws(GeneralSecurityException::class, IOException::class)
|
|
||||||
fun verify(jwsResult: String): Boolean {
|
|
||||||
// 1. Parse the JWS: Split into header, payload, and signature.
|
|
||||||
val parts = jwsResult.split(".")
|
|
||||||
if (parts.size != 3) {
|
|
||||||
Log.e(TAG, "Invalid JWS format")
|
|
||||||
return false // Invalid JWS format
|
|
||||||
}
|
|
||||||
|
|
||||||
val header = parts[0]
|
|
||||||
val payload = parts[1]
|
|
||||||
val signature = parts[2]
|
|
||||||
|
|
||||||
// 2. Decode the payload (it's Base64 encoded).
|
|
||||||
val decodedPayload = Base64.decode(payload, Base64.DEFAULT)
|
|
||||||
val payloadJson = JSONObject(String(decodedPayload))
|
|
||||||
|
|
||||||
// 3. Check the ctsProfileMatch and basicIntegrity.
|
|
||||||
val ctsProfileMatch = payloadJson.optBoolean("ctsProfileMatch", false)
|
|
||||||
val basicIntegrity = payloadJson.optBoolean("basicIntegrity", false)
|
|
||||||
|
|
||||||
Log.d(TAG, "ctsProfileMatch: $ctsProfileMatch, basicIntegrity: $basicIntegrity")
|
|
||||||
|
|
||||||
// 4. **CRITICAL: In a real application, you *must* send the JWS to your
|
|
||||||
// backend server for verification. The server should use the
|
|
||||||
// Google SafetyNet API (or a library that wraps it) to verify
|
|
||||||
// the signature and check the fields. This prevents attackers
|
|
||||||
// from tampering with the response on the device.**
|
|
||||||
//
|
|
||||||
// // Example (pseudo-code) of what the backend check would do:
|
|
||||||
// // GoogleCredential credential = ...;
|
|
||||||
// // SafetyNet safetyNet = new SafetyNet.Builder(httpTransport, jsonFactory)
|
|
||||||
// // .setApplicationName("YourAppName")
|
|
||||||
// // .setHttpRequestInitializer(credential)
|
|
||||||
// // .build();
|
|
||||||
// // SafetyNetApi.VerifyJwsRequest request = safetyNet.safetynet().verifyJws(jwsResult);
|
|
||||||
// // SafetyNetApi.VerifyJwsResponse response = request.execute();
|
|
||||||
// // return response.isValidSignature() && response.getCtsProfileMatch() && response.getBasicIntegrity();
|
|
||||||
|
|
||||||
// 5. For this *example* (without a backend), we'll just check the fields.
|
|
||||||
// This is *NOT SECURE* for production!
|
|
||||||
return ctsProfileMatch && basicIntegrity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="?android:colorBackground" />
|
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
|
||||||
|
Before Width: | Height: | Size: 158 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="@android:color/white" />
|
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
|
||||||
|
Before Width: | Height: | Size: 162 KiB |
@@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/notification_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Title" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/notification_message"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 61 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="security_warning_title">تحذير أمني</string>
|
|
||||||
<string name="security_warning_message">تم اكتشاف مشكلة أمنية أو تعديل على هذا الجهاز. لا يمكن تشغيل التطبيق على هذا الجهاز.</string>
|
|
||||||
<string name="exit_button">إغلاق التطبيق</string>
|
|
||||||
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
|
||||||
<!-- <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">-->
|
|
||||||
<style name="LaunchTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<!-- <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">-->
|
|
||||||
<style name="NormalTheme" parent="Theme.MaterialComponents">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">My App</string>
|
|
||||||
<string name="default_notification_channel_id">high_importance_channel</string>
|
|
||||||
<string name="api_key">kffk</string>
|
|
||||||
<string name="api_key_safety">AIzaSyB04YNW3LbvmQ5lX1t2bOwEU18-</string>
|
|
||||||
<string name="channel_name">FCM Notifications</string>
|
|
||||||
<string name="channel_description">Notifications from Firebase Cloud Messaging</string>
|
|
||||||
<string name="security_warning_title">Security Warning</string>
|
|
||||||
<string name="security_warning_message">A security issue or modification has been detected on
|
|
||||||
this device. The app cannot run on this device.</string>
|
|
||||||
<string name="exit_button">Exit App</string>
|
|
||||||
<string name="device_secure">Device is secure. Proceeding normally.</string>
|
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
|
||||||
<!-- <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">-->
|
|
||||||
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<!-- <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">-->
|
|
||||||
<style name="NormalTheme" parent="Theme.MaterialComponents">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<network-security-config>
|
|
||||||
<!-- منع أي اتصال HTTP غير مشفّر -->
|
|
||||||
<base-config cleartextTrafficPermitted="false">
|
|
||||||
<trust-anchors>
|
|
||||||
<!-- الاعتماد على شهادات النظام فقط -->
|
|
||||||
<certificates src="system" />
|
|
||||||
</trust-anchors>
|
|
||||||
</base-config>
|
|
||||||
|
|
||||||
<!-- اسمح بالاتصال بدوميناتك فقط -->
|
|
||||||
<domain-config cleartextTrafficPermitted="false">
|
|
||||||
<domain includeSubdomains="true">tripz-egypt.com</domain>
|
|
||||||
|
|
||||||
</domain-config>
|
|
||||||
|
|
||||||
<!-- في debug فقط: اسمح بشهادات المستخدم لسهولة الاختبار عبر Proxy -->
|
|
||||||
<debug-overrides>
|
|
||||||
<trust-anchors>
|
|
||||||
<certificates src="system" />
|
|
||||||
<certificates src="user" />
|
|
||||||
</trust-anchors>
|
|
||||||
</debug-overrides>
|
|
||||||
</network-security-config>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
|
||||||
the Flutter tool needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
ext.kotlin_version = '2.3.10'
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:8.13.2'
|
|
||||||
classpath 'com.google.gms:google-services:4.3.15'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
|
||||||
|
|
||||||
subprojects {
|
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
|
||||||
project.evaluationDependsOn(':app')
|
|
||||||
|
|
||||||
def setupAndroid = {
|
|
||||||
if (project.plugins.hasPlugin('com.android.library') || project.plugins.hasPlugin('com.android.application')) {
|
|
||||||
project.android {
|
|
||||||
if (namespace == null || namespace == "") {
|
|
||||||
namespace = project.group
|
|
||||||
}
|
|
||||||
compileSdk 36
|
|
||||||
defaultConfig {
|
|
||||||
targetSdk 36
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project.state.executed) {
|
|
||||||
setupAndroid()
|
|
||||||
} else {
|
|
||||||
project.afterEvaluate {
|
|
||||||
setupAndroid()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("clean", Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
def flutterSdkPath = {
|
|
||||||
def properties = new Properties()
|
|
||||||
file("local.properties").withInputStream { properties.load(it) }
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
|
||||||
return flutterSdkPath
|
|
||||||
}()
|
|
||||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
|
||||||
id "com.android.application" version '8.13.2' apply false
|
|
||||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
|
||||||
id "org.jetbrains.kotlin.android" version "2.3.10" apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
include ":app"
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 715 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 448 B |
|
Before Width: | Height: | Size: 3.0 MiB |
@@ -1,7 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
.dart_tool/
|
|
||||||
|
|
||||||
.packages
|
|
||||||
.pub/
|
|
||||||
|
|
||||||
build/
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
|
||||||
#
|
|
||||||
# This file should be version controlled and should not be manually edited.
|
|
||||||
|
|
||||||
version:
|
|
||||||
revision: f4abaa0735eba4dfd8f33f73363911d63931fe03
|
|
||||||
channel: stable
|
|
||||||
|
|
||||||
project_type: plugin
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
## 0.0.1
|
|
||||||
|
|
||||||
* Created and implemented startBubbleHead and closeBubbleHead usecases for bubble head package
|
|
||||||
|
|
||||||
|
|
||||||
## 0.0.2
|
|
||||||
* Fix read-me documentation
|
|
||||||
|
|
||||||
## 0.0.3
|
|
||||||
* Fix read-me documentation (added `Buy me a coffee link`)
|
|
||||||
|
|
||||||
## 0.0.4
|
|
||||||
* Added optional parameter to enable or disable send-app-to-background
|
|
||||||
* Updated documentation
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 chrisoftech
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# bubble_head
|
|
||||||
|
|
||||||
|
|
||||||
A flutter plugin to enable you launch a bubble while putting your application to background and upon clicking the bubble brings your application back to foreground
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
### Add dependency
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
dependencies:
|
|
||||||
bubble_head: ^0.0.4
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Add in android-manifest file (**../main/AndroidManifest.xml**)
|
|
||||||
|
|
||||||
If you are unsure on where to do this, you can reference the example project AndroidManifest.xml file [here](example/android/app/src/main/AndroidManifest.xml)
|
|
||||||
|
|
||||||
|
|
||||||
Add `SYSTEM_ALERT_WINDOW` permission in manifest
|
|
||||||
```xml
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: For best UX practices, you should request for `SYSTEM_ALERT_WINDOW` permission on your application launch (if permission `status` is not granted)
|
|
||||||
To request for permission, we advise the use of this [package](https://pub.dev/packages/permission_handler)
|
|
||||||
|
|
||||||
|
|
||||||
Add `intent-filter` in activity tag
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="intent.bring.app.to.foreground" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
```
|
|
||||||
|
|
||||||
Add `service` in application tag
|
|
||||||
```xml
|
|
||||||
<service
|
|
||||||
android:name="com.dsaved.bubblehead.bubble.BubbleHeadService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Note: To set bubble icon, create `assets/images` folder path and add your png icon with name `icon.png` to the directory (ensure to import assets in your `pubspec.yaml` file)
|
|
||||||
|
|
||||||
**GIF illustration**
|
|
||||||
|
|
||||||
[](example/assets/images/bubble_head_example.gif "Bubble-head example")
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
**To start bubble**
|
|
||||||
[This puts your app in background and can be re-launched (brought to foreground) on tap of the bubble]
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Bubble _bubble = new Bubble();
|
|
||||||
|
|
||||||
Future<void> startBubbleHead() async {
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _bubble.startBubbleHead();
|
|
||||||
} on PlatformException {
|
|
||||||
print('Failed to call startBubbleHead');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**To stop/close bubble**
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Bubble _bubble = new Bubble();
|
|
||||||
|
|
||||||
Future<void> stopBubbleHead() async {
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _bubble.stopBubbleHead();
|
|
||||||
} on PlatformException {
|
|
||||||
print('Failed to call stopBubbleHead');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can prevent the default action of putting your application in background when starting `bubble_head` by setting `sendAppToBackground` parameter when starting bubble head (if you choose to use another means of sending your application to background)
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Bubble _bubble = new Bubble();
|
|
||||||
|
|
||||||
Future<void> startBubbleHead() async {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// this will only display the bubble-head without sending the application to background
|
|
||||||
await _bubble.startBubbleHead(sendAppToBackground: false);
|
|
||||||
} on PlatformException {
|
|
||||||
print('Failed to call startBubbleHead');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
**Other parameters**
|
|
||||||
(You can choose to tweak **optional** parameters when initializing bubble)
|
|
||||||
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Bubble({
|
|
||||||
this.shouldBounce = true,
|
|
||||||
this.allowDragToClose = true,
|
|
||||||
this.showCloseButton = false,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
```dart
|
|
||||||
Bubble().startBubbleHead(sendAppToBackground: true);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameter Definition**
|
|
||||||
- shouldBounce - Defaults to `True`
|
|
||||||
(Adds animation to bubble-head)
|
|
||||||
- allowDragToClose - Defaults to `True`
|
|
||||||
(Enables dragging bubble to bottom screen to exit)
|
|
||||||
- showCloseButton - Defaults to `False`
|
|
||||||
(Adds a close button icon to the bubble-head)
|
|
||||||
- sendAppToBackground - Defaults to `True`
|
|
||||||
(Sends application to background)
|
|
||||||
|
|
||||||
## [Buy me a Coffee](https://www.buymeacoffee.com/dsaved)
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/libraries
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
group 'com.dsaved.bubblehead.bubble'
|
|
||||||
version '1.0'
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.library'
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'com.dsaved.bubblehead.bubble'
|
|
||||||
compileSdkVersion 36
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
|
||||||
android.useAndroidX=true
|
|
||||||
android.enableJetifier=true
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
rootProject.name = 'bubble'
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.dsaved.bubblehead.bubble">
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
package com.dsaved.bubblehead.bubble;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.PixelFormat;
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.CountDownTimer;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.util.Base64;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
public class BubbleHeadService extends Service implements View.OnClickListener {
|
|
||||||
private WindowManager mWindowManager;
|
|
||||||
private View mFloatingWidgetView;
|
|
||||||
private ImageView remove_image_view;
|
|
||||||
private final Point szWindow = new Point();
|
|
||||||
private View removeFloatingWidgetView;
|
|
||||||
private static boolean showCloseButton = false, _bounce = true, _dragToClose = true, _sendAppToBackground = true;
|
|
||||||
private boolean _continueToSnap = false;
|
|
||||||
|
|
||||||
private int x_init_cord, y_init_cord, x_init_margin, y_init_margin;
|
|
||||||
static Bitmap _image;
|
|
||||||
|
|
||||||
// Set the value for showing close button to true or false
|
|
||||||
public static void shouldShowCloseButton(Boolean show) {
|
|
||||||
showCloseButton = show;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set to true to enable bouncing
|
|
||||||
public static void bounce(Boolean bounce) {
|
|
||||||
_bounce = bounce;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the value for drag to close to enable dragging bubble to close
|
|
||||||
public static void dragToClose(Boolean dragToClose) {
|
|
||||||
_dragToClose = dragToClose;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sendAppToBackground(Boolean sendAppToBackground) {
|
|
||||||
_sendAppToBackground = sendAppToBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void startService(Context activity, String image) {
|
|
||||||
byte[] decodedBytes = Base64.decode(image, Base64.DEFAULT);
|
|
||||||
_image = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);
|
|
||||||
|
|
||||||
// send application to background if this is true
|
|
||||||
if (_sendAppToBackground) {
|
|
||||||
Intent i = new Intent();
|
|
||||||
i.setAction(Intent.ACTION_MAIN);
|
|
||||||
i.addCategory(Intent.CATEGORY_HOME);
|
|
||||||
activity.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent(activity, BubbleHeadService.class);
|
|
||||||
activity.startService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void stopService(Context activity) {
|
|
||||||
Intent intent = new Intent(activity, BubbleHeadService.class);
|
|
||||||
activity.stopService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create an empty constructor
|
|
||||||
public BubbleHeadService() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint({"ClickableViewAccessibility", "InflateParams"})
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
// init WindowManager
|
|
||||||
mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
|
||||||
getWindowManagerDefaultDisplay();
|
|
||||||
|
|
||||||
// Init LayoutInflater
|
|
||||||
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
|
|
||||||
|
|
||||||
// Inflate the removing view layout we created
|
|
||||||
removeFloatingWidgetView = inflater.inflate(R.layout.bubble_head_remove_widget, null);
|
|
||||||
|
|
||||||
// Add the view to the window.
|
|
||||||
WindowManager.LayoutParams paramRemove;
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
paramRemove = new WindowManager.LayoutParams(
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.TYPE_PHONE,
|
|
||||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
} else {
|
|
||||||
paramRemove = new WindowManager.LayoutParams(
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
|
||||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specify the view position
|
|
||||||
paramRemove.gravity = Gravity.TOP | Gravity.LEFT;
|
|
||||||
|
|
||||||
// Initially the Removing widget view is not visible, so set visibility to GONE
|
|
||||||
removeFloatingWidgetView.setVisibility(View.GONE);
|
|
||||||
remove_image_view = (ImageView) removeFloatingWidgetView.findViewById(R.id.remove_img);
|
|
||||||
|
|
||||||
// Add the view to the window
|
|
||||||
mWindowManager.addView(removeFloatingWidgetView, paramRemove);
|
|
||||||
|
|
||||||
// Inflate the floating view layout we created
|
|
||||||
mFloatingWidgetView = inflater.inflate(R.layout.layout_bubble_head, null);
|
|
||||||
|
|
||||||
// Add the view to the window.
|
|
||||||
WindowManager.LayoutParams params;
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
params = new WindowManager.LayoutParams(
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.TYPE_PHONE,
|
|
||||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
} else {
|
|
||||||
params = new WindowManager.LayoutParams(
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
||||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
|
||||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specify the view position
|
|
||||||
params.gravity = Gravity.TOP | Gravity.LEFT;
|
|
||||||
|
|
||||||
// Initially view will be added to top-left corner, you change x-y coordinates according to your need
|
|
||||||
params.x = 0;
|
|
||||||
params.y = 100;
|
|
||||||
|
|
||||||
// Add the view to the window
|
|
||||||
mWindowManager.addView(mFloatingWidgetView, params);
|
|
||||||
|
|
||||||
//set image to chatHead
|
|
||||||
ImageView chatHeadImage = mFloatingWidgetView.findViewById(R.id.chat_head_profile_iv);
|
|
||||||
chatHeadImage.setImageBitmap(_image);
|
|
||||||
|
|
||||||
// find id of close image button
|
|
||||||
ImageView closeBubbleHead = mFloatingWidgetView.findViewById(R.id.close_bubble_head);
|
|
||||||
closeBubbleHead.setOnClickListener(this);
|
|
||||||
if (!showCloseButton) {
|
|
||||||
closeBubbleHead.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
implementTouchListenerToFloatingWidgetView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getWindowManagerDefaultDisplay() {
|
|
||||||
mWindowManager.getDefaultDisplay().getSize(szWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private void implementTouchListenerToFloatingWidgetView() {
|
|
||||||
_continueToSnap = true;
|
|
||||||
// Drag and move chat head using user's touch action.
|
|
||||||
mFloatingWidgetView.findViewById(R.id.root_container);
|
|
||||||
mFloatingWidgetView.setOnTouchListener(new View.OnTouchListener() {
|
|
||||||
long time_start = 0, time_end = 0;
|
|
||||||
|
|
||||||
boolean isLongClick = false;
|
|
||||||
boolean inBounded = false;
|
|
||||||
int remove_img_width = 0, remove_img_height = 0;
|
|
||||||
|
|
||||||
final Handler handler_longClick = new Handler();
|
|
||||||
final Runnable runnable_longClick = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
isLongClick = true;
|
|
||||||
removeFloatingWidgetView.setVisibility(View.VISIBLE);
|
|
||||||
onFloatingWidgetLongClick();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
|
||||||
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams();
|
|
||||||
|
|
||||||
int x_cord = (int) event.getRawX();
|
|
||||||
int y_cord = (int) event.getRawY();
|
|
||||||
|
|
||||||
int x_cord_Destination, y_cord_Destination;
|
|
||||||
|
|
||||||
switch (event.getAction()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
time_start = System.currentTimeMillis();
|
|
||||||
|
|
||||||
if (_dragToClose) {
|
|
||||||
handler_longClick.postDelayed(runnable_longClick, 100);
|
|
||||||
}
|
|
||||||
remove_img_width = remove_image_view.getLayoutParams().width;
|
|
||||||
remove_img_height = remove_image_view.getLayoutParams().height;
|
|
||||||
|
|
||||||
x_init_cord = x_cord;
|
|
||||||
y_init_cord = y_cord;
|
|
||||||
|
|
||||||
// remember the initial position.
|
|
||||||
x_init_margin = layoutParams.x;
|
|
||||||
y_init_margin = layoutParams.y;
|
|
||||||
return true;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
isLongClick = false;
|
|
||||||
removeFloatingWidgetView.setVisibility(View.GONE);
|
|
||||||
remove_image_view.getLayoutParams().height = remove_img_height;
|
|
||||||
remove_image_view.getLayoutParams().width = remove_img_width;
|
|
||||||
|
|
||||||
if (_dragToClose) {
|
|
||||||
handler_longClick.removeCallbacks(runnable_longClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user drag and drop the floating widget view
|
|
||||||
// into remove view then stop the service
|
|
||||||
if (inBounded) {
|
|
||||||
stopSelf();
|
|
||||||
inBounded = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Difference between initial coordinate and current coordinate
|
|
||||||
int x_diff = x_cord - x_init_cord;
|
|
||||||
int y_diff = y_cord - y_init_cord;
|
|
||||||
|
|
||||||
// check if action move is little as move happen on view with just a tap
|
|
||||||
if (Math.abs(x_diff) < 5 && Math.abs(y_diff) < 5) {
|
|
||||||
time_end = System.currentTimeMillis();
|
|
||||||
// only perform click if time is less than 200ms
|
|
||||||
if ((time_end - time_start) < 200) {
|
|
||||||
onFloatingWidgetClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
y_cord_Destination = y_init_margin + y_diff;
|
|
||||||
|
|
||||||
int barHeight = getStatusBarHeight();
|
|
||||||
if (y_cord_Destination < 0) {
|
|
||||||
y_cord_Destination = 0;
|
|
||||||
} else if (y_cord_Destination + (mFloatingWidgetView.getHeight() + barHeight) > szWindow.y) {
|
|
||||||
y_cord_Destination = szWindow.y - (mFloatingWidgetView.getHeight() + barHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutParams.y = y_cord_Destination;
|
|
||||||
|
|
||||||
inBounded = false;
|
|
||||||
|
|
||||||
// reset position
|
|
||||||
resetPosition(x_cord);
|
|
||||||
return true;
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
int x_diff_move = x_cord - x_init_cord;
|
|
||||||
int y_diff_move = y_cord - y_init_cord;
|
|
||||||
|
|
||||||
x_cord_Destination = x_init_margin + x_diff_move;
|
|
||||||
y_cord_Destination = y_init_margin + y_diff_move;
|
|
||||||
|
|
||||||
// If user long click the floating view, update remove view
|
|
||||||
if (isLongClick) {
|
|
||||||
int x_bound_left = szWindow.x / 2 - (int) (remove_img_width * 1.5);
|
|
||||||
int x_bound_right = szWindow.x / 2 + (int) (remove_img_width * 1.5);
|
|
||||||
int y_bound_top = szWindow.y - (int) (remove_img_height * 1.5);
|
|
||||||
|
|
||||||
// If Floating view comes under Remove View update Window Manager
|
|
||||||
if ((x_cord >= x_bound_left && x_cord <= x_bound_right) && y_cord >= y_bound_top) {
|
|
||||||
inBounded = true;
|
|
||||||
|
|
||||||
int x_cord_remove = (int) ((szWindow.x - (remove_img_height * 1.5)) / 2);
|
|
||||||
int y_cord_remove = (int) (szWindow.y - ((remove_img_width * 1.5) + getStatusBarHeight()));
|
|
||||||
|
|
||||||
if (remove_image_view.getLayoutParams().height == remove_img_height) {
|
|
||||||
WindowManager.LayoutParams param_remove = (WindowManager.LayoutParams) removeFloatingWidgetView.getLayoutParams();
|
|
||||||
param_remove.x = x_cord_remove;
|
|
||||||
param_remove.y = y_cord_remove;
|
|
||||||
|
|
||||||
mWindowManager.updateViewLayout(removeFloatingWidgetView, param_remove);
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutParams.x = x_cord_remove + (Math.abs(removeFloatingWidgetView.getWidth() - mFloatingWidgetView.getWidth())) / 2;
|
|
||||||
layoutParams.y = y_cord_remove + (Math.abs(removeFloatingWidgetView.getHeight() - mFloatingWidgetView.getHeight())) / 2;
|
|
||||||
|
|
||||||
// Update the layout with new X & Y coordinate
|
|
||||||
mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// If Floating window gets out of the Remove view update Remove view again
|
|
||||||
inBounded = false;
|
|
||||||
remove_image_view.getLayoutParams().height = remove_img_height;
|
|
||||||
remove_image_view.getLayoutParams().width = remove_img_width;
|
|
||||||
// onFloatingWidgetClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutParams.x = x_cord_Destination;
|
|
||||||
layoutParams.y = y_cord_Destination;
|
|
||||||
|
|
||||||
// Update the layout with new X & Y coordinate
|
|
||||||
mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
int id = v.getId();
|
|
||||||
if (id == R.id.close_bubble_head) {
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onFloatingWidgetLongClick() {
|
|
||||||
// Get remove Floating view params
|
|
||||||
WindowManager.LayoutParams removeParams = (WindowManager.LayoutParams) removeFloatingWidgetView.getLayoutParams();
|
|
||||||
|
|
||||||
// get x and y coordinates of remove view
|
|
||||||
int x_cord = (szWindow.x - removeFloatingWidgetView.getWidth()) / 2;
|
|
||||||
int y_cord = szWindow.y - (removeFloatingWidgetView.getHeight() + getStatusBarHeight());
|
|
||||||
|
|
||||||
removeParams.x = x_cord;
|
|
||||||
removeParams.y = y_cord;
|
|
||||||
|
|
||||||
// Update Remove view params
|
|
||||||
mWindowManager.updateViewLayout(removeFloatingWidgetView, removeParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset position of Floating Widget view on dragging
|
|
||||||
private void resetPosition(int x_cord_now) {
|
|
||||||
if (_continueToSnap) {
|
|
||||||
if (x_cord_now <= szWindow.x / 2) {
|
|
||||||
snapToLeft(x_cord_now);
|
|
||||||
} else {
|
|
||||||
snapToRight(x_cord_now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void snapToLeft(final int current_x_cord) {
|
|
||||||
final int x = szWindow.x - current_x_cord;
|
|
||||||
new CountDownTimer(500, 5) {
|
|
||||||
final WindowManager.LayoutParams mParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams();
|
|
||||||
|
|
||||||
public void onTick(long t) {
|
|
||||||
long step = (500 - t) / 5;
|
|
||||||
mParams.x = -(int) (current_x_cord * current_x_cord * step);
|
|
||||||
if (_bounce) {
|
|
||||||
mParams.x = -(int) (double) bounceValue(step, x);
|
|
||||||
}
|
|
||||||
mWindowManager.updateViewLayout(mFloatingWidgetView, mParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onFinish() {
|
|
||||||
mParams.x = 0;
|
|
||||||
mWindowManager.updateViewLayout(mFloatingWidgetView, mParams);
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void snapToRight(final int current_x_cord) {
|
|
||||||
new CountDownTimer(500, 5) {
|
|
||||||
final WindowManager.LayoutParams mParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams();
|
|
||||||
|
|
||||||
public void onTick(long t) {
|
|
||||||
long step = (500 - t) / 5;
|
|
||||||
mParams.x = (int) (szWindow.x + (current_x_cord * current_x_cord * step) - mFloatingWidgetView.getWidth());
|
|
||||||
if (_bounce) {
|
|
||||||
mParams.x = szWindow.x + (int) (double) bounceValue(step, current_x_cord) - mFloatingWidgetView.getWidth();
|
|
||||||
}
|
|
||||||
mWindowManager.updateViewLayout(mFloatingWidgetView, mParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onFinish() {
|
|
||||||
mParams.x = szWindow.x - mFloatingWidgetView.getWidth();
|
|
||||||
mWindowManager.updateViewLayout(mFloatingWidgetView, mParams);
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private double bounceValue(long step, long scale) {
|
|
||||||
return scale * Math.exp(-0.15 * step) * Math.cos(0.08 * step);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getStatusBarHeight() {
|
|
||||||
return (int) Math.ceil(25 * getApplicationContext().getResources().getDisplayMetrics().density);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
|
||||||
super.onConfigurationChanged(newConfig);
|
|
||||||
|
|
||||||
getWindowManagerDefaultDisplay();
|
|
||||||
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams();
|
|
||||||
|
|
||||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
if (layoutParams.y + (mFloatingWidgetView.getHeight() + getStatusBarHeight()) > szWindow.y) {
|
|
||||||
layoutParams.y = szWindow.y - (mFloatingWidgetView.getHeight() + getStatusBarHeight());
|
|
||||||
mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutParams.x != 0 && layoutParams.x < szWindow.x) {
|
|
||||||
resetPosition(szWindow.x);
|
|
||||||
}
|
|
||||||
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
if (layoutParams.x > szWindow.x) {
|
|
||||||
resetPosition(szWindow.x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onFloatingWidgetClick() {
|
|
||||||
_continueToSnap = false;
|
|
||||||
// bring the application to front
|
|
||||||
Intent it = new Intent("intent.bring.app.to.foreground");
|
|
||||||
it.setComponent(new ComponentName(getPackageName(), getApplicationContext().getPackageName() + ".MainActivity"));
|
|
||||||
it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
getApplicationContext().startActivity(it);
|
|
||||||
|
|
||||||
// stop the service
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
if (mFloatingWidgetView != null) {
|
|
||||||
mWindowManager.removeView(mFloatingWidgetView);
|
|
||||||
}
|
|
||||||
if (removeFloatingWidgetView != null) {
|
|
||||||
mWindowManager.removeView(removeFloatingWidgetView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package com.dsaved.bubblehead.bubble;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.Settings;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
|
||||||
import io.flutter.plugin.common.MethodCall;
|
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
|
||||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
|
||||||
import io.flutter.plugin.common.MethodChannel.Result;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BubblePlugin
|
|
||||||
*/
|
|
||||||
public class BubblePlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
|
|
||||||
/// The MethodChannel that will the communication between Flutter and native Android
|
|
||||||
///
|
|
||||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
|
||||||
/// when the Flutter Engine is detached from the Activity
|
|
||||||
private MethodChannel channel;
|
|
||||||
private Context activity;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
|
|
||||||
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "com.dsaved.bubble.head");
|
|
||||||
channel.setMethodCallHandler(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
@Override
|
|
||||||
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
|
|
||||||
if (call.method.equals("startBubbleHead")) {
|
|
||||||
startBubbleHead(result, call);
|
|
||||||
} else if (call.method.equals("stopBubbleHead")) {
|
|
||||||
BubbleHeadService.stopService(activity);
|
|
||||||
} else {
|
|
||||||
result.notImplemented();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
public void startBubbleHead(@NonNull Result result, MethodCall call) {
|
|
||||||
if (Settings.canDrawOverlays(activity)) {
|
|
||||||
boolean bounce = call.argument("bounce");
|
|
||||||
BubbleHeadService.bounce(bounce);
|
|
||||||
|
|
||||||
boolean showClose = call.argument("showClose");
|
|
||||||
BubbleHeadService.shouldShowCloseButton(showClose);
|
|
||||||
|
|
||||||
boolean dragToClose = call.argument("dragToClose");
|
|
||||||
BubbleHeadService.dragToClose(dragToClose);
|
|
||||||
|
|
||||||
boolean sendAppToBackground = call.argument("sendAppToBackground");
|
|
||||||
BubbleHeadService.sendAppToBackground(sendAppToBackground);
|
|
||||||
|
|
||||||
String imageByte = call.argument("image");
|
|
||||||
BubbleHeadService.startService(activity, imageByte);
|
|
||||||
} else {
|
|
||||||
//Permission is not available
|
|
||||||
result.error("EPERMNOTGRANTED", "permission not available", "Please request permission for: android.permission.SYSTEM_ALERT_WINDOW. with out this permission you cannot launch the bubble head.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
|
||||||
channel.setMethodCallHandler(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
|
|
||||||
this.activity = binding.getActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachedFromActivityForConfigChanges() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetachedFromActivity() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="@android:color/black" />
|
|
||||||
|
|
||||||
</shape>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24.0"
|
|
||||||
android:viewportWidth="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="@android:color/white" />
|
|
||||||
|
|
||||||
|
|
||||||
</shape>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/remove_relativelayout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="10dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/remove_img"
|
|
||||||
android:layout_width="60dp"
|
|
||||||
android:layout_height="60dp"
|
|
||||||
android:background="@drawable/white_circle_shape"
|
|
||||||
android:padding="10dp"
|
|
||||||
android:src="@drawable/ic_close_white_24dp" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<!-- Root container of Floating Widget View -->
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/root_container"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<!-- View while view is collapsed -->
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/collapse_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="visible">
|
|
||||||
|
|
||||||
<!-- ImageView of floating widget -->
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/chat_head_profile_iv"
|
|
||||||
android:layout_width="70dp"
|
|
||||||
android:layout_height="70dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:src="@drawable/ic_launcher"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
||||||
<!-- Close button to close Floating Widget View -->
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/close_bubble_head"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="50dp"
|
|
||||||
android:layout_marginTop="5dp"
|
|
||||||
android:padding="1dp"
|
|
||||||
android:background="@drawable/circle_shape"
|
|
||||||
android:src="@drawable/ic_close_white_24dp"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="ShapeAppearance.Image.PILL" parent="">
|
|
||||||
<item name="cornerFamily">rounded</item>
|
|
||||||
<item name="cornerSize">50%</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="ShapeAppearance.Image.Top.PILL" parent="">
|
|
||||||
<item name="cornerSizeTopLeft">6dp</item>
|
|
||||||
<item name="cornerFamilyTopLeft">rounded</item>
|
|
||||||
<item name="cornerSizeTopRight">6dp</item>
|
|
||||||
<item name="cornerFamilyTopRight">rounded</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
class Bubble {
|
|
||||||
static const MethodChannel _channel =
|
|
||||||
const MethodChannel('com.dsaved.bubble.head');
|
|
||||||
|
|
||||||
bool shouldBounce;
|
|
||||||
bool showCloseButton;
|
|
||||||
bool allowDragToClose;
|
|
||||||
|
|
||||||
Bubble({
|
|
||||||
this.shouldBounce = true,
|
|
||||||
this.allowDragToClose = true,
|
|
||||||
this.showCloseButton = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// puts app in background and shows floaty-bubble head
|
|
||||||
Future<void> startBubbleHead({bool sendAppToBackground = true}) async {
|
|
||||||
ByteData bytes = await rootBundle.load(
|
|
||||||
'assets/images/s.png',
|
|
||||||
);
|
|
||||||
var buffer = bytes.buffer;
|
|
||||||
var encodedImage = base64.encode(Uint8List.view(buffer));
|
|
||||||
await _channel.invokeMethod('startBubbleHead', {
|
|
||||||
"image": encodedImage,
|
|
||||||
"bounce": shouldBounce,
|
|
||||||
"showClose": showCloseButton,
|
|
||||||
"dragToClose": allowDragToClose,
|
|
||||||
"sendAppToBackground": sendAppToBackground,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// closes floaty-bubble head
|
|
||||||
Future<void> stopBubbleHead() async {
|
|
||||||
await _channel.invokeMethod('stopBubbleHead');
|
|
||||||
}
|
|
||||||
}
|
|
||||||