├── .gitignore ├── LICENSE ├── PRIVACY.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── digital │ │ └── ventral │ │ └── ips │ │ ├── BaseService.kt │ │ ├── ClientService.kt │ │ ├── ClientServiceStarter.kt │ │ ├── EncryptionUtils.kt │ │ ├── MainActivity.kt │ │ ├── ServerService.kt │ │ ├── SettingsActivity.kt │ │ └── ui │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ └── ic_launcher_monochrome.xml │ ├── layout │ └── settings_activity.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-ar │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-ko │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-zh │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ └── root_preferences.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── metadata ├── ar │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── de-DE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── en-US │ ├── changelogs │ │ └── 1.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.drawio │ │ ├── featureGraphic.jpg │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── screenshot-client-notifications.png │ │ │ ├── screenshot-main-activity.png │ │ │ ├── screenshot-server-notification.png │ │ │ ├── screenshot-settings-activity.png │ │ │ └── screenshot-share-from-gallery.png │ ├── short_description.txt │ └── title.txt ├── es-ES │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── fr-FR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── it-IT │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── ja-JP │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── ko-KR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── pt-PT │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── ru-RU │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt └── zh-CN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof 34 | 35 | *.iml 36 | .gradle 37 | /local.properties 38 | /.idea/caches 39 | /.idea/libraries 40 | /.idea/modules.xml 41 | /.idea/workspace.xml 42 | /.idea/navEditor.xml 43 | /.idea/assetWizardSettings.xml 44 | .DS_Store 45 | /build 46 | /captures 47 | .externalNativeBuild 48 | .cxx 49 | local.properties 50 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Inter Profile Sharing Privacy Policy 2 | 3 | Effective Date: 30. October 2024 4 | 5 | ## Introduction 6 | 7 | *Ventral Digital LLC* ("we", "us", "our") develops and maintains the *Inter Profile Sharing* mobile application (the "App"). This Privacy Policy informs you of our policies regarding the collection, use, and disclosure of personal data when you use our App and the choices you have associated with that data. 8 | 9 | ## Information We Collect 10 | 11 | We do not collect any personal information or usage data through the App. 12 | 13 | ## Third-Party Services 14 | 15 | While our App does not collect data, please be aware that platforms providing the App for download (such as Google Play Store, GitHub) may collect data during the browsing and installation process. Refer to their respective privacy policies for more information. Once installed, our App does not use an internet connection to send data anywhere. It does not contain any third-party tracking software or make use of any advertising providers. 16 | 17 | ## Use of Data 18 | 19 | Since we do not collect any personal data, we do not use your personal information in any way. Since we do not collect any personal data, many aspects of data protection regulations like GDPR and CCPA may not apply. However, we are committed to respecting user privacy and maintaining transparency about our data practices. 20 | 21 | ## Disclosure of Data 22 | 23 | We do not disclose any personal data to third parties on request because we do not collect any. 24 | 25 | Please note that sharing data locally is a primary function of our App. When you share items such as files, links, text, or clipboard contents, these items may become accessible to other applications or parts of the operating system on your device. We are not responsible for any unauthorized access or disclosure of data that occurs through other applications, the operating system, or due to malicious software on your device. We encourage you to use the encryption feature within our App to enhance the security of your shared data. 26 | 27 | ## Security of Data 28 | 29 | While our App does not collect personal data, we prioritize the security and integrity of the App to protect our users from unauthorized access, alteration, or misuse. We implement standard security practices to ensure the App functions safely on your device. These measures include 2 Factor Authentication on platforms where the source code and application binaries are hosted, avoiding the inclusion of experimental and unmaintained 3rd party libraries, the use of isolated development environments, the requirement of thorough code review before the addition or changes of the source code from 3rd party contributors. 30 | 31 | ## Children's Privacy 32 | 33 | Our App does not address anyone under the age of 13. We do not knowingly collect personal information from children under 13. If you are a parent or guardian and believe your child has provided us with personal data, please contact us, and we will take steps to remove that information. 34 | 35 | ## Your Rights 36 | 37 | Since we do not collect or process your personal data, you retain all rights to your personal information when using our App. 38 | 39 | ## Changes to This Privacy Policy 40 | 41 | We may update our Privacy Policy at any time to correct any grammar or wording issues. But we won't start collecting data with this App at any point in the future. Any changes will be posted on this page with an updated effective date. 42 | 43 | ## Contact 44 | 45 | ips-privacy@ventral.org 46 | Ventral Digital LLC 47 | 2880 W Oakland Park Blvd 48 | Suite 225C 49 | Oakland Park, FL 33311 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.compose) 5 | } 6 | 7 | android { 8 | namespace = "digital.ventral.ips" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | applicationId = "digital.ventral.ips" 13 | minSdk = 34 14 | targetSdk = 34 15 | versionCode = 1 16 | versionName = "1.0" 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | isMinifyEnabled = false 24 | vcsInfo.include = false 25 | proguardFiles( 26 | getDefaultProguardFile("proguard-android-optimize.txt"), 27 | "proguard-rules.pro" 28 | ) 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_11 33 | targetCompatibility = JavaVersion.VERSION_11 34 | } 35 | kotlinOptions { 36 | jvmTarget = "11" 37 | } 38 | buildFeatures { 39 | compose = true 40 | buildConfig = true 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation(libs.androidx.core.ktx) 46 | implementation(libs.androidx.lifecycle.runtime.ktx) 47 | implementation(libs.androidx.activity.compose) 48 | implementation(platform(libs.androidx.compose.bom)) 49 | implementation(libs.androidx.ui) 50 | implementation(libs.androidx.ui.graphics) 51 | implementation(libs.androidx.ui.tooling.preview) 52 | implementation(libs.androidx.material3) 53 | implementation(libs.androidx.appcompat) 54 | implementation(libs.androidx.preference) 55 | implementation(libs.material) 56 | implementation(libs.gson) 57 | implementation(libs.kotlinx.coroutines.android) 58 | implementation(libs.androidx.security.crypto) 59 | implementation(libs.google.tink.android) 60 | testImplementation(libs.junit) 61 | androidTestImplementation(libs.androidx.junit) 62 | androidTestImplementation(libs.androidx.espresso.core) 63 | androidTestImplementation(platform(libs.androidx.compose.bom)) 64 | androidTestImplementation(libs.androidx.ui.test.junit4) 65 | debugImplementation(libs.androidx.ui.tooling) 66 | debugImplementation(libs.androidx.ui.test.manifest) 67 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 51 | 55 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/BaseService.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips 2 | 3 | import android.app.NotificationChannel 4 | import android.app.NotificationManager 5 | import android.app.Service 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.os.IBinder 9 | import android.provider.OpenableColumns 10 | import android.provider.Settings 11 | import android.webkit.MimeTypeMap 12 | import androidx.preference.PreferenceManager 13 | import com.google.gson.Gson 14 | import java.net.InetAddress 15 | import java.net.InetSocketAddress 16 | import java.net.ServerSocket 17 | import kotlinx.coroutines.CoroutineScope 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.Job 20 | import java.net.Socket 21 | 22 | /** 23 | * Base Class holding things needed by both Server and Client Services. 24 | */ 25 | abstract class BaseService : Service() { 26 | internal var TAG = "BaseService" 27 | internal val serviceScope = CoroutineScope(Dispatchers.IO + Job()) 28 | 29 | companion object { 30 | internal const val DEFAULT_PORT = 2411 31 | internal const val CHANNEL_ID = "ServiceChannel" 32 | 33 | internal val gson = Gson() 34 | } 35 | 36 | data class SharedItem( 37 | val type: String, 38 | val timestamp: Long, 39 | // Fields for type FILE 40 | val uri: String? = null, 41 | val name: String? = null, 42 | val size: Long? = null, 43 | val mimeType: String? = null, 44 | // Fields for type TEXT 45 | val text: String? = null 46 | ) { 47 | companion object { 48 | const val TYPE_FILE = "FILE" 49 | const val TYPE_TEXT = "TEXT" 50 | } 51 | } 52 | 53 | data class ClientRequest( 54 | val action: String, 55 | // Fields for type SHARES_SINCE 56 | val timestamp: Long? = null, 57 | // Fields for type FETCH_FILE 58 | val uri: String? = null 59 | ) { 60 | companion object { 61 | const val ACTION_SHARES_SINCE = "SHARES_SINCE" 62 | const val ACTION_FETCH_FILE = "FETCH_FILE" 63 | const val ACTION_STOP_SHARING = "STOP_SHARING" 64 | } 65 | } 66 | 67 | fun onCreate(tag: String) { 68 | super.onCreate() 69 | 70 | // Adding ANDROID_ID (unique per Device+User+App) to logging TAG to be able to tell apart 71 | // logs of this App running within different Android User Profiles. 72 | val androidId: String = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) 73 | TAG = "${tag}[${androidId.take(2)}]" 74 | 75 | createNotificationChannel() 76 | } 77 | 78 | private fun createNotificationChannel() { 79 | try { 80 | val serviceChannel = NotificationChannel( 81 | CHANNEL_ID, 82 | getString(R.string.notifications_channel_name), 83 | // Silent notifications. Not only likely to be less annoying, but also prevents 84 | // Android's NotifAttentionHelper from muting us if we're being noisy. 85 | NotificationManager.IMPORTANCE_LOW 86 | ).apply { 87 | description = getString(R.string.notifications_channel_description) 88 | setShowBadge(true) 89 | } 90 | val manager = getSystemService(NotificationManager::class.java) 91 | manager.createNotificationChannel(serviceChannel) 92 | } catch (e: Exception) { 93 | android.util.Log.e(TAG, "Error creating notification channel", e) 94 | stopSelf() 95 | } 96 | } 97 | 98 | /** 99 | * A custom TCP Port can be configured within the SettingsActivity. 100 | */ 101 | internal fun getPort(): Int { 102 | val prefs = PreferenceManager.getDefaultSharedPreferences(this) 103 | val port = prefs.getString("port", DEFAULT_PORT.toString())?.toInt() ?: DEFAULT_PORT 104 | return port 105 | } 106 | 107 | /** 108 | * Whether encryption is currently turned on in the settings. 109 | */ 110 | internal fun useEncryption(): Boolean { 111 | val prefs = PreferenceManager.getDefaultSharedPreferences(this) 112 | return prefs.getBoolean("encryption", false) 113 | } 114 | 115 | internal fun hasNotificationPermission(): Boolean { 116 | return androidx.core.content.ContextCompat.checkSelfPermission( 117 | applicationContext, 118 | android.Manifest.permission.POST_NOTIFICATIONS 119 | ) == android.content.pm.PackageManager.PERMISSION_GRANTED 120 | } 121 | 122 | internal fun isPortAvailable(): Boolean { 123 | return try { 124 | val socket = ServerSocket() 125 | val loopbackAddress = InetAddress.getLoopbackAddress() 126 | socket.bind(InetSocketAddress(loopbackAddress, getPort())) 127 | socket.close() 128 | true 129 | } catch (e: Exception) { 130 | false 131 | } 132 | } 133 | 134 | /** 135 | * Sends a STOP_SHARING request telling the remote ServerService, presumably running in another 136 | * User Profile, to shut down and free the port. 137 | * 138 | * Should rightfully be part of ClientService but ended up being needed in ServerService, well. 139 | */ 140 | fun sendStopSharing() { 141 | try { 142 | Socket().use { socket -> 143 | val loopbackAddress = InetAddress.getLoopbackAddress() 144 | socket.connect(InetSocketAddress(loopbackAddress, getPort()), 2000) // 2 second timeout 145 | 146 | var outputStream = socket.getOutputStream() 147 | if (useEncryption()) { 148 | outputStream = EncryptionUtils.encryptStream(applicationContext, outputStream) 149 | } 150 | 151 | val writer = outputStream.bufferedWriter() 152 | val request = ClientRequest(action = ClientRequest.ACTION_STOP_SHARING) 153 | writer.write(gson.toJson(request)) 154 | writer.newLine() 155 | writer.flush() 156 | } 157 | } catch (e: Exception) { 158 | android.util.Log.e(TAG, "Error sending stop sharing request", e) 159 | } 160 | } 161 | 162 | internal fun getFileName(uri: Uri): String? { 163 | var name = uri.lastPathSegment 164 | contentResolver.query(uri, null, null, null, null)?.use { cursor -> 165 | if (cursor.moveToFirst()) { 166 | val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) 167 | if (nameIndex != -1) { 168 | name = cursor.getString(nameIndex) 169 | } 170 | } 171 | } 172 | return name 173 | } 174 | 175 | internal fun getFileSize(uri: Uri): Long? { 176 | var size: Long? = null 177 | contentResolver.query(uri, null, null, null, null)?.use { cursor -> 178 | if (cursor.moveToFirst()) { 179 | val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) 180 | if (sizeIndex != -1) { 181 | size = cursor.getLong(sizeIndex) 182 | } 183 | } 184 | } 185 | return size 186 | } 187 | 188 | internal fun getMimeType(fileName: String): String { 189 | return MimeTypeMap.getSingleton() 190 | .getMimeTypeFromExtension(fileName.substringAfterLast('.', "")) 191 | ?: "application/octet-stream" 192 | } 193 | 194 | internal fun getMimeType(uri: Uri): String? { 195 | return contentResolver.getType(uri) 196 | } 197 | 198 | override fun onBind(intent: Intent): IBinder? = null 199 | } -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/ClientServiceStarter.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | /** 8 | * Receives events to start the ClientService 9 | * 10 | * The ServerService is started only on-demand when there's something to share and isn't killed 11 | * by Android to save battery because it's a foreground service keeping itself alive with a sticky 12 | * notification. 13 | * 14 | * The ClientService on the other hand is a simple background service which should silently wait 15 | * for a ServerService to become available with new shared items. Ideally we'd want the service to 16 | * always be running in the background, waiting for relevant runtime-registered events (eg. user 17 | * switch) triggering a check. But that won't always be the case as Android has the tendency to 18 | * kill Services it deems unnecessarily consuming battery power. 19 | * 20 | * - ACTION_BOOT_COMPLETED 21 | * Broadcast once, after the user has finished booting. Requires a permission of the same name. 22 | * - ACTION_LOCKED_BOOT_COMPLETED 23 | * Broadcast once, after the user has finished booting, but while still in the "locked" state. 24 | * Also requires the ACTION_BOOT_COMPLETED permission. 25 | * - ACTION_DREAMING_STOPPED 26 | * Sent after the system stops "dreaming", ie. taken out of docking station where it showed 27 | * an interactive screensaver. 28 | * - ACTION_BATTERY_OKAY 29 | * Sent after battery is charged after being low. 30 | * - ACTION_POWER_CONNECTED 31 | * External power has been connected to the device. 32 | * - ACTION_POWER_DISCONNECTED 33 | * External power has been removed from the device. 34 | * - ACTION_MY_PACKAGE_REPLACED 35 | * New version of this application has been installed over an existing one. 36 | * - ACTION_MY_PACKAGE_UNSUSPENDED 37 | * This application is no longer suspended and may create notifications again. 38 | */ 39 | class ClientServiceStarter : BroadcastReceiver() { 40 | override fun onReceive(context: Context, intent: Intent) { 41 | when (intent.action) { 42 | Intent.ACTION_BOOT_COMPLETED, 43 | Intent.ACTION_LOCKED_BOOT_COMPLETED, 44 | Intent.ACTION_DREAMING_STOPPED, 45 | Intent.ACTION_BATTERY_OKAY, 46 | Intent.ACTION_POWER_CONNECTED, 47 | Intent.ACTION_POWER_DISCONNECTED, 48 | Intent.ACTION_MY_PACKAGE_REPLACED, 49 | Intent.ACTION_MY_PACKAGE_UNSUSPENDED -> { 50 | android.util.Log.d("ClientServiceStarter", "Manifest registered broadcast of ${intent.action} received, start ClientService") 51 | context.startService(Intent(context, ClientService::class.java)) 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/EncryptionUtils.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips 2 | 3 | import android.content.Context 4 | import android.util.Base64 5 | import androidx.security.crypto.EncryptedSharedPreferences 6 | import androidx.security.crypto.MasterKeys 7 | import com.google.crypto.tink.subtle.AesGcmJce 8 | import java.security.MessageDigest 9 | import java.security.SecureRandom 10 | import java.io.* 11 | import java.nio.* 12 | 13 | object EncryptionUtils { 14 | private const val DOMAIN_SEPARATOR = "|digital.ventral.ips|SharedPassword" 15 | private const val PREFS_NAME = "encryption_prefs" 16 | private const val KEY_DERIVED_KEY = "derived_key" 17 | private const val TAG = "EncryptionUtils" 18 | private const val HEADER_SIZE_LENGTH = 4 19 | private const val HEADER_IV_LENGTH = 12 20 | private const val HEADER_SIZE = HEADER_SIZE_LENGTH + HEADER_IV_LENGTH 21 | private const val BODY_SIZE_LIMIT = 100*1024*1024 22 | 23 | /** 24 | * Derives an AES key from the provided password using a deterministic approach. 25 | * 26 | * Uses SHA-256 double hashing with domain separator appended to the first hash. 27 | */ 28 | private fun deriveKey(password: String): ByteArray { 29 | val hash1 = MessageDigest.getInstance("SHA-256").digest(password.toByteArray()) 30 | val hash2 = MessageDigest.getInstance("SHA-256").digest(hash1 + DOMAIN_SEPARATOR.toByteArray()) 31 | return hash2 32 | } 33 | 34 | /** 35 | * Gets or creates encrypted shared preferences instance. 36 | */ 37 | private fun getEncryptedPrefs(context: Context) = try { 38 | val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) 39 | EncryptedSharedPreferences.create( 40 | PREFS_NAME, 41 | masterKeyAlias, 42 | context, 43 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, 44 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM 45 | ) 46 | } catch (e: Exception) { 47 | android.util.Log.e(TAG, "Error creating EncryptedSharedPreferences", e) 48 | throw e 49 | } 50 | 51 | /** 52 | * Updates the encryption key based on the password and stores it securely. 53 | */ 54 | fun updateEncryptionKey(context: Context, newPassword: String): Boolean { 55 | return try { 56 | val encryptedPrefs = getEncryptedPrefs(context) 57 | val key = deriveKey(newPassword) 58 | encryptedPrefs.edit() 59 | .putString(KEY_DERIVED_KEY, Base64.encodeToString(key, Base64.NO_WRAP)) 60 | .apply() 61 | true 62 | } catch (e: Exception) { 63 | android.util.Log.e(TAG, "Error updating encryption key", e) 64 | false 65 | } 66 | } 67 | 68 | fun hasEncryptionKey(context: Context): Boolean { 69 | return try { 70 | val encryptedPrefs = getEncryptedPrefs(context) 71 | val keyStr = encryptedPrefs.getString(KEY_DERIVED_KEY, null) 72 | return keyStr != null 73 | } catch (e: Exception) { 74 | android.util.Log.e(TAG, "Error updating encryption key", e) 75 | false 76 | } 77 | } 78 | 79 | /** 80 | * Retrieves the stored encryption key from encrypted storage. 81 | */ 82 | fun getStoredKey(context: Context): ByteArray { 83 | val encryptedPrefs = getEncryptedPrefs(context) 84 | val keyStr = encryptedPrefs.getString(KEY_DERIVED_KEY, null) 85 | ?: throw IllegalStateException("No encryption key found") 86 | return Base64.decode(keyStr, Base64.NO_WRAP) 87 | } 88 | 89 | /** 90 | * Wraps the given output stream such that every time write() is called on the returned 91 | * encrypting output stream, we encrypt the data before passing it on. 92 | */ 93 | fun encryptStream(context: Context, stream: OutputStream): OutputStream { 94 | return EncryptingOutputStream(stream, getStoredKey(context)) 95 | } 96 | 97 | /** 98 | * Wraps the given input stream such that every time read() is called on the returned 99 | * decrypting input stream, we decrypt the data coming in before returning it. 100 | */ 101 | fun decryptStream(context: Context, stream: InputStream): InputStream { 102 | return DecryptingInputStream(stream, getStoredKey(context)) 103 | } 104 | 105 | /** 106 | * Encrypts stream data before passing it on to the given output. 107 | * 108 | * The passed cleartext is encrypted and its ciphertext becomes the message body, while 109 | * the ciphertext size and the AES initialization vector become part of the header. 110 | * 111 | * With every write the contents streamed will be in the form [HEADER][BODY], or rather 112 | * [[SIZE][IV]][CIPHERTEXT] which we'll call a message. 113 | */ 114 | private class EncryptingOutputStream(out: OutputStream, private val key: ByteArray) : FilterOutputStream(out) { 115 | private val aead = AesGcmJce(key) 116 | override fun write(data: ByteArray, off: Int, len: Int) { 117 | val cleartext = data.copyOfRange(off, off + len) 118 | // Encrypt. 119 | val iv = ByteArray(HEADER_IV_LENGTH).apply { SecureRandom().nextBytes(this) } // random initialization vector 120 | val ciphertext = aead.encrypt(cleartext, iv) // auth tag is appended at end of ciphertext 121 | // Send ciphertext as message body, ciphertext size and iv as message header. 122 | val message = ByteArrayOutputStream() 123 | message.write(ByteBuffer.allocate(HEADER_SIZE_LENGTH).order(ByteOrder.BIG_ENDIAN).putInt(ciphertext.size).array()) 124 | message.write(iv) 125 | message.write(ciphertext) 126 | out.write(message.toByteArray()) 127 | } 128 | } 129 | 130 | /** 131 | * Decrypts received data before passing it back to the reader. 132 | * 133 | * There's no guarantee that we actually receive a single, full encrypted message with 134 | * each chunk of data we read from the input stream. It could be any of these variations: 135 | * 136 | * [HEAD 137 | * [HEAD] 138 | * [HEAD][BO 139 | * [HEAD][BODY] 140 | * [HEAD][BODY][HE 141 | * ... etc 142 | * 143 | * This is why decrypting the stream requires more careful handling, and buffers. 144 | */ 145 | private class DecryptingInputStream(input: InputStream, private val key: ByteArray) : FilterInputStream(input) { 146 | private val aead = AesGcmJce(key) 147 | private var headerBytesRead = 0 148 | private var headerBuffer = ByteArray(HEADER_SIZE) 149 | private var expectedBodySize = -1 150 | private var iv: ByteArray? = null 151 | private var bodyBytesRead = 0 152 | private var bodyBuffer: ByteArray? = null 153 | private var cleartext: ByteArray? = null 154 | private var cleartextRead = 0 155 | 156 | override fun read(data: ByteArray, off: Int, len: Int): Int { 157 | if (len <= 0) return 0 158 | // According to this function's description we MUST return at least one byte. So we'll 159 | // have to keep blocking until we've read enough to decrypt. 160 | while (true) { 161 | // Read some more header if it's still incomplete. 162 | if (headerBytesRead < HEADER_SIZE) { 163 | val count = super.read(headerBuffer, headerBytesRead, HEADER_SIZE - headerBytesRead) 164 | if (count <= 0) return -1 165 | headerBytesRead += count 166 | // Header now complete? Parse it. 167 | if (headerBytesRead == HEADER_SIZE) { 168 | expectedBodySize = ByteBuffer.wrap(headerBuffer.copyOfRange(0, HEADER_SIZE_LENGTH)).order(ByteOrder.BIG_ENDIAN).int 169 | // If App we're talking to is not configured for encryption, we'll end up 170 | // interpreting JSON encoded rubbish as a valid header here. Abort early. 171 | if (expectedBodySize > BODY_SIZE_LIMIT) return -1 172 | bodyBuffer = ByteArray(expectedBodySize) 173 | iv = headerBuffer.copyOfRange(HEADER_SIZE_LENGTH, HEADER_SIZE_LENGTH + HEADER_IV_LENGTH) 174 | } 175 | } 176 | // Read some more body if it's still incomplete. 177 | if (bodyBytesRead < expectedBodySize) { 178 | val count = super.read(bodyBuffer, bodyBytesRead, expectedBodySize - bodyBytesRead) 179 | if (count <= 0) return -1 180 | bodyBytesRead += count 181 | // Body now complete? Decrypt. 182 | if (bodyBytesRead == expectedBodySize) { 183 | cleartext = aead.decrypt(bodyBuffer, iv) 184 | } 185 | } 186 | // We have the cleartext, but it hasn't been fully read by the caller yet. 187 | if (cleartext != null) { 188 | val count = minOf(len, cleartext!!.size) 189 | System.arraycopy(cleartext!!, cleartextRead, data, off, count) 190 | cleartextRead += count 191 | // Cleartext now completely read? Reset. 192 | if (cleartextRead == cleartext!!.size) { 193 | headerBytesRead = 0 194 | expectedBodySize = -1 195 | bodyBytesRead = 0 196 | bodyBuffer = null 197 | cleartext = null 198 | cleartextRead = 0 199 | } 200 | return count 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips 2 | 3 | import android.Manifest 4 | import android.content.pm.PackageManager 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.content.ClipData 8 | import android.content.ClipboardManager 9 | import android.net.Uri 10 | import android.widget.Toast 11 | import androidx.activity.ComponentActivity 12 | import androidx.activity.compose.setContent 13 | import androidx.activity.result.contract.ActivityResultContracts 14 | import androidx.activity.enableEdgeToEdge 15 | import androidx.compose.foundation.layout.* 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.filled.Settings 18 | import androidx.compose.material3.* 19 | import androidx.compose.runtime.* 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import androidx.compose.ui.res.stringResource 25 | import digital.ventral.ips.ui.theme.InterProfileSharingTheme 26 | 27 | 28 | @OptIn(ExperimentalMaterial3Api::class) 29 | class MainActivity : ComponentActivity() { 30 | 31 | /** 32 | * Invoke ServerService to share files between Android Profiles. 33 | * 34 | * - Called when picking files via "Share Files" button in the App 35 | * - Called when files are forwarded from another App (eg. Share image via Gallery) 36 | */ 37 | private fun handleShareFiles(uris: List) { 38 | uris.forEach { uri -> 39 | try { 40 | // In some cases the permission this activity received for handling a file for 41 | // sharing needs to be explicitly granted to other components of the App. 42 | grantUriPermission("digital.ventral.ips", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) 43 | val serviceIntent = Intent(this, ServerService::class.java) 44 | serviceIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 45 | serviceIntent.putExtra(ServerService.EXTRA_URI, uri) 46 | startForegroundService(serviceIntent) 47 | } catch (e: Exception) { 48 | android.util.Log.e("MainActivity", "Error starting ServerService", e) 49 | showToast("Error: ${e.message}") 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Invoke ServerService to share plain text between Android Profiles. 56 | * 57 | * - Called when clipboard contents are shared via button in the App 58 | * - Called when text is forwarded from another App (eg. Share URL via Browser) 59 | */ 60 | private fun handleShareText(text: String?) { 61 | try { 62 | val serviceIntent = Intent(this, ServerService::class.java) 63 | serviceIntent.putExtra(ServerService.EXTRA_TEXT, text) 64 | startForegroundService(serviceIntent) 65 | } catch (e: Exception) { 66 | android.util.Log.e("MainActivity", "Error starting ServerService", e) 67 | showToast("Error: ${e.message}") 68 | } 69 | } 70 | 71 | /** 72 | * Handle Intents, specifically other Apps sharing data with us. 73 | * 74 | * - Could be a single media file shared from some Messenger. 75 | * - Could be multiple images shared from a Gallery App. 76 | * - Could be a URL shared from an Internet Browser. 77 | * 78 | * If such an intent invoked the MainActivity, close it once done, nothing more to do here. 79 | */ 80 | private fun handleIntent(intent: Intent) { 81 | when (intent.action) { 82 | // A single item has been shared with this App. 83 | Intent.ACTION_SEND -> { 84 | val type = intent.type 85 | val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) 86 | // Handle links or some other shorter texts. 87 | if (type == "text/plain" && sharedText != null) { 88 | val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) 89 | sharedText?.let { handleShareText(it) } 90 | } 91 | // Handle single files. 92 | else { 93 | val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) 94 | uri?.let { handleShareFiles(listOf(it)) } 95 | } 96 | finish() 97 | } 98 | // Multiple files have been shared with this App. 99 | Intent.ACTION_SEND_MULTIPLE -> { 100 | val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) 101 | uris?.let { handleShareFiles(it) } 102 | finish() 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Called when the "Share Files" Button is tapped. 109 | */ 110 | private fun onShareFilesClick() { 111 | filePickerLauncher.launch( 112 | arrayOf("*/*") // Allow all file types. 113 | ) 114 | } 115 | 116 | /** 117 | * Callback after File Picker finished. 118 | */ 119 | private val filePickerLauncher = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris: List -> 120 | if (uris.isNotEmpty()) { 121 | handleShareFiles(uris) 122 | } else { 123 | showToast(getString(R.string.message_files_none)) 124 | } 125 | } 126 | 127 | /** 128 | * Called when the "Share Copied Text" Button is tapped. 129 | */ 130 | private fun onShareClipboardClick() { 131 | val clipboard = getSystemService(ClipboardManager::class.java) 132 | val clipData: ClipData? = clipboard.primaryClip 133 | if (clipData != null && clipData.itemCount > 0) { 134 | val text = clipData.getItemAt(0).text?.toString() 135 | if (!text.isNullOrEmpty()) { 136 | handleShareText(text) 137 | } else { 138 | showToast(getString(R.string.message_clipboard_empty)) 139 | } 140 | } else { 141 | showToast(getString(R.string.message_clipboard_empty)) 142 | } 143 | } 144 | 145 | /** 146 | * When App is opened, check for Notification Permission. 147 | * 148 | * To prevent the ServerService from being killed in the background, it needs to run as a 149 | * ForegroundService with a "sticky" notification - which requires the permission. 150 | * 151 | * Posting Notifications is considered a Dangerous Permission, that's why we have to request 152 | * it during runtime in addition to mentioning it within the AndroidManifest. 153 | */ 154 | private fun checkNotificationPermission() { 155 | when { 156 | checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED -> { 157 | // Permission is already granted. 158 | } 159 | shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { 160 | // Explain why the app needs this permission. 161 | showToast(getString(R.string.message_notification_required)) 162 | requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 163 | } 164 | else -> { 165 | // Directly ask for the permission. 166 | requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 167 | } 168 | } 169 | } 170 | 171 | /** 172 | * Callback after Permission Request finished. 173 | */ 174 | private val requestPermissionLauncher = registerForActivityResult( 175 | ActivityResultContracts.RequestPermission() 176 | ) { isGranted: Boolean -> 177 | if (isGranted) { 178 | // Now that we were granted the notification permission, let the ClientService check 179 | // whether there's new items to create notifications for. 180 | startService(Intent(this, ClientService::class.java)) 181 | } 182 | else { 183 | // Permission denied. Explain necessity. 184 | showToast(getString(R.string.message_notification_required)) 185 | } 186 | } 187 | 188 | /** 189 | * Triggered when MainActivity UI comes into focus. 190 | * 191 | * This happens after onCreate(), when coming back from the SettingsActivity or simply when 192 | * the user comes back from looking at some other App. We use this opportunity to start the 193 | * ClientService (in case it was killed by Android to save battery) 194 | */ 195 | override fun onResume() { 196 | super.onResume() 197 | startService(Intent(this, ClientService::class.java)) 198 | } 199 | 200 | /** 201 | * Mostly boring UI stuff from this point onwards. 202 | */ 203 | override fun onCreate(savedInstanceState: Bundle?) { 204 | super.onCreate(savedInstanceState) 205 | checkNotificationPermission() 206 | enableEdgeToEdge() 207 | intent?.let { handleIntent(it) } 208 | 209 | setContent { 210 | InterProfileSharingTheme { 211 | Scaffold( 212 | // Top Bar with Title and Settings link. 213 | topBar = { 214 | TopAppBar( 215 | title = { 216 | Text(text = stringResource(id = R.string.main_topbar_title)) 217 | }, 218 | actions = { 219 | IconButton(onClick = { 220 | // Start SettingsActivity when the Settings link is tapped. 221 | val intent = Intent(this@MainActivity, SettingsActivity::class.java) 222 | startActivity(intent) 223 | }) { 224 | Icon( 225 | imageVector = Icons.Default.Settings, 226 | contentDescription = stringResource(id = R.string.main_topbar_settings) 227 | ) 228 | } 229 | } 230 | ) 231 | }, 232 | modifier = Modifier.fillMaxSize() 233 | ) { innerPadding -> 234 | // Content below Top Bar. 235 | ContentColumn( 236 | modifier = Modifier.padding(innerPadding), 237 | onShareFilesClick = { onShareFilesClick() }, 238 | onShareClipboardClick = { onShareClipboardClick() } 239 | ) 240 | } 241 | } 242 | } 243 | } 244 | 245 | /** 246 | * Helper function for displaying short messages at the bottom of the screen. 247 | */ 248 | private fun showToast(message: String) { 249 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 250 | } 251 | } 252 | 253 | /** 254 | * Everything below builds the UI located below the title top bar. 255 | */ 256 | @Composable 257 | fun ContentColumn(modifier: Modifier = Modifier, onShareFilesClick: () -> Unit, onShareClipboardClick: () -> Unit) { 258 | Column( 259 | modifier = modifier 260 | .fillMaxSize() 261 | .padding(16.dp) 262 | ) { 263 | // Explanatory text. 264 | Text( 265 | text = stringResource(R.string.main_text_1), 266 | fontSize = 16.sp, 267 | modifier = Modifier.padding(bottom = 8.dp) 268 | ) 269 | Text( 270 | text = stringResource(R.string.main_text_2), 271 | fontSize = 16.sp, 272 | modifier = Modifier.padding(bottom = 8.dp) 273 | ) 274 | Text( 275 | text = stringResource(R.string.main_text_3), 276 | fontSize = 16.sp, 277 | modifier = Modifier.padding(bottom = 16.dp) 278 | ) 279 | // Large Share Files / Copied Text Buttons below explanatory text. 280 | ButtonColumn( 281 | modifier = Modifier 282 | .fillMaxHeight() 283 | .weight(1f), 284 | onShareFilesClick = onShareFilesClick, 285 | onShareClipboardClick = onShareClipboardClick 286 | ) 287 | } 288 | } 289 | 290 | @Composable 291 | fun ButtonColumn(modifier: Modifier = Modifier, onShareFilesClick: () -> Unit, onShareClipboardClick: () -> Unit) { 292 | Column( 293 | modifier = modifier 294 | .fillMaxSize() 295 | .padding(16.dp) 296 | ) { 297 | // Note: Weight modifiers make sure the buttons cover all of the remaining space on the screen. 298 | LargeButton( 299 | title = stringResource(R.string.main_button_share_files_title), 300 | description = stringResource(R.string.main_button_share_files_description), 301 | onClick = onShareFilesClick, 302 | modifier = Modifier.weight(1f) 303 | ) 304 | Spacer(modifier = Modifier.height(16.dp)) 305 | LargeButton( 306 | title = stringResource(R.string.main_button_share_text_title), 307 | description = stringResource(R.string.main_button_share_text_description), 308 | onClick = onShareClipboardClick, 309 | modifier = Modifier.weight(1f) 310 | ) 311 | } 312 | } 313 | 314 | @Composable 315 | fun LargeButton(title: String, description: String, onClick: () -> Unit, modifier: Modifier = Modifier) { 316 | Button( 317 | onClick = onClick, 318 | modifier = modifier 319 | .fillMaxWidth() 320 | .padding(8.dp) 321 | ) { 322 | Column( 323 | modifier = Modifier.fillMaxWidth(), 324 | verticalArrangement = Arrangement.Center 325 | ) { 326 | Text( 327 | text = title, 328 | fontSize = 18.sp, 329 | fontWeight = FontWeight.Bold, 330 | modifier = Modifier.fillMaxWidth(), 331 | maxLines = 1 332 | ) 333 | Text( 334 | text = description, 335 | fontSize = 14.sp, 336 | modifier = Modifier.fillMaxWidth() 337 | ) 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/ServerService.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips 2 | 3 | import android.app.Notification 4 | import android.app.PendingIntent 5 | import android.content.Intent 6 | import android.net.Uri 7 | import androidx.core.app.NotificationCompat 8 | import java.net.InetAddress 9 | import java.net.ServerSocket 10 | import java.net.Socket 11 | import kotlinx.coroutines.* 12 | 13 | class ServerService : BaseService() { 14 | 15 | private var serverSocket: ServerSocket? = null 16 | // List of items that are currently shared by the server. 17 | private var sharingList = mutableListOf() 18 | 19 | companion object { 20 | private const val LOGGING_TAG = "ServerService" 21 | private const val NOTIFICATION_ID = 1 22 | const val EXTRA_URI = "digital.ventral.ips.extra.URI" 23 | const val EXTRA_TEXT = "digital.ventral.ips.extra.TEXT" 24 | 25 | // Allows others to easily check whether the ServerService is running in the current 26 | // profile. (eg. if this profile is sharing something, don't show notifications for it) 27 | @Volatile 28 | var isRunning = false 29 | private set 30 | } 31 | 32 | override fun onCreate() { 33 | super.onCreate(LOGGING_TAG) 34 | isRunning = true 35 | ensurePortAvailable() 36 | startServer() 37 | } 38 | 39 | override fun onDestroy() { 40 | stopServer() 41 | isRunning = false 42 | sharingList.clear() 43 | super.onDestroy() 44 | stopForeground(STOP_FOREGROUND_REMOVE) 45 | } 46 | 47 | /** 48 | * If the configured port isn't available, it's most likely used by another ServerService 49 | * instance running within another User Profile. Tell it to stop sharing to free the port. 50 | */ 51 | private fun ensurePortAvailable() { 52 | runBlocking(Dispatchers.IO) { 53 | if (!isPortAvailable()) { 54 | runBlocking { 55 | sendStopSharing() 56 | delay(500) // Give it some time to shut down. 57 | } 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * We're serving shared items via a socket on the local loopback interface (127.0.0.1). 64 | * 65 | * This is sufficient since we only intend to share with other instances of this App within 66 | * other User Profiles. While this doesn't prevent other, potentially malicious, Apps from 67 | * connecting to us, it should at least prevent connections from an external network. 68 | * 69 | * We'll optionally use encryption to protect malicious local applications from interacting 70 | * with the server or intercepting inter-profile communications. 71 | */ 72 | private fun startServer() { 73 | try { 74 | val port = getPort() 75 | val loopbackAddress = InetAddress.getLoopbackAddress() 76 | serverSocket = ServerSocket(port, 0, loopbackAddress).also { server -> 77 | android.util.Log.d(TAG, "Server started on port $port") 78 | 79 | // Start accepting connections in a coroutine. 80 | serviceScope.launch { 81 | while (isActive) { 82 | try { 83 | val client = server.accept() 84 | handleClient(client) 85 | } catch (e: Exception) { 86 | // Log error only if it's not due to normal socket closure. 87 | if (e !is java.net.SocketException || serverSocket != null) { 88 | android.util.Log.e(TAG, "Error accepting client", e) 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } catch (e: Exception) { 95 | android.util.Log.e(TAG, "Error starting server", e) 96 | stopSelf() 97 | } 98 | } 99 | 100 | private fun stopServer() { 101 | try { 102 | serverSocket?.close() 103 | serverSocket = null 104 | serviceScope.cancel() // Cancel all coroutines. 105 | android.util.Log.d(TAG, "Server stopped") 106 | } catch (e: Exception) { 107 | android.util.Log.e(TAG, "Error stopping server", e) 108 | } 109 | } 110 | 111 | /** 112 | * Called after Server accepted a connection. 113 | * 114 | * Could've used an HTTP Server here, but that seemed overkill. Instead we're using a simple 115 | * Client-Server pattern where the ClientRequest is a JSON encoded object containing the 116 | * desired action. 117 | * 118 | * For simple message framing we use newlines at the end of each JSON encoded object. The 119 | * exception to this is when we're transferring raw file data, as the binary data could contain 120 | * newlines we instead rely on the fact that the client will have knowledge of the file size. 121 | * 122 | * Available actions are: 123 | * - SHARES_SINCE 124 | * Server returns a JSON encoded list of currently shared items which are newer than the 125 | * specified timestamp. Used by the Client to check for newly shared items. 126 | * - FETCH_FILE 127 | * Server returns the raw data of the file located at the specified uri. No newline framing 128 | * but Client should be aware of file size as it is part of SHARES_SINCE response. 129 | * - STOP_SHARING 130 | * Causes the ServerService to shutdown, making the port available to use for another User 131 | * Profile desiring to share items. 132 | * 133 | */ 134 | private fun handleClient(client: Socket) { 135 | serviceScope.launch { 136 | try { 137 | var inputStream = client.getInputStream() 138 | var outputStream = client.getOutputStream() 139 | if (useEncryption()) { 140 | outputStream = EncryptionUtils.encryptStream(applicationContext, outputStream) 141 | inputStream = EncryptionUtils.decryptStream(applicationContext, inputStream) 142 | } 143 | val reader = inputStream.bufferedReader() 144 | val writer = outputStream.bufferedWriter() 145 | 146 | val requestJson = reader.readLine() 147 | val request = gson.fromJson(requestJson, ClientRequest::class.java) 148 | 149 | when (request?.action) { 150 | 151 | ClientRequest.ACTION_SHARES_SINCE -> { 152 | val timestamp = request.timestamp ?: 0 153 | val shares = sharingList.filter { it.timestamp > timestamp } 154 | val response = gson.toJson(shares) 155 | writer.write(response) 156 | writer.newLine() 157 | writer.flush() 158 | } 159 | 160 | ClientRequest.ACTION_FETCH_FILE -> { 161 | val requestedUri = request.uri 162 | if (requestedUri != null) { 163 | // Find the shared item, to make sure the specified uri is actually being shared. 164 | val sharedItem = sharingList.find { 165 | it.type == SharedItem.TYPE_FILE && it.uri == requestedUri 166 | } 167 | if (sharedItem != null) { 168 | val uri = Uri.parse(sharedItem.uri) 169 | val contentResolver = applicationContext.contentResolver 170 | contentResolver.openInputStream(uri)?.use { contentStream -> 171 | contentStream.buffered().copyTo(outputStream) 172 | outputStream.flush() 173 | } ?: throw Exception("Could not open input stream for URI") 174 | } 175 | } 176 | } 177 | 178 | ClientRequest.ACTION_STOP_SHARING -> { 179 | android.util.Log.d(TAG, "Stop sharing requested via TCP") 180 | client.close() 181 | stopSelf() 182 | } 183 | 184 | else -> { 185 | android.util.Log.w(TAG, "Unknown action: ${request?.action}") 186 | } 187 | } 188 | 189 | } catch (e: Exception) { 190 | android.util.Log.e(TAG, "Error handling client", e) 191 | } finally { 192 | try { 193 | client.close() 194 | } catch (e: Exception) { 195 | android.util.Log.e(TAG, "Error closing client socket", e) 196 | } 197 | } 198 | } 199 | } 200 | 201 | /** 202 | * Invoked via MainActivity for each new item to share. 203 | * 204 | * Called with EXTRA_URI 205 | * - when picking files via "Share Files" button in the App's MainActivity 206 | * - when files are forwarded from another App (eg. Share image via Gallery) 207 | * Called with EXTRA_TEXT 208 | * - when clipboard contents are shared via button in the App's MainActivity 209 | * - when text is forwarded from another App (eg. Share URL via Browser) 210 | */ 211 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 212 | try { 213 | // Handle taps on the notification's "Stop Sharing" button. 214 | if (intent?.action == "STOP_SERVICE") { 215 | stopSelf() 216 | return START_NOT_STICKY 217 | } 218 | 219 | // All other intents should be requests to share more items. 220 | val uri = intent?.getParcelableExtra(EXTRA_URI, Uri::class.java) 221 | val text = intent?.getStringExtra(EXTRA_TEXT) 222 | when { 223 | uri != null -> { 224 | addFileItem(uri) 225 | } 226 | text != null -> { 227 | addTextItem(text) 228 | } 229 | else -> { 230 | android.util.Log.w(TAG, "No URI or text found in intent") 231 | } 232 | } 233 | 234 | // We must call startForeground within 5 seconds of service start. 235 | startForeground(NOTIFICATION_ID, createNotification()) 236 | 237 | } catch (e: Exception) { 238 | android.util.Log.e(TAG, "Error in onStartCommand", e) 239 | } 240 | return START_STICKY 241 | } 242 | 243 | /** 244 | * Adds a FILE to the list of currently shared items. 245 | */ 246 | private fun addFileItem(uri: Uri) { 247 | try { 248 | // Remove existing item with same uri to avoid duplicates. 249 | sharingList.removeAll { 250 | it.type == SharedItem.TYPE_FILE && it.uri == uri.toString() 251 | } 252 | val sharedItem = SharedItem( 253 | type = SharedItem.TYPE_FILE, 254 | uri = uri.toString(), 255 | name = getFileName(uri), 256 | size = getFileSize(uri), 257 | mimeType = getMimeType(uri), 258 | timestamp = System.currentTimeMillis() 259 | ) 260 | sharingList.add(sharedItem) 261 | } catch (e: Exception) { 262 | android.util.Log.e(TAG, "Error adding FILE item", e) 263 | } 264 | } 265 | 266 | /** 267 | * Adds a TEXT to the list of currently shared items. 268 | */ 269 | private fun addTextItem(text: String) { 270 | // Remove existing item with same text to avoid duplicates. 271 | sharingList.removeAll { 272 | it.type == SharedItem.TYPE_TEXT && it.text == text 273 | } 274 | val sharedItem = SharedItem( 275 | type = SharedItem.TYPE_TEXT, 276 | text = text, 277 | timestamp = System.currentTimeMillis() 278 | ) 279 | sharingList.add(sharedItem) 280 | } 281 | 282 | /** 283 | * To prevent Android from killing our Server while sharing to conserve battery, we run it as 284 | * a "Foreground Service" which requires a notification being displayed while its running. 285 | * 286 | * This notification will inform the user that items are currently being actively shared with 287 | * other profiles, and gives him control to stop sharing them via the "Stop Sharing" button 288 | * below the notification. 289 | */ 290 | private fun createNotification(): Notification { 291 | val stopIntent = Intent(this, ServerService::class.java).apply { 292 | action = "STOP_SERVICE" 293 | } 294 | val stopPendingIntent = PendingIntent.getService( 295 | this, 296 | 0, 297 | stopIntent, 298 | PendingIntent.FLAG_IMMUTABLE 299 | ) 300 | return NotificationCompat.Builder(this, CHANNEL_ID) 301 | .setContentTitle(getString(R.string.notifications_server_title)) 302 | .setContentText(getNotificationText()) 303 | .setSmallIcon(R.drawable.ic_launcher_foreground) 304 | .setOngoing(true) 305 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 306 | .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notifications_server_action_stop), stopPendingIntent) 307 | .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) 308 | .build() 309 | } 310 | 311 | /** 312 | * Generates an informative text about what is being shared depending on the current item list. 313 | */ 314 | private fun getNotificationText(): String { 315 | return when { 316 | // Case: Not sharing anything (This shouldn't happen). 317 | sharingList.isEmpty() -> { 318 | getString(R.string.notifications_server_description_ready) 319 | } 320 | sharingList.size == 1 -> { 321 | val item = sharingList[0] 322 | when (item.type) { 323 | // Case: Sharing one Text item. 324 | SharedItem.TYPE_TEXT -> { 325 | // NotificationCompat handles truncation of long text. 326 | getString(R.string.notifications_server_description_text, item.text) 327 | } 328 | // Case: Sharing one File item. 329 | SharedItem.TYPE_FILE -> { 330 | val size = item.size?.let { 331 | android.text.format.Formatter.formatShortFileSize(this, it) 332 | } ?: getString(R.string.notifications_share_file_size_unknown) 333 | getString(R.string.notifications_server_description_file, item.name, size) 334 | } 335 | else -> getString(R.string.notifications_server_description_unknown) 336 | } 337 | } 338 | // Case: Sharing multiple Text items. 339 | sharingList.all { it.type == SharedItem.TYPE_TEXT } -> { 340 | val lastText = sharingList.last().text 341 | getString(R.string.notifications_server_description_texts, lastText) 342 | } 343 | // Case: Sharing multiple items (All Files, or mixed). 344 | else -> { 345 | val totalSize = sharingList 346 | .mapNotNull { it.size } 347 | .sum() 348 | val sizeStr = android.text.format.Formatter.formatShortFileSize(this, totalSize) 349 | getString(R.string.notifications_server_description_mixed, sizeStr) 350 | } 351 | } 352 | } 353 | 354 | } 355 | -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.preference.EditTextPreference 7 | import androidx.preference.PreferenceFragmentCompat 8 | import androidx.preference.SwitchPreferenceCompat 9 | 10 | class SettingsActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.settings_activity) 15 | if (savedInstanceState == null) { 16 | supportFragmentManager 17 | .beginTransaction() 18 | .replace(R.id.settings, SettingsFragment()) 19 | .commit() 20 | } 21 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 22 | } 23 | 24 | override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean { 25 | return when (item.itemId) { 26 | // Handle "Home" (Back) link by closing settings, going back to MainActivity. 27 | android.R.id.home -> { 28 | finish() 29 | true 30 | } 31 | else -> super.onOptionsItemSelected(item) 32 | } 33 | } 34 | 35 | class SettingsFragment : PreferenceFragmentCompat() { 36 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 37 | setPreferencesFromResource(R.xml.root_preferences, rootKey) 38 | val hasKey = EncryptionUtils.hasEncryptionKey(requireContext()) 39 | 40 | // Get references to preferences 41 | val encryptionToggle: SwitchPreferenceCompat? = findPreference("encryption") 42 | val passwordPreference: EditTextPreference? = findPreference("password") 43 | val portPreference: EditTextPreference? = findPreference("port") 44 | 45 | // Handle password field. 46 | passwordPreference?.summary = if (hasKey) getString(R.string.settings_password_set) 47 | else getString(R.string.settings_password_not_set) 48 | passwordPreference?.setOnPreferenceChangeListener { preference, newValue -> 49 | val password = newValue as String 50 | if (password.length >= 8) { 51 | if (EncryptionUtils.updateEncryptionKey(requireContext(), password)) { 52 | encryptionToggle?.isChecked = true 53 | encryptionToggle?.isEnabled = true 54 | passwordPreference.summary = getString(R.string.settings_password_set) 55 | // We don't want the password to actually be stored, always return false. 56 | false 57 | } else { 58 | showToast(getString(R.string.message_key_generation_failed)) 59 | false 60 | } 61 | } else { 62 | showToast(getString(R.string.message_password_invalid)) 63 | false 64 | } 65 | } 66 | 67 | // Handle encryption toggle 68 | encryptionToggle?.isEnabled = hasKey 69 | encryptionToggle?.setOnPreferenceChangeListener { _, newValue -> 70 | val enabled = newValue as Boolean 71 | if (enabled) { 72 | // Don't allow enabling encryption without a key being set 73 | if (!hasKey) { 74 | showToast(getString(R.string.message_set_password_first)) 75 | false 76 | } else { 77 | true 78 | } 79 | } 80 | true 81 | } 82 | 83 | // Validate Server TCP Port setting. 84 | portPreference?.setOnPreferenceChangeListener { preference, newValue -> 85 | val portString = newValue as String 86 | try { 87 | val port = portString.toInt() 88 | if (port in 0..65535) { 89 | true 90 | } else { 91 | showToast(getString(R.string.message_port_invalid)) 92 | false 93 | } 94 | } catch (e: NumberFormatException) { 95 | showToast(getString(R.string.message_port_invalid)) 96 | false 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * Helper function for displaying short messages at the bottom of the screen. 103 | */ 104 | private fun showToast(message: String) { 105 | Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() 106 | } 107 | } 108 | } 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.platform.LocalContext 13 | 14 | private val DarkColorScheme = darkColorScheme( 15 | primary = Purple80, 16 | secondary = PurpleGrey80, 17 | tertiary = Pink80 18 | ) 19 | 20 | private val LightColorScheme = lightColorScheme( 21 | primary = Purple40, 22 | secondary = PurpleGrey40, 23 | tertiary = Pink40 24 | 25 | /* Other default colors to override 26 | background = Color(0xFFFFFBFE), 27 | surface = Color(0xFFFFFBFE), 28 | onPrimary = Color.White, 29 | onSecondary = Color.White, 30 | onTertiary = Color.White, 31 | onBackground = Color(0xFF1C1B1F), 32 | onSurface = Color(0xFF1C1B1F), 33 | */ 34 | ) 35 | 36 | @Composable 37 | fun InterProfileSharingTheme( 38 | darkTheme: Boolean = isSystemInDarkTheme(), 39 | // Dynamic color is available on Android 12+ 40 | dynamicColor: Boolean = true, 41 | content: @Composable () -> Unit 42 | ) { 43 | val colorScheme = when { 44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 45 | val context = LocalContext.current 46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 47 | } 48 | 49 | darkTheme -> DarkColorScheme 50 | else -> LightColorScheme 51 | } 52 | 53 | MaterialTheme( 54 | colorScheme = colorScheme, 55 | typography = Typography, 56 | content = content 57 | ) 58 | } -------------------------------------------------------------------------------- /app/src/main/java/digital/ventral/ips/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package digital.ventral.ips.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | 14 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_activity.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | المشاركة بين الملفات الشخصية 8 | الإعدادات 9 | شارك الملفات والروابط والنصوص مع الملفات الشخصية الأخرى التي تم تثبيت هذا التطبيق عليها. 10 | يُنصح بتفعيل التشفير (⚙️) لمنع التطبيقات الأخرى من الوصول إلى البيانات المشتركة. قد يؤدي هذا إلى إبطاء عملية النقل وتقليل موثوقيتها. 11 | إذا لم تظهر إشعارات البيانات المشتركة في ملف شخصي آخر على الفور، افتح هذا التطبيق لبدء التحقق. 12 | مشاركة الملفات 13 | اختر ملفات للمشاركة مع الملفات الشخصية الأخرى التي تم تثبيت هذا التطبيق عليها. 14 | مشاركة النص المنسوخ 15 | شارك محتوى الحافظة الحالي مع الملفات الشخصية الأخرى التي تم تثبيت هذا التطبيق عليها. 16 | 17 | 18 | الإعدادات 19 | الأمان 20 | الخادم 21 | التشفير 22 | تشفير الاتصال بين الملفات الشخصية باستخدام AES، مع مفتاح مبني على كلمة المرور المشتركة. 23 | كلمة المرور المشتركة 24 | استخدم نفس كلمة المرور في جميع الملفات الشخصية 25 | تُستخدم كلمة المرور المشتركة لتشفير الاتصال بين ملفات Android الشخصية وضمان عدم تمكن أي تطبيق آخر من الوصول إلى البيانات المشتركة.\n\nاختر كلمة مرور طويلة - تحتاج فقط لتذكرها حتى تقوم بإعدادها في جميع نسخ هذا التطبيق (أي في جميع الملفات الشخصية التي تم تثبيته فيها).\n\nالمتطلبات: ٨ أحرف كحد أدنى. 26 | تم تعيين كلمة المرور 27 | لم يتم تعيين كلمة المرور 28 | المنفذ 29 | يجب تعيين نفس منفذ TCP في جميع الملفات الشخصية 30 | بعد اختيار المعلومات للمشاركة، يتم فتح منفذ TCP على واجهة loopback المحلية لخدمة البيانات لنسخ هذا التطبيق الأخرى في الملفات الشخصية الأخرى.\n\nعادةً لا يوجد سبب للابتعاد عن المنفذ الافتراضي 2411. 31 | 32 | 33 | يحتاج هذا التطبيق إلى الإشعارات. لا رسائل مزعجة، وعد. 34 | الحافظة فارغة. 35 | لم يتم اختيار ملفات. 36 | يجب أن تحتوي كلمة المرور على ٨ أحرف على الأقل. 37 | فشل إنشاء مفتاح AES من كلمة المرور المشتركة 38 | يتطلب التشفير كلمة مرور مشتركة 39 | يجب أن يكون المنفذ رقماً بين ٠ و٦٥٥٣٥. 40 | تم نسخ النص إلى الحافظة 41 | 42 | 43 | إشعارات المشاركة 44 | إشعارات حول البيانات المشتركة حالياً 45 | ملف مشترك جديد 46 | حجم غير معروف 47 | تنزيل 48 | تنزيل ومشاركة… 49 | نص مشترك جديد 50 | نسخ إلى الحافظة 51 | مشاركة… 52 | مشاركة النص عبر 53 | النص المشترك 54 | ملفات %1$s جديدة مشتركة 55 | %1$s ملف (%2$s) 56 | تنزيل الكل 57 | تنزيل ومشاركة الكل… 58 | جارٍ تنزيل %1$s 59 | جارٍ تنزيل %1$s للمشاركة 60 | فشل التنزيل 61 | فشلت المشاركة 62 | فشل تنزيل %1$s 63 | فشل تنزيل بعض الملفات 64 | اكتمل التنزيل 65 | تم حفظ %1$s في التنزيلات 66 | تم حفظ %1$s ملف في التنزيلات 67 | مشاركة %1$s عبر 68 | مشاركة الملفات عبر 69 | جاهز لمشاركة %1$s 70 | انقر للمشاركة 71 | جاهز لمشاركة %1$s ملف 72 | المشاركة بين الملفات الشخصية 73 | إيقاف المشاركة 74 | جاهز، لكن لا توجد مشاركة حالياً 75 | جارٍ مشاركة النص "\%1$s"\ 76 | جارٍ مشاركة الملف "\%1$s\" (%2$s) 77 | جارٍ مشاركة عدة نصوص "\%1$s"\ 78 | جارٍ مشاركة بيانات متعددة (%1$s) 79 | جارٍ مشاركة البيانات 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | Profil-Datenaustausch 8 | Einstellungen 9 | Teilen Sie Dateien, Links und Text mit anderen Profilen, in denen diese App installiert ist. 10 | Die Aktivierung der Verschlüsselung (⚙️) wird empfohlen, um zu verhindern, dass andere installierte Apps auf aktuell geteilte Daten zugreifen können. Dies kann die Dateiübertragung verlangsamen und weniger zuverlässig machen. 11 | Falls Benachrichtigungen über in einem anderen Profil geteilte Daten nicht sofort erscheinen, öffnen Sie diese App, um eine Überprüfung auszulösen. 12 | Dateien teilen 13 | Wählen Sie Dateien aus, die Sie mit anderen Profilen teilen möchten, in denen diese App installiert ist. 14 | Kopierten Text teilen 15 | Teilen Sie den aktuellen Inhalt Ihrer Zwischenablage mit anderen Profilen, in denen diese App installiert ist. 16 | 17 | 18 | Einstellungen 19 | Sicherheit 20 | Server 21 | Verschlüsselung 22 | Kommunikation zwischen Profilen mit AES verschlüsseln, Schlüssel basierend auf dem gemeinsamen Passwort. 23 | Gemeinsames Passwort 24 | Setzen Sie das gleiche Passwort in allen Profilen 25 | Das gemeinsame Passwort wird verwendet, um die Kommunikation dieser App zwischen Android-Benutzerprofilen zu verschlüsseln und sicherzustellen, dass keine andere App auf geteilte Daten zugreifen kann.\n\nWählen Sie etwas Langes - Sie müssen es sich nur merken, bis Sie es in allen Instanzen dieser App gesetzt haben (d.h. in allen Profilen, in denen diese App installiert ist).\n\nBeschränkungen: Minimale Länge von 8 Zeichen. 26 | Ein Passwort ist gesetzt 27 | Kein Passwort gesetzt 28 | Port 29 | Der gleiche TCP-Port muss in allen Profilen eingestellt sein 30 | Nach der Auswahl der zu teilenden Informationen wird ein TCP-Socket auf der lokalen Loopback-Schnittstelle geöffnet, um Daten an Instanzen dieser App in anderen Android-Benutzerprofilen zu übertragen.\n\nNormalerweise sollte es keinen Grund geben, vom Standard-Port 2411 abzuweichen. 31 | 32 | 33 | Diese App benötigt Benachrichtigungen. Kein Spam, versprochen. 34 | Zwischenablage ist leer. 35 | Keine Dateien ausgewählt. 36 | Passwort muss mindestens 8 Zeichen lang sein. 37 | Generierung des AES Schlüssels aus dem gemeinsamen Passwort fehlgeschlagen 38 | Verschlüsselung erfordert ein gemeinsames Passwort 39 | Port muss eine Zahl zwischen 0 und 65535 sein. 40 | Text in Zwischenablage kopiert 41 | 42 | 43 | Freigabe-Benachrichtigungen 44 | Benachrichtigungen über aktuell geteilte Daten 45 | Neue geteilte Datei 46 | unbekannte Größe 47 | Herunterladen 48 | Herunterladen und teilen… 49 | Neuer geteilter Text 50 | In Zwischenablage kopieren 51 | Teilen… 52 | Text teilen über 53 | Geteilter Text 54 | Neue geteilte %1$s Dateien 55 | %1$s Dateien (%2$s) 56 | Alle herunterladen 57 | Alle herunterladen und teilen… 58 | Lade %1$s herunter 59 | Lade %1$s zum Teilen herunter 60 | Download fehlgeschlagen 61 | Teilen fehlgeschlagen 62 | Herunterladen von %1$s fehlgeschlagen 63 | Einige Dateien konnten nicht heruntergeladen werden 64 | Download abgeschlossen 65 | %1$s in Downloads gespeichert 66 | %1$s Dateien in Downloads gespeichert 67 | %1$s teilen über 68 | Dateien teilen über 69 | Bereit zum Teilen von %1$s 70 | Tippen, um zu teilen 71 | Bereit zum Teilen von %1$s Dateien 72 | Profil-Datenaustausch 73 | Teilen beenden 74 | Bereit, aber nichts wird geteilt 75 | Teile Text \"%1$s\" 76 | Teile Datei \"%1$s\" (%2$s) 77 | Teile mehrere Texte \"%1$s\" 78 | Teile mehrere Dinge (%1$s) 79 | Teile daten 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | Compartir entre Perfiles 8 | Ajustes 9 | Comparte archivos, enlaces y texto con otros perfiles que tengan esta app instalada. 10 | Se recomienda activar el cifrado (⚙️) para evitar que otras apps accedan a los datos compartidos. Esto puede hacer que la transferencia sea más lenta y menos confiable. 11 | Si no ves inmediatamente las notificaciones de datos compartidos en otro perfil, abre esta app para activar una verificación. 12 | Compartir archivos 13 | Selecciona archivos para compartir con otros perfiles que tengan esta app instalada. 14 | Compartir texto copiado 15 | Comparte el contenido actual del portapapeles con otros perfiles que tengan esta app instalada. 16 | 17 | 18 | Ajustes 19 | Seguridad 20 | Servidor 21 | Cifrado 22 | Cifra la comunicación entre perfiles usando AES, con clave basada en la contraseña compartida. 23 | Contraseña compartida 24 | Usa la misma contraseña en todos los perfiles 25 | La contraseña compartida se usa para cifrar la comunicación entre perfiles de Android y asegurar que ninguna otra app pueda acceder a los datos compartidos.\n\nElige una contraseña larga - solo necesitas recordarla hasta que la configures en todas las instancias de esta app (es decir, en todos los perfiles donde está instalada).\n\nRequisitos: Mínimo 8 caracteres. 26 | Contraseña configurada 27 | Sin contraseña 28 | Puerto 29 | El mismo puerto TCP debe configurarse en todos los perfiles 30 | Al seleccionar información para compartir, se abre un socket TCP en la interfaz de loopback local para servir datos a otras instancias de esta app en otros perfiles.\n\nNormalmente no hay razón para cambiar el puerto predeterminado 2411. 31 | 32 | 33 | Esta app necesita notificaciones. Sin spam, lo prometo. 34 | El portapapeles está vacío. 35 | No hay archivos seleccionados. 36 | La contraseña debe tener al menos 8 caracteres. 37 | Error al generar la clave AES de la contraseña compartida 38 | El cifrado requiere una contraseña compartida 39 | El puerto debe ser un número entre 0 y 65535. 40 | Texto copiado al portapapeles 41 | 42 | 43 | Notificaciones de compartir 44 | Notificaciones sobre datos actualmente compartidos 45 | Nuevo archivo compartido 46 | tamaño desconocido 47 | Descargar 48 | Descargar y compartir… 49 | Nuevo texto compartido 50 | Copiar al portapapeles 51 | Compartir… 52 | Compartir texto por 53 | Texto compartido 54 | Nuevos archivos %1$s compartidos 55 | %1$s archivos (%2$s) 56 | Descargar todo 57 | Descargar y compartir todo… 58 | Descargando %1$s 59 | Descargando %1$s para compartir 60 | Error al descargar 61 | Error al compartir 62 | Error al descargar %1$s 63 | Error al descargar algunos archivos 64 | Descarga completa 65 | %1$s guardado en Descargas 66 | %1$s archivos guardados en Descargas 67 | Compartir %1$s por 68 | Compartir archivos por 69 | Listo para compartir %1$s 70 | Toca para compartir 71 | Listo para compartir %1$s archivos 72 | Compartir entre Perfiles 73 | Dejar de compartir 74 | Listo, pero nada compartido 75 | Compartiendo texto \"%1$s\" 76 | Compartiendo archivo \"%1$s\" (%2$s) 77 | Compartiendo varios textos \"%1$s\" 78 | Compartiendo varios datos (%1$s) 79 | Compartiendo datos 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | Partage entre profils 8 | Paramètres 9 | Partage des fichiers, des liens et du texte avec d\'autres profils ayant cette application installée. 10 | L\'activation du chiffrement (⚙️) est recommandée pour empêcher d\'autres applications d\'accéder aux données partagées. Cela peut ralentir le transfert et le rendre moins fiable. 11 | Si les notifications de données partagées dans un autre profil n\'apparaissent pas immédiatement, ouvre cette application pour déclencher une vérification. 12 | Partager des fichiers 13 | Sélectionne des fichiers à partager avec d\'autres profils ayant cette application installée. 14 | Partager le texte copié 15 | Partage le contenu actuel du presse-papiers avec d\'autres profils ayant cette application installée. 16 | 17 | 18 | Paramètres 19 | Sécurité 20 | Serveur 21 | Chiffrement 22 | Chiffre la communication entre les profils avec AES, avec une clé basée sur le mot de passe partagé. 23 | Mot de passe partagé 24 | Utilise le même mot de passe dans tous les profils 25 | Le mot de passe partagé est utilisé pour chiffrer la communication entre les profils Android et garantir qu\'aucune autre application ne puisse accéder aux données partagées.\n\nChoisis un mot de passe long - tu dois seulement t\'en souvenir jusqu\'à ce que tu l\'aies configuré dans toutes les instances de cette application (c\'est-à-dire dans tous les profils où elle est installée).\n\nExigences : minimum 8 caractères. 26 | Mot de passe configuré 27 | Aucun mot de passe 28 | Port 29 | Le même port TCP doit être configuré dans tous les profils 30 | Après la sélection des informations à partager, un socket TCP est ouvert sur l\'interface de loopback locale pour servir les données aux autres instances de cette application dans d\'autres profils.\n\nIl n\'y a généralement aucune raison de s\'écarter du port 2411 par défaut. 31 | 32 | 33 | Cette application nécessite des notifications. Pas de spam, promis ! 34 | Le presse-papiers est vide. 35 | Aucun fichier sélectionné. 36 | Le mot de passe doit contenir au moins 8 caractères. 37 | Échec de la génération de la clé AES à partir du mot de passe partagé 38 | Le chiffrement nécessite un mot de passe partagé 39 | Le port doit être un nombre entre 0 et 65535. 40 | Texte copié dans le presse-papiers 41 | 42 | 43 | Notifications de partage 44 | Notifications des données actuellement partagées 45 | Nouveau fichier partagé 46 | taille inconnue 47 | Télécharger 48 | Télécharger et partager… 49 | Nouveau texte partagé 50 | Copier 51 | Partager… 52 | Partager le texte via 53 | Texte partagé 54 | Nouveaux fichiers %1$s partagés 55 | %1$s fichiers (%2$s) 56 | Tout télécharger 57 | Tout télécharger et partager… 58 | Téléchargement de %1$s 59 | Téléchargement de %1$s pour partage 60 | Échec du téléchargement 61 | Échec du partage 62 | Échec du téléchargement de %1$s 63 | Échec du téléchargement de certains fichiers 64 | Téléchargement terminé 65 | %1$s enregistré dans Téléchargements 66 | %1$s fichiers enregistrés dans Téléchargements 67 | Partager %1$s via 68 | Partager les fichiers via 69 | Prêt à partager %1$s 70 | Touche pour partager 71 | Prêt à partager %1$s fichiers 72 | Partage entre profils 73 | Arrêter le partage 74 | Prêt, mais rien n\'est partagé 75 | Partage du texte « %1$s » 76 | Partage du fichier « %1$s » (%2$s) 77 | Partage de plusieurs textes « %1$s » 78 | Partage de données multiples (%1$s) 79 | Partage de données 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | Condivisione tra Profili 8 | Impostazioni 9 | Condividi file, link e testo con altri profili che hanno questa app installata. 10 | Si consiglia di attivare la crittografia (⚙️) per impedire ad altre app di accedere ai dati condivisi. Questo potrebbe rallentare il trasferimento e renderlo meno affidabile. 11 | Se le notifiche dei dati condivisi in un altro profilo non appaiono subito, apri questa app per avviare una verifica. 12 | Condividi file 13 | Seleziona i file da condividere con altri profili che hanno questa app installata. 14 | Condividi testo copiato 15 | Condividi il contenuto attuale degli appunti con altri profili che hanno questa app installata. 16 | 17 | 18 | Impostazioni 19 | Sicurezza 20 | Server 21 | Crittografia 22 | Cripta la comunicazione tra i profili usando AES, con chiave basata sulla password condivisa. 23 | Password condivisa 24 | Usa la stessa password in tutti i profili 25 | La password condivisa viene usata per crittografare la comunicazione tra i profili Android e garantire che nessun\'altra app possa accedere ai dati condivisi.\n\nScegli una password lunga - devi solo ricordarla finché non l\'hai configurata in tutte le istanze di questa app (cioè in tutti i profili dove è installata).\n\nRequisiti: minimo 8 caratteri. 26 | Password configurata 27 | Nessuna password 28 | Porta 29 | La stessa porta TCP deve essere configurata in tutti i profili 30 | Dopo aver selezionato le informazioni da condividere, viene aperto un socket TCP sull\'interfaccia di loopback locale per fornire dati ad altre istanze di questa app in altri profili.\n\nDi solito non c\'è motivo di cambiare la porta predefinita 2411. 31 | 32 | 33 | Questa app ha bisogno delle notifiche. Niente spam, promesso! 34 | Gli appunti sono vuoti. 35 | Nessun file selezionato. 36 | La password deve essere di almeno 8 caratteri. 37 | Errore nella generazione della chiave AES dalla password condivisa 38 | La crittografia richiede una password condivisa 39 | La porta deve essere un numero tra 0 e 65535. 40 | Testo copiato negli appunti 41 | 42 | 43 | Notifiche di condivisione 44 | Notifiche sui dati attualmente condivisi 45 | Nuovo file condiviso 46 | dimensione sconosciuta 47 | Scarica 48 | Scarica e condividi… 49 | Nuovo testo condiviso 50 | Copia negli appunti 51 | Condividi… 52 | Condividi testo tramite 53 | Testo condiviso 54 | Nuovi file %1$s condivisi 55 | %1$s file (%2$s) 56 | Scarica tutto 57 | Scarica e condividi tutto… 58 | Download di %1$s 59 | Download di %1$s per condivisione 60 | Download fallito 61 | Condivisione fallita 62 | Download di %1$s fallito 63 | Download di alcuni file fallito 64 | Download completato 65 | %1$s salvato in Download 66 | %1$s file salvati in Download 67 | Condividi %1$s tramite 68 | Condividi file tramite 69 | Pronto per condividere %1$s 70 | Tocca per condividere 71 | Pronto per condividere %1$s file 72 | Condivisione tra Profili 73 | Ferma condivisione 74 | Pronto, ma nulla in condivisione 75 | Condivisione testo \"%1$s\" 76 | Condivisione file \"%1$s\" (%2$s) 77 | Condivisione di più testi \"%1$s\" 78 | Condivisione di più dati (%1$s) 79 | Condivisione dati 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | プロファイル間共有 8 | 設定 9 | このアプリをインストールした他のプロファイルとファイル、リンク、テキストを共有できます。 10 | 他のアプリから共有データへのアクセスを防ぐため、暗号化(⚙️)の有効化を推奨します。転送速度と信頼性に影響する可能性があります。 11 | 他のプロファイルでの共有通知が表示されない場合は、このアプリを開いて確認を実行してください。 12 | ファイル共有 13 | このアプリをインストールした他のプロファイルと共有するファイルを選択 14 | コピーしたテキストを共有 15 | クリップボードの内容を他のプロファイルと共有 16 | 17 | 18 | 設定 19 | セキュリティ 20 | サーバー 21 | 暗号化 22 | 共有パスワードを基にしたAES暗号化でプロファイル間の通信を保護します。 23 | 共有パスワード 24 | 全プロファイルで同じパスワードを設定 25 | 共有パスワードは、Androidプロファイル間の通信を暗号化し、他のアプリからのデータアクセスを防ぎます。\n\n長いパスワードを選んでください。全プロファイルでの設定が完了するまでの間だけ覚えておく必要があります。\n\n要件:8文字以上 26 | パスワード設定済み 27 | 未設定 28 | ポート 29 | 全プロファイルで同じTCPポートを設定 30 | 共有時、ローカルループバックインターフェースにTCPソケットを開き、他プロファイルのアプリにデータを提供します。\n\n通常、デフォルトポート2411から変更する必要はありません。 31 | 32 | 33 | 通知が必要です。スパムはありません。 34 | クリップボードが空です 35 | ファイルが選択されていません 36 | パスワードは8文字以上必要です 37 | 暗号化キーの生成に失敗しました 38 | 暗号化にはパスワードが必要です 39 | ポートは0~65535の数値で指定してください 40 | クリップボードにコピーしました 41 | 42 | 43 | 共有通知 44 | 現在共有中のデータに関する通知 45 | 新しい共有ファイル 46 | サイズ不明 47 | 保存 48 | 保存して共有 49 | 新しい共有テキスト 50 | コピー 51 | 共有 52 | 共有方法を選択 53 | 共有テキスト 54 | 新しい共有ファイル:%1$s 55 | %1$sファイル(%2$s) 56 | 全て保存 57 | 全て保存して共有 58 | %1$sを保存中 59 | 共有用に%1$sを保存中 60 | 保存失敗 61 | 共有失敗 62 | %1$sの保存に失敗しました 63 | 一部のファイルの保存に失敗しました 64 | 保存完了 65 | %1$sをダウンロードフォルダに保存しました 66 | %1$sファイルをダウンロードフォルダに保存しました 67 | %1$sの共有方法を選択 68 | ファイルの共有方法を選択 69 | %1$sを共有できます 70 | タップして共有 71 | %1$sファイルを共有できます 72 | プロファイル間共有 73 | 共有停止 74 | 準備完了(共有なし) 75 | テキスト「%1$s」を共有中 76 | ファイル「%1$s」(%2$s)を共有中 77 | 複数のテキスト「%1$s」を共有中 78 | 複数のデータ(%1$s)を共有中 79 | データを共有中 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | 프로필 간 공유 8 | 설정 9 | 이 앱이 설치된 다른 프로필과 파일, 링크, 텍스트를 공유할 수 있습니다. 10 | 다른 앱이 공유 중인 데이터에 접근하는 것을 방지하기 위해 암호화(⚙️)를 활성화하는 것이 좋습니다. 전송 속도가 느려지고 안정성이 떨어질 수 있습니다. 11 | 다른 프로필에서 공유된 데이터의 알림이 바로 표시되지 않는 경우, 이 앱을 열어 확인을 시작해 주세요. 12 | 파일 공유 13 | 이 앱이 설치된 다른 프로필과 공유할 파일을 선택하세요. 14 | 복사된 텍스트 공유 15 | 현재 클립보드의 내용을 이 앱이 설치된 다른 프로필과 공유합니다. 16 | 17 | 18 | 설정 19 | 보안 20 | 서버 21 | 암호화 22 | 공유 비밀번호를 기반으로 한 AES 키로 프로필 간 통신을 암호화합니다. 23 | 공유 비밀번호 24 | 모든 프로필에서 동일한 비밀번호를 사용하세요 25 | 공유 비밀번호는 Android 프로필 간 통신을 암호화하고 다른 앱이 공유된 데이터에 접근하지 못하도록 하는 데 사용됩니다.\n\n긴 비밀번호를 선택하세요 - 이 앱의 모든 인스턴스(즉, 앱이 설치된 모든 프로필)에서 설정할 때까지만 기억하면 됩니다.\n\n요구사항: 최소 8자 26 | 비밀번호가 설정되었습니다 27 | 비밀번호가 설정되지 않았습니다 28 | 포트 29 | 모든 프로필에서 동일한 TCP 포트를 설정해야 합니다 30 | 공유할 정보를 선택하면 다른 프로필의 앱 인스턴스에 데이터를 제공하기 위해 로컬 루프백 인터페이스에 TCP 소켓이 열립니다.\n\n기본 포트 2411을 변경할 필요는 없습니다. 31 | 32 | 33 | 이 앱은 알림이 필요합니다. 스팸은 보내지 않습니다. 34 | 클립보드가 비어 있습니다. 35 | 선택된 파일이 없습니다. 36 | 비밀번호는 최소 8자 이상이어야 합니다. 37 | 공유 비밀번호에서 AES 키 생성 실패 38 | 암호화를 위해 공유 비밀번호가 필요합니다 39 | 포트는 0에서 65535 사이의 숫자여야 합니다. 40 | 텍스트가 클립보드에 복사되었습니다 41 | 42 | 43 | 공유 알림 44 | 현재 공유 중인 데이터에 대한 알림 45 | 새로운 공유 파일 46 | 크기 알 수 없음 47 | 다운로드 48 | 다운로드 후 공유… 49 | 새로운 공유 텍스트 50 | 클립보드에 복사 51 | 공유… 52 | 텍스트 공유 방법 53 | 공유된 텍스트 54 | 새로운 %1$s 파일 공유됨 55 | 파일 %1$s개 (%2$s) 56 | 모두 다운로드 57 | 모두 다운로드 후 공유… 58 | %1$s 다운로드 중 59 | 공유를 위해 %1$s 다운로드 중 60 | 다운로드 실패 61 | 공유 실패 62 | %1$s 다운로드 실패 63 | 일부 파일 다운로드 실패 64 | 다운로드 완료 65 | %1$s이(가) 다운로드 폴더에 저장되었습니다 66 | %1$s개의 파일이 다운로드 폴더에 저장되었습니다 67 | %1$s 공유 방법 68 | 파일 공유 방법 69 | %1$s 공유 준비됨 70 | 터치하여 공유 71 | %1$s개의 파일 공유 준비됨 72 | 프로필 간 공유 73 | 공유 중지 74 | 준비되었지만 공유 중인 항목 없음 75 | 텍스트 공유 중: \"%1$s\" 76 | 파일 공유 중: \"%1$s\" (%2$s) 77 | 여러 텍스트 공유 중: \"%1$s\" 78 | 여러 데이터 공유 중 (%1$s) 79 | 데이터 공유 중 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | Compartilhamento entre Perfis 8 | Configurações 9 | Compartilhe arquivos, links e texto com outros perfis que tenham este aplicativo instalado. 10 | Recomenda-se ativar a criptografia (⚙️) para evitar que outros aplicativos acessem os dados compartilhados. Isso pode tornar a transferência mais lenta e menos confiável. 11 | Se as notificações de dados compartilhados em outro perfil não aparecerem imediatamente, abra este aplicativo para iniciar uma verificação. 12 | Compartilhar arquivos 13 | Selecione arquivos para compartilhar com outros perfis que tenham este aplicativo instalado. 14 | Compartilhar texto copiado 15 | Compartilhe o conteúdo atual da área de transferência com outros perfis que tenham este aplicativo instalado. 16 | 17 | 18 | Configurações 19 | Segurança 20 | Servidor 21 | Criptografia 22 | Criptografa a comunicação entre perfis usando AES, com chave baseada na senha compartilhada. 23 | Senha compartilhada 24 | Use a mesma senha em todos os perfis 25 | A senha compartilhada é usada para criptografar a comunicação entre perfis do Android e garantir que nenhum outro aplicativo possa acessar os dados compartilhados.\n\nEscolha uma senha longa - você só precisa lembrá-la até configurar em todas as instâncias deste aplicativo (ou seja, em todos os perfis onde está instalado).\n\nRequisitos: Mínimo de 8 caracteres. 26 | Senha configurada 27 | Sem senha 28 | Porta 29 | A mesma porta TCP deve ser configurada em todos os perfis 30 | Após selecionar informações para compartilhar, um socket TCP é aberto na interface de loopback local para servir dados a outras instâncias deste aplicativo em outros perfis.\n\nNormalmente não há motivo para alterar a porta padrão 2411. 31 | 32 | 33 | Este aplicativo precisa de notificações. Sem spam, prometo. 34 | Área de transferência vazia. 35 | Nenhum arquivo selecionado. 36 | A senha deve ter pelo menos 8 caracteres. 37 | Erro ao gerar chave AES da senha compartilhada 38 | A criptografia requer uma senha compartilhada 39 | A porta deve ser um número entre 0 e 65535. 40 | Texto copiado para a área de transferência 41 | 42 | 43 | Notificações de compartilhamento 44 | Notificações sobre dados atualmente compartilhados 45 | Novo arquivo compartilhado 46 | tamanho desconhecido 47 | Baixar 48 | Baixar e compartilhar… 49 | Novo texto compartilhado 50 | Copiar para área de transferência 51 | Compartilhar… 52 | Compartilhar texto via 53 | Texto compartilhado 54 | Novos arquivos %1$s compartilhados 55 | %1$s arquivos (%2$s) 56 | Baixar tudo 57 | Baixar e compartilhar tudo… 58 | Baixando %1$s 59 | Baixando %1$s para compartilhar 60 | Falha no download 61 | Falha ao compartilhar 62 | Falha ao baixar %1$s 63 | Falha ao baixar alguns arquivos 64 | Download concluído 65 | %1$s salvo em Downloads 66 | %1$s arquivos salvos em Downloads 67 | Compartilhar %1$s via 68 | Compartilhar arquivos via 69 | Pronto para compartilhar %1$s 70 | Toque para compartilhar 71 | Pronto para compartilhar %1$s arquivos 72 | Compartilhamento entre Perfis 73 | Parar compartilhamento 74 | Pronto, mas nada compartilhado 75 | Compartilhando texto \"%1$s\" 76 | Compartilhando arquivo \"%1$s\" (%2$s) 77 | Compartilhando vários textos \"%1$s\" 78 | Compartilhando vários dados (%1$s) 79 | Compartilhando dados 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | Обмен между профилями 8 | Настройки 9 | Делись файлами, ссылками и текстом с другими профилями, где установлено это приложение. 10 | Рекомендуется включить шифрование (⚙️), чтобы другие приложения не могли получить доступ к передаваемым данным. Это может замедлить передачу и снизить её надёжность. 11 | Если уведомления о данных из другого профиля не появляются сразу, открой приложение для проверки. 12 | Поделиться файлами 13 | Выбери файлы для передачи в другие профили, где установлено это приложение. 14 | Поделиться текстом 15 | Отправь содержимое буфера обмена в другие профили, где установлено это приложение. 16 | 17 | 18 | Настройки 19 | Безопасность 20 | Сервер 21 | Шифрование 22 | Шифрует обмен данными между профилями с помощью AES, используя ключ на основе общего пароля. 23 | Общий пароль 24 | Используй одинаковый пароль во всех профилях 25 | Общий пароль используется для шифрования обмена данными между профилями Android и защиты от доступа других приложений к передаваемым данным.\n\nВыбери длинный пароль – его нужно будет помнить только до настройки во всех копиях приложения (то есть во всех профилях, где оно установлено).\n\nТребования: минимум 8 символов. 26 | Пароль установлен 27 | Пароль не задан 28 | Порт 29 | Во всех профилях должен быть настроен одинаковый TCP-порт 30 | После выбора данных для передачи, открывается TCP-сокет на локальном интерфейсе для обмена с копиями приложения в других профилях.\n\nОбычно нет причин менять порт по умолчанию 2411. 31 | 32 | 33 | Приложению нужны уведомления. Спама не будет, обещаю. 34 | Буфер обмена пуст. 35 | Файлы не выбраны. 36 | Пароль должен быть не менее 8 символов. 37 | Не удалось создать ключ AES из общего пароля 38 | Для шифрования нужен общий пароль 39 | Порт должен быть числом от 0 до 65535. 40 | Текст скопирован в буфер обмена 41 | 42 | 43 | Уведомления об обмене 44 | Уведомления о передаваемых данных 45 | Новый файл 46 | размер неизвестен 47 | Скачать 48 | Скачать и поделиться… 49 | Новый текст 50 | Копировать 51 | Поделиться… 52 | Поделиться текстом через 53 | Полученный текст 54 | Новые %1$s файлы 55 | %1$s файлов (%2$s) 56 | Скачать все 57 | Скачать и поделиться всем… 58 | Скачивание: %1$s 59 | Скачивание %1$s для отправки 60 | Ошибка скачивания 61 | Ошибка отправки 62 | Не удалось скачать %1$s 63 | Не удалось скачать некоторые файлы 64 | Скачивание завершено 65 | %1$s сохранён в загрузках 66 | %1$s файлов сохранено в загрузках 67 | Отправить %1$s через 68 | Отправить файлы через 69 | Готово к отправке: %1$s 70 | Нажми для отправки 71 | Готово к отправке: %1$s файлов 72 | Обмен между профилями 73 | Остановить 74 | Готово, но ничего не передаётся 75 | Передаётся текст «%1$s» 76 | Передаётся файл «%1$s» (%2$s) 77 | Передаётся несколько текстов «%1$s» 78 | Передаются данные (%1$s) 79 | Передача данных 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Inter Profile Sharing 4 | 5 | 6 | Inter Profile Sharing 7 | 跨配置文件共享 8 | 设置 9 | 与安装此应用的其他配置文件共享文件、链接和文本。 10 | 建议启用加密(⚙️)以防止其他应用访问共享数据。这可能会降低传输速度和可靠性。 11 | 如果在其他配置文件中共享的数据通知没有立即显示,请打开此应用触发检查。 12 | 共享文件 13 | 选择要与其他已安装此应用的配置文件共享的文件。 14 | 共享剪贴板文本 15 | 与其他已安装此应用的配置文件共享当前剪贴板内容。 16 | 17 | 18 | 设置 19 | 安全 20 | 服务器 21 | 加密 22 | 使用基于共享密码的 AES 密钥加密配置文件间通信。 23 | 共享密码 24 | 在所有配置文件中使用相同的密码 25 | 共享密码用于加密 Android 配置文件之间的通信,确保其他应用无法访问共享数据。\n\n请选择较长的密码——您只需要记住它直到在所有配置文件中设置完成(即在所有安装此应用的配置文件中)。\n\n要求:至少 8 个字符。 26 | 已设置密码 27 | 未设置密码 28 | 端口 29 | 所有配置文件必须设置相同的 TCP 端口 30 | 选择要共享的信息后,将在本地回环接口上打开 TCP 套接字,为其他配置文件中的应用实例提供数据。\n\n通常无需更改默认端口 2411。 31 | 32 | 33 | 此应用需要通知权限。保证不会发送垃圾信息。 34 | 剪贴板为空。 35 | 未选择文件。 36 | 密码至少需要 8 个字符。 37 | 从共享密码生成 AES 密钥失败 38 | 加密需要共享密码 39 | 端口必须是 0 到 65535 之间的数字。 40 | 文本已复制到剪贴板 41 | 42 | 43 | 共享通知 44 | 当前共享数据的通知 45 | 新共享文件 46 | 大小未知 47 | 下载 48 | 下载并分享… 49 | 新共享文本 50 | 复制到剪贴板 51 | 分享… 52 | 分享文本方式 53 | 共享文本 54 | 新共享的%1$s文件 55 | %1$s个文件(%2$s) 56 | 全部下载 57 | 下载并分享全部… 58 | 正在下载%1$s 59 | 正在下载%1$s以分享 60 | 下载失败 61 | 分享失败 62 | 下载%1$s失败 63 | 部分文件下载失败 64 | 下载完成 65 | %1$s已保存到下载文件夹 66 | %1$s个文件已保存到下载文件夹 67 | 分享%1$s方式 68 | 分享文件方式 69 | 准备分享%1$s 70 | 点击分享 71 | 准备分享%1$s个文件 72 | 跨配置文件共享 73 | 停止共享 74 | 就绪,但未共享内容 75 | 正在共享文本:\"%1$s\" 76 | 正在共享文件:\"%1$s\"(%2$s) 77 | 正在共享多个文本:\"%1$s\" 78 | 正在共享多个数据(%1$s) 79 | 正在共享数据 80 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Inter Profile Sharing 3 | 4 | 5 | Inter Profile Sharing 6 | Inter Profile Sharing 7 | Settings 8 | Share Files, Links and Text with other profiles that have this App installed. 9 | Enabling encryption (⚙️) is recommended to prevent other installed Apps from accessing currently shared data. It may make file transfer slower and less reliable. 10 | If notifications of data shared in another profile do not immediately appear, open this App to trigger a check. 11 | Share Files 12 | Select Files to share with other profiles that have this App installed. 13 | Share copied Text 14 | Share the current contents of your clipboard with other profiles that have this App installed. 15 | 16 | 17 | Settings 18 | Security 19 | Server 20 | Encryption 21 | Encrypt cross-profile communications using AES, with key based on Sharing Password. 22 | Sharing Password 23 | Set the same Sharing Password in all Profiles 24 | The Sharing Password is used to encrypt communication of this App between Android User Profiles, ensuring no other App is able to access shared data.\n\nChoose something long - you only have to remember it until you\'ve set it within all instances of this App (ie. in all Profiles where this App is installed).\n\nRestrictions: Minimum length of 8 characters. 25 | A password is set 26 | No password set 27 | Port 28 | The same TCP Port must be set in all Profiles 29 | After selecting information to share, a TCP socket will be opened on the local loopback interface to serve data to instances of this App in other Android User Profiles.\n\nThere should normally be no need to deviate from the default port 2411. 30 | 31 | 32 | This App requires notifications. No spam, promise. 33 | Clipboard is empty. 34 | No files selected. 35 | Password must be at least 8 characters long. 36 | Failed to generate encryption key from Shared Password 37 | Encryption requires a Shared Password 38 | Port must be a number between 0 and 65535. 39 | Text copied to clipboard 40 | 41 | 42 | Sharing Notifications 43 | Notifications about currently shared items 44 | New shared file 45 | unknown size 46 | Add to Downloads 47 | Download and Share via… 48 | New shared text 49 | Copy to Clipboard 50 | Share via… 51 | Share text via 52 | Shared Text 53 | New shared %1$s files 54 | %1$s files (%2$s) 55 | Add all to Downloads 56 | Download and Share all… 57 | Downloading %1$s 58 | Downloading %1$s for sharing 59 | Download failed 60 | Share failed 61 | Failed to download %1$s 62 | Failed to download some files 63 | Download complete 64 | %1$s saved to Downloads 65 | %1$s files saved to Downloads 66 | Share %1$s via 67 | Share files via 68 | Ready to share %1$s 69 | Tap to choose how to share 70 | Ready to share %1$s files 71 | Inter Profile Sharing 72 | Stop Sharing 73 | Ready but not sharing anything 74 | Sharing text \"%1$s\" 75 | Sharing file \"%1$s\" (%2$s) 76 | Sharing multiple texts \"%1$s\" 77 | Sharing multiple items (%1$s) 78 | Sharing item 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |