feat: import/export cross profile intent filters (#240)

Add intent filter presets
This commit is contained in:
BinTianqi
2026-02-12 15:19:50 +08:00
parent 64191761b7
commit baaf2871e7
8 changed files with 247 additions and 76 deletions

View File

@@ -494,7 +494,11 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
) )
} }
composable<CrossProfileIntentFilter> { composable<CrossProfileIntentFilter> {
CrossProfileIntentFilterScreen(vm::addCrossProfileIntentFilter, ::navigateUp) CrossProfileIntentFilterScreen(
vm::addCrossProfileIntentFilter, vm::clearCrossProfileIntentFilters,
vm::importCrossProfileIntentFilters, vm::exportCrossProfileIntentFilters,
::navigateUp
)
} }
composable<DeleteWorkProfile> { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) } composable<DeleteWorkProfile> { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) }

View File

@@ -57,6 +57,7 @@ class ManageSpaceActivity: FragmentActivity() {
cacheDir.deleteRecursively() cacheDir.deleteRecursively()
codeCacheDir.deleteRecursively() codeCacheDir.deleteRecursively()
if(Build.VERSION.SDK_INT >= 24) { if(Build.VERSION.SDK_INT >= 24) {
dataDir.resolve("databases").deleteRecursively()
dataDir.resolve("shared_prefs").deleteRecursively() dataDir.resolve("shared_prefs").deleteRecursively()
} else { } else {
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE) val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)

View File

@@ -4,12 +4,13 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 4) { class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 5) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
db.execSQL(DHIZUKU_CLIENTS_TABLE) db.execSQL(DHIZUKU_CLIENTS_TABLE)
db.execSQL(SECURITY_LOGS_TABLE) db.execSQL(SECURITY_LOGS_TABLE)
db.execSQL(NETWORK_LOGS_TABLE) db.execSQL(NETWORK_LOGS_TABLE)
db.execSQL(APP_GROUPS_TABLE) db.execSQL(APP_GROUPS_TABLE)
db.execSQL(CP_INTENTS_TABLE)
} }
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) { if (oldVersion < 2) {
@@ -21,6 +22,9 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 4) {
if (oldVersion < 4) { if (oldVersion < 4) {
db.execSQL(APP_GROUPS_TABLE) db.execSQL(APP_GROUPS_TABLE)
} }
if (oldVersion < 5) {
db.execSQL(CP_INTENTS_TABLE)
}
} }
companion object { companion object {
const val DHIZUKU_CLIENTS_TABLE = "CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," + const val DHIZUKU_CLIENTS_TABLE = "CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," +
@@ -33,5 +37,7 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 4) {
const val APP_GROUPS_TABLE = "CREATE TABLE app_groups(" + const val APP_GROUPS_TABLE = "CREATE TABLE app_groups(" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," + "id INTEGER PRIMARY KEY AUTOINCREMENT," +
"name TEXT, apps TEXT)" "name TEXT, apps TEXT)"
const val CP_INTENTS_TABLE = "CREATE TABLE cross_profile_intent_filters (" +
"action_str TEXT, category TEXT, mime_type TEXT, direction INTEGER)"
} }
} }

View File

