├── .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 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/root_preferences.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
13 |
14 |
21 |
22 |
23 |
24 |
25 |
26 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.compose) apply false
6 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.7.1"
3 | kotlin = "2.0.0"
4 | coreKtx = "1.13.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.2.1"
7 | espressoCore = "3.6.1"
8 | lifecycleRuntimeKtx = "2.8.6"
9 | activityCompose = "1.9.3"
10 | composeBom = "2024.04.01"
11 | appcompat = "1.7.0"
12 | preference = "1.2.0"
13 | material = "1.4.0"
14 | gson = "2.10.1"
15 | kotlinxCoroutinesAndroid = "1.7.3"
16 | security-crypto = "1.0.0"
17 | tink-android = "1.12.0"
18 |
19 | [libraries]
20 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
21 | junit = { group = "junit", name = "junit", version.ref = "junit" }
22 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
23 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
24 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
25 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
26 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
27 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
28 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
29 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
30 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
31 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
32 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
33 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
34 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
35 | androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
36 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
37 | gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
38 | kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
39 | androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security-crypto" }
40 | google-tink-android = { group = "com.google.crypto.tink", name = "tink-android", version.ref = "tink-android" }
41 |
42 | [plugins]
43 | android-application = { id = "com.android.application", version.ref = "agp" }
44 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
45 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
46 |
47 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/metadata/ar/full_description.txt:
--------------------------------------------------------------------------------
1 | أداة بسيطة تتيح لك مشاركة الوسائط والنصوص مع ملف تعريف Android آخر بنفس سهولة إرسالها إلى أي تطبيق.
2 |
3 | تتيح ملفات تعريف Android عزل التطبيقات عن بعضها، مما يسمح للمستخدمين بفصل التطبيقات الحساسة عن التطبيقات الأقل موثوقية. لا يمكن لأي ملف تعريف الوصول إلى تطبيقات أو بيانات أو ملفات ملف تعريف آخر، مما يعني أيضاً عدم وجود طريقة سهلة لتبادل المعلومات بين المستخدمين.
4 |
5 | مع هذا التطبيق، يمكنك اختيار المعلومات التي تريد مشاركتها مع ملفات التعريف الأخرى على نفس الجهاز. ستظهر إشعارات حول البيانات المشتركة في ملفات التعريف الأخرى، مما يتيح لك تنزيلها أو مشاركتها أو نسخها من ملف التعريف المصدر.
6 |
7 | تتم جميع الاتصالات بين ملفات التعريف محلياً دون الحاجة إلى اتصال بالإنترنت أو استخدام خوادم أو تدخل من أطراف خارجية.
8 |
9 | بدون إعلانات. بدون تتبع. بدون مشتريات داخل التطبيق. برنامج مفتوح المصدر مجاني.
--------------------------------------------------------------------------------
/metadata/ar/short_description.txt:
--------------------------------------------------------------------------------
1 | مشاركة الوسائط والنصوص بين ملفات تعريف Android بسهولة
--------------------------------------------------------------------------------
/metadata/ar/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/de-DE/full_description.txt:
--------------------------------------------------------------------------------
1 | Ein einfaches Tool, mit dem Sie Medien und Text so leicht mit einem anderen Android-Benutzerprofil teilen können wie das weiterleiten an eine andere App.
2 |
3 | Android-Benutzerprofile ermöglichen die Isolierung von Apps, sodass Nutzer sensible Anwendungen von weniger vertrauenswürdigen trennen können. Kein Profil kann auf die Apps, App-Daten oder Dateien eines anderen Profils zugreifen, was allerdings auch bedeutet, dass es für Benutzer verschiedener Profile keine einfache Möglichkeit gibt, Informationen auszutauschen.
4 |
5 | Mit dieser App können Sie auswählen, welche Informationen Sie mit anderen Profilen auf demselben Gerät teilen möchten. In den anderen Profilen werden Benachrichtigungen über die geteilten Daten angezeigt, die es ermöglichen, diese aus dem Quellprofil herunterzuladen, weiterzuleiten oder zu kopieren.
6 |
7 | Die gesamte Kommunikation zwischen den Profilen erfolgt lokal ohne Internetverbindung, ohne Server und ohne Beteiligung von Drittanbietern.
8 |
9 | Keine Werbung. Kein Tracking. Keine In-App-Käufe. Freie Open-Source-Software.
--------------------------------------------------------------------------------
/metadata/de-DE/short_description.txt:
--------------------------------------------------------------------------------
1 | Medien und Text einfach zwischen Android-Benutzerprofilen austauschen
--------------------------------------------------------------------------------
/metadata/de-DE/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/1.txt:
--------------------------------------------------------------------------------
1 | * Share Links, Files, and Text across Android User Profiles
2 | * Share Files or Links from other Applications to this App to share them with other User Profiles
3 | * Share Clip Board contents across Android User Profiles
4 | * Inter-Profile communication can optionally be encrypted (AES with key derived from a Password set in this App in both profiles)
--------------------------------------------------------------------------------
/metadata/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | This is a simple Tool allowing you to share media and text with another Android User Profile as easily as forwarding it to another App.
2 |
3 | Android's User Profiles allow isolating Apps from each other such that users are able to keep sensitive applications separate from less trustworthy ones. No profile can access the apps, app data or files of another profile, but that also means that there's no easy way for for users of profiles to exchange information.
4 |
5 | With this App you'll be able to select which information you'd like to share with other profiles on the same device. Notifications about the shared data will be shown in the other profiles allowing you to download, share or copy it from the profile the data is located in.
6 |
7 | All inter-profile communications happen locally without requiring an Internet connection, the use of servers, or involvement of 3rd party services.
8 |
9 | No Ads. No Tracking. No In-App Purchases. Free Open Source Software.
--------------------------------------------------------------------------------
/metadata/en-US/images/featureGraphic.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/metadata/en-US/images/featureGraphic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/featureGraphic.jpg
--------------------------------------------------------------------------------
/metadata/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/metadata/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/icon.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-client-notifications.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/phoneScreenshots/screenshot-client-notifications.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-main-activity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/phoneScreenshots/screenshot-main-activity.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-server-notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/phoneScreenshots/screenshot-server-notification.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-settings-activity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/phoneScreenshots/screenshot-settings-activity.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot-share-from-gallery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VentralDigital/InterProfileSharing/d29705839f3d1f41d21b91ef686f1ce143b43f95/metadata/en-US/images/phoneScreenshots/screenshot-share-from-gallery.png
--------------------------------------------------------------------------------
/metadata/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Easily share media and text between Android user profiles
--------------------------------------------------------------------------------
/metadata/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/es-ES/full_description.txt:
--------------------------------------------------------------------------------
1 | Una herramienta simple que te permite compartir medios y texto con otro perfil de usuario de Android tan fácilmente como compartir con otra aplicación.
2 |
3 | Los perfiles de usuario de Android permiten aislar las aplicaciones entre sí, permitiendo mantener las aplicaciones sensibles separadas de las menos confiables. Ningún perfil puede acceder a las aplicaciones, datos o archivos de otro perfil, lo que también significa que no hay una manera fácil para que los usuarios de diferentes perfiles intercambien información.
4 |
5 | Con esta aplicación, puedes seleccionar qué información deseas compartir con otros perfiles en el mismo dispositivo. Se mostrarán notificaciones sobre los datos compartidos en los otros perfiles, permitiéndote descargar, compartir o copiar desde el perfil donde se encuentran los datos.
6 |
7 | Toda la comunicación entre perfiles ocurre localmente sin requerir conexión a Internet, uso de servidores o participación de servicios de terceros.
8 |
9 | Sin anuncios. Sin rastreo. Sin compras dentro de la aplicación. Software libre de código abierto.
--------------------------------------------------------------------------------
/metadata/es-ES/short_description.txt:
--------------------------------------------------------------------------------
1 | Comparte medios y texto fácilmente entre perfiles de usuario de Android
--------------------------------------------------------------------------------
/metadata/es-ES/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/fr-FR/full_description.txt:
--------------------------------------------------------------------------------
1 | Un outil simple qui permet de partager des médias et du texte avec un autre profil utilisateur Android aussi facilement que de les transférer à une autre application.
2 |
3 | Les profils utilisateurs Android permettent d'isoler les applications, permettant aux utilisateurs de séparer les applications sensibles des applications moins fiables. Aucun profil ne peut accéder aux applications, aux données ou aux fichiers d'un autre profil, ce qui signifie également qu'il n'existe pas de moyen simple pour les utilisateurs des différents profils d'échanger des informations.
4 |
5 | Avec cette application, vous pouvez sélectionner les informations que vous souhaitez partager avec d'autres profils sur le même appareil. Des notifications concernant les données partagées s'afficheront dans les autres profils, permettant de les télécharger, les partager ou les copier depuis le profil source.
6 |
7 | Toute la communication entre les profils s'effectue localement sans connexion Internet, sans serveur et sans intervention de services tiers.
8 |
9 | Pas de publicité. Pas de traçage. Pas d'achats intégrés. Logiciel libre et open source.
--------------------------------------------------------------------------------
/metadata/fr-FR/short_description.txt:
--------------------------------------------------------------------------------
1 | Partagez facilement médias et textes entre profils utilisateurs Android
--------------------------------------------------------------------------------
/metadata/fr-FR/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/it-IT/full_description.txt:
--------------------------------------------------------------------------------
1 | Un semplice strumento per condividere contenuti con altri profili utente Android, con la stessa facilità di condividerli con un'altra app.
2 |
3 | I profili utente di Android permettono di isolare le app, consentendo di separare le applicazioni sensibili da quelle meno affidabili. Nessun profilo può accedere alle app, ai dati o ai file di un altro profilo, ma questo significa anche che non esiste un modo semplice per scambiare informazioni tra i profili.
4 |
5 | Con questa app puoi scegliere quali contenuti condividere con altri profili sullo stesso dispositivo. Le notifiche dei contenuti condivisi appariranno negli altri profili, permettendo di scaricarli, condividerli o copiarli dal profilo di origine.
6 |
7 | Tutta la comunicazione tra i profili avviene localmente senza connessione Internet, senza server e senza servizi di terze parti.
8 |
9 | Niente pubblicità. Niente tracciamento. Niente acquisti in-app. Software open source gratuito.
--------------------------------------------------------------------------------
/metadata/it-IT/short_description.txt:
--------------------------------------------------------------------------------
1 | Condividi facilmente contenuti tra i profili utente Android
--------------------------------------------------------------------------------
/metadata/it-IT/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/ja-JP/full_description.txt:
--------------------------------------------------------------------------------
1 | 別のアプリに転送するように、Android端末の異なるユーザープロファイル間で簡単にメディアやテキストを共有できるシンプルなツールです。
2 |
3 | Androidのユーザープロファイル機能により、信頼性の低いアプリから重要なアプリを分離できます。各プロファイルは他のプロファイルのアプリやデータにアクセスできないため、プロファイル間での情報共有が困難でした。
4 |
5 | 本アプリを使用すると:
6 | - 同一端末の他のプロファイルと共有したい情報を選択できます
7 | - 共有されたデータの通知が他のプロファイルに表示されます
8 | - データのダウンロード、共有、コピーが可能です
9 |
10 | 特徴:
11 | - インターネット接続不要
12 | - すべての通信は端末内で完結
13 | - 外部サーバーや第三者サービスを使用しません
14 |
15 | 広告なし・トラッキングなし・アプリ内課金なし・オープンソースの無料ソフトウェアです。
--------------------------------------------------------------------------------
/metadata/ja-JP/short_description.txt:
--------------------------------------------------------------------------------
1 | アプリ間で簡単にメディアとテキストを共有できるAndroidプロファイル向けツール
--------------------------------------------------------------------------------
/metadata/ja-JP/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/ko-KR/full_description.txt:
--------------------------------------------------------------------------------
1 | 다른 앱과 공유하듯이 간단하게 다른 안드로이드 사용자 프로필과 미디어 및 텍스트를 공유할 수 있는 도구입니다.
2 |
3 | 안드로이드 사용자 프로필을 사용하면 앱을 서로 격리할 수 있어 신뢰할 수 있는 앱과 그렇지 않은 앱을 분리할 수 있습니다. 프로필 간에는 앱, 앱 데이터, 파일에 접근할 수 없어 정보 교환이 어려웠습니다.
4 |
5 | 이 앱을 통해 같은 기기의 다른 프로필과 공유하고 싶은 정보를 선택할 수 있습니다. 다른 프로필에서는 공유된 데이터에 대한 알림을 받아 원본 프로필에서 다운로드, 공유 또는 복사할 수 있습니다.
6 |
7 | 프로필 간의 모든 통신은 인터넷 연결, 서버 사용, 제3자 서비스 없이 로컬에서 이루어집니다.
8 |
9 | 광고 없음・추적 없음・인앱 결제 없음・무료 오픈소스 소프트웨어
--------------------------------------------------------------------------------
/metadata/ko-KR/short_description.txt:
--------------------------------------------------------------------------------
1 | 안드로이드 프로필 간 간편한 미디어/텍스트 공유
--------------------------------------------------------------------------------
/metadata/ko-KR/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/pt-PT/full_description.txt:
--------------------------------------------------------------------------------
1 | Uma ferramenta simples que permite compartilhar mídia e texto com outro perfil de usuário do Android tão facilmente quanto encaminhar para outro aplicativo.
2 |
3 | Os perfis de usuário do Android permitem isolar aplicativos, possibilitando que usuários mantenham aplicativos sensíveis separados dos menos confiáveis. Nenhum perfil pode acessar os aplicativos, dados ou arquivos de outro perfil, mas isso também significa que não há uma maneira fácil para os usuários de diferentes perfis trocarem informações.
4 |
5 | Com este aplicativo, você pode selecionar quais informações deseja compartilhar com outros perfis no mesmo dispositivo. Notificações sobre os dados compartilhados serão exibidas nos outros perfis, permitindo baixar, compartilhar ou copiar do perfil onde os dados estão localizados.
6 |
7 | Toda a comunicação entre perfis acontece localmente sem necessidade de conexão com a Internet, uso de servidores ou envolvimento de serviços de terceiros.
8 |
9 | Sem anúncios. Sem rastreamento. Sem compras no aplicativo. Software livre de código aberto.
--------------------------------------------------------------------------------
/metadata/pt-PT/short_description.txt:
--------------------------------------------------------------------------------
1 | Compartilhe mídia e texto facilmente entre perfis de usuário do Android
--------------------------------------------------------------------------------
/metadata/pt-PT/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/ru-RU/full_description.txt:
--------------------------------------------------------------------------------
1 | Простой инструмент, позволяющий делиться медиафайлами и текстом между профилями Android так же легко, как с любым другим приложением.
2 |
3 | Профили Android позволяют изолировать приложения друг от друга, что дает возможность отделить важные приложения от менее надежных. Ни один профиль не может получить доступ к приложениям, данным или файлам другого профиля, но это также означает, что пользователям разных профилей непросто обмениваться информацией.
4 |
5 | С помощью этого приложения вы можете выбрать, какой информацией поделиться с другими профилями на том же устройстве. В других профилях появятся уведомления о доступных данных, позволяющие загрузить, переслать или скопировать их из исходного профиля.
6 |
7 | Весь обмен данными между профилями происходит локально, без подключения к интернету, без серверов и без участия сторонних сервисов.
8 |
9 | Без рекламы. Без отслеживания. Без встроенных покупок. Бесплатное приложение с открытым исходным кодом.
--------------------------------------------------------------------------------
/metadata/ru-RU/short_description.txt:
--------------------------------------------------------------------------------
1 | Простой обмен файлами и текстом между профилями Android
--------------------------------------------------------------------------------
/metadata/ru-RU/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/metadata/zh-CN/full_description.txt:
--------------------------------------------------------------------------------
1 | 这是一个简单的工具,可以像转发到其他应用一样轻松地与其他 Android 用户档案分享媒体和文本。
2 |
3 | Android 用户档案功能允许隔离应用程序,使用户能够将敏感应用与不太可信的应用分开。每个档案都无法访问其他档案的应用、应用数据或文件。但这也意味着档案之间没有简单的信息交换方式。
4 |
5 | 使用此应用,您可以选择要与同一设备上的其他档案分享的信息。其他档案会收到关于共享数据的通知,允许您从源档案下载、转发或复制这些数据。
6 |
7 | 所有档案间通信都在本地进行,无需互联网连接、服务器或第三方服务。
8 |
9 | 无广告。无跟踪。无应用内购买。免费开源软件。
--------------------------------------------------------------------------------
/metadata/zh-CN/short_description.txt:
--------------------------------------------------------------------------------
1 | 在 Android 用户档案之间轻松分享媒体和文本
--------------------------------------------------------------------------------
/metadata/zh-CN/title.txt:
--------------------------------------------------------------------------------
1 | Inter Profile Sharing
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Inter Profile Sharing"
23 | include(":app")
24 |
--------------------------------------------------------------------------------