@@ -10,6 +10,7 @@ import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import com.bintianqi.owndroid.dpm.AppGroup import com.bintianqi.owndroid.dpm.AppGroup
import com.bintianqi.owndroid.dpm.IntentFilterOptions
import com.bintianqi.owndroid.dpm.NetworkLog import com.bintianqi.owndroid.dpm.NetworkLog
import com.bintianqi.owndroid.dpm.SecurityEvent import com.bintianqi.owndroid.dpm.SecurityEvent
import com.bintianqi.owndroid.dpm.SecurityEventWithData import com.bintianqi.owndroid.dpm.SecurityEventWithData
@@ -248,4 +249,29 @@ class MyRepository(val dbHelper: MyDbHelper) {
fun deleteAppGroup(id: Int) { fun deleteAppGroup(id: Int) {
dbHelper.writableDatabase.delete("app_groups", "id = ?", arrayOf(id.toString())) dbHelper.writableDatabase.delete("app_groups", "id = ?", arrayOf(id.toString()))
} }
fun setCrossProfileIntentFilter(data: IntentFilterOptions) {
val cv = ContentValues()
cv.put("action_str", data.action)
cv.put("category", data.category)
cv.put("mime_type", data.mimeType)
cv.put("direction", data.direction)
dbHelper.writableDatabase.insert("cross_profile_intent_filters", null, cv)
}
fun getAllCrossProfileIntentFilters(): List<IntentFilterOptions> {
val list = mutableListOf<IntentFilterOptions>()
dbHelper.readableDatabase.rawQuery(
"SELECT * FROM cross_profile_intent_filters", null
).use {
while (it.moveToNext()) {
list += IntentFilterOptions(
it.getString(0), it.getString(1), it.getString(2), it.getInt(3)
)
}
}
return list
}
fun deleteAllCrossProfileIntentFilters() {
dbHelper.writableDatabase.delete("cross_profile_intent_filters", null, null);
}
} }

View File

@@ -73,7 +73,6 @@ import com.bintianqi.owndroid.dpm.DelegatedAdmin
import com.bintianqi.owndroid.dpm.DeviceAdmin import com.bintianqi.owndroid.dpm.DeviceAdmin
import com.bintianqi.owndroid.dpm.FrpPolicyInfo import com.bintianqi.owndroid.dpm.FrpPolicyInfo
import com.bintianqi.owndroid.dpm.HardwareProperties import com.bintianqi.owndroid.dpm.HardwareProperties
import com.bintianqi.owndroid.dpm.IntentFilterDirection
import com.bintianqi.owndroid.dpm.IntentFilterOptions import com.bintianqi.owndroid.dpm.IntentFilterOptions
import com.bintianqi.owndroid.dpm.IpMode import com.bintianqi.owndroid.dpm.IpMode
import com.bintianqi.owndroid.dpm.KeyguardDisableConfig import com.bintianqi.owndroid.dpm.KeyguardDisableConfig
@@ -1417,13 +1416,28 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
val filter = IntentFilter(options.action) val filter = IntentFilter(options.action)
if (options.category.isNotEmpty()) filter.addCategory(options.category) if (options.category.isNotEmpty()) filter.addCategory(options.category)
if (options.mimeType.isNotEmpty()) filter.addDataType(options.mimeType) if (options.mimeType.isNotEmpty()) filter.addDataType(options.mimeType)
val flags = when(options.direction) { DPM.addCrossProfileIntentFilter(DAR, filter, options.direction)
IntentFilterDirection.ToManaged -> DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED myRepo.setCrossProfileIntentFilter(options)
IntentFilterDirection.ToParent -> DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT }
IntentFilterDirection.Both -> DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED or fun clearCrossProfileIntentFilters() {
DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT DPM.clearCrossProfileIntentFilters(DAR)
myRepo.deleteAllCrossProfileIntentFilters()
}
fun importCrossProfileIntentFilters(uri: Uri) {
val bytes = application.contentResolver.openInputStream(uri)!!.use {
it.readBytes().decodeToString()
}
val data = Json.decodeFromString<List<IntentFilterOptions>>(bytes)
data.forEach {
addCrossProfileIntentFilter(it)
}
}
fun exportCrossProfileIntentFilters(uri: Uri) {
val data = myRepo.getAllCrossProfileIntentFilters()
val bytes = Json.encodeToString(data).encodeToByteArray()
application.contentResolver.openOutputStream(uri)!!.use {
it.write(bytes)
} }
DPM.addCrossProfileIntentFilter(DAR, filter, flags)
} }
val UM = application.getSystemService(Context.USER_SERVICE) as UserManager val UM = application.getSystemService(Context.USER_SERVICE) as UserManager

View File

@@ -4,34 +4,46 @@ import android.app.admin.DevicePolicyManager
import android.app.admin.DevicePolicyManager.WIPE_EUICC import android.app.admin.DevicePolicyManager.WIPE_EUICC
import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build.VERSION import android.os.Build.VERSION
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -45,20 +57,24 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.BottomPadding
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.adaptiveInsets
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.CircularProgressDialog import com.bintianqi.owndroid.ui.CircularProgressDialog
import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem
import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.Notes
import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.yesOrNo import com.bintianqi.owndroid.yesOrNo
@@ -240,21 +256,24 @@ fun SuspendPersonalAppScreen(
} }
} }
@Serializable
data class IntentFilterOptions( data class IntentFilterOptions(
val action: String, val category: String, val mimeType: String, val action: String, val category: String, val mimeType: String,
val direction: IntentFilterDirection val direction: Int // 1: private to work, 2: work to private, 3: both
)
val crossProfileIntentFilterPresets = mapOf(
R.string.allow_file_sharing to
IntentFilterOptions(Intent.ACTION_SEND, Intent.CATEGORY_DEFAULT, "*/*", 3)
) )
enum class IntentFilterDirection(val text: Int) {
ToParent(R.string.work_to_personal), ToManaged(R.string.personal_to_work),
Both(R.string.both_direction)
}
@Serializable object CrossProfileIntentFilter @Serializable object CrossProfileIntentFilter
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CrossProfileIntentFilterScreen( fun CrossProfileIntentFilterScreen(
addFilter: (IntentFilterOptions) -> Unit, addFilter: (IntentFilterOptions) -> Unit, clearFilters: () -> Unit,
importFilters: (Uri) -> Unit, exportFilters: (Uri) -> Unit,
onNavigateUp: () -> Unit onNavigateUp: () -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -265,12 +284,87 @@ fun CrossProfileIntentFilterScreen(
var customMimeType by remember { mutableStateOf(false) } var customMimeType by remember { mutableStateOf(false) }
var mimeType by remember { mutableStateOf("") } var mimeType by remember { mutableStateOf("") }
var dropdown by remember { mutableStateOf(false) } var dropdown by remember { mutableStateOf(false) }
var direction by remember { mutableStateOf(IntentFilterDirection.Both) } var direction by remember { mutableIntStateOf(3) }
MyScaffold(R.string.intent_filter, onNavigateUp) { var dialog by remember { mutableStateOf(false) }
val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
if (it != null) {
importFilters(it)
context.showOperationResultToast(true)
}
}
val exportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) {
if (it != null) {
exportFilters(it)
context.showOperationResultToast(true)
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.intent_filter)) },
navigationIcon = { NavIcon(onNavigateUp) },
actions = {
var menu by remember { mutableStateOf(false) }
Box {
IconButton({ menu = !menu }) {
Icon(Icons.Default.MoreVert, null)
}
DropdownMenu(menu, { menu = false }) {
DropdownMenuItem(
{ Text(stringResource(R.string.presets)) },
{
dialog = true
menu = false
},
leadingIcon = {
Icon(painterResource(R.drawable.list_fill0), null)
}
)
DropdownMenuItem(
{ Text(stringResource(R.string.import_str)) },
{
importLauncher.launch(arrayOf("application/json"))
menu = false
},
leadingIcon = {
Icon(painterResource(R.drawable.file_open_fill0), null)
}
)
DropdownMenuItem(
{ Text(stringResource(R.string.export)) },
{
exportLauncher.launch("owndroid_intent_filters")
menu = false
},
leadingIcon = {
Icon(painterResource(R.drawable.file_export_fill0), null)
}
)
}
}
}
)
},
contentWindowInsets = adaptiveInsets()
) { paddingValues ->
val directionTextMap = mapOf(
1 to R.string.personal_to_work,
2 to R.string.work_to_personal,
3 to R.string.both_direction
)
Column(
Modifier
.padding(paddingValues)
.padding(horizontal = HorizontalPadding)
.verticalScroll(rememberScrollState())
) {
OutlinedTextField( OutlinedTextField(
value = action, onValueChange = { action = it }, value = action, onValueChange = { action = it },
label = { Text("Action") }, label = { Text("Action") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }), keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@@ -296,15 +390,17 @@ fun CrossProfileIntentFilterScreen(
} }
ExposedDropdownMenuBox(dropdown, { dropdown = it }, Modifier.padding(vertical = 5.dp)) { ExposedDropdownMenuBox(dropdown, { dropdown = it }, Modifier.padding(vertical = 5.dp)) {
OutlinedTextField( OutlinedTextField(
stringResource(direction.text), {}, stringResource(directionTextMap[direction]!!), {},
Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth(), Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
label = { Text(stringResource(R.string.direction)) }, readOnly = true, label = { Text(stringResource(R.string.direction)) }, readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(dropdown) } trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(dropdown) }
) )
ExposedDropdownMenu(dropdown, { dropdown = false }) { ExposedDropdownMenu(dropdown, { dropdown = false }) {
IntentFilterDirection.entries.forEach { directionTextMap.forEach {
DropdownMenuItem({ Text(stringResource(it.text)) }, { DropdownMenuItem({ Text(stringResource(it.value)) }, {
direction = it direction = it.key
dropdown = false dropdown = false
}) })
} }
@@ -312,9 +408,7 @@ fun CrossProfileIntentFilterScreen(
} }
Button( Button(
{ {
addFilter(IntentFilterOptions( addFilter(IntentFilterOptions(action, category, mimeType, direction))
action, category, mimeType, direction
))
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -325,7 +419,7 @@ fun CrossProfileIntentFilterScreen(
} }
Button( Button(
onClick = { onClick = {
Privilege.DPM.clearCrossProfileIntentFilters(Privilege.DAR) clearFilters()
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp) modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
@@ -333,6 +427,28 @@ fun CrossProfileIntentFilterScreen(
Text(stringResource(R.string.clear_cross_profile_filters)) Text(stringResource(R.string.clear_cross_profile_filters))
} }
Notes(R.string.info_cross_profile_intent_filter) Notes(R.string.info_cross_profile_intent_filter)
Spacer(Modifier.height(BottomPadding))
}
if (dialog) AlertDialog(
title = { Text(stringResource(R.string.presets)) },
text = {
crossProfileIntentFilterPresets.forEach {
Button({
addFilter(it.value)
context.showOperationResultToast(true)
dialog = false
}) {
Text(stringResource(it.key))
}
}
},
confirmButton = {
TextButton({ dialog = false }) {
Text(stringResource(R.string.cancel))
}
},
onDismissRequest = { dialog = false }
)
} }
} }

View File

@@ -331,6 +331,8 @@
<string name="profile_max_time_out_desc">工作资料处于关闭状态的时间达到该限制后会挂起个人应用0为无限制</string> <string name="profile_max_time_out_desc">工作资料处于关闭状态的时间达到该限制后会挂起个人应用0为无限制</string>
<string name="personal_app_suspended_because_timeout">个人应用已经因此挂起:%1$s</string> <string name="personal_app_suspended_because_timeout">个人应用已经因此挂起:%1$s</string>
<string name="intent_filter">Intent过滤器</string> <string name="intent_filter">Intent过滤器</string>
<string name="presets">预设</string>
<string name="allow_file_sharing">允许分享文件</string>
<string name="direction">方向</string> <string name="direction">方向</string>
<string name="both_direction">双向</string> <string name="both_direction">双向</string>
<string name="work_to_personal">工作到个人</string> <string name="work_to_personal">工作到个人</string>

View File

@@ -365,6 +365,8 @@
<string name="profile_max_time_out_desc">Personal apps will be suspended after the work profile is closed for this amount of time. 0 means no limit. </string> <string name="profile_max_time_out_desc">Personal apps will be suspended after the work profile is closed for this amount of time. 0 means no limit. </string>
<string name="personal_app_suspended_because_timeout">Personal app suspended because of this: %1$s</string> <string name="personal_app_suspended_because_timeout">Personal app suspended because of this: %1$s</string>
<string name="intent_filter">Intent filter</string> <string name="intent_filter">Intent filter</string>
<string name="presets">Presets</string>
<string name="allow_file_sharing">Allow file sharing</string>
<string name="direction">Direction</string> <string name="direction">Direction</string>
<string name="both_direction">Both direction</string> <string name="both_direction">Both direction</string>
<string name="work_to_personal">Work to personal</string> <string name="work_to_personal">Work to personal</string>