Compare commits

...
Author SHA1 Message Date
maxjivi05 4798ee84d5 fix(playstore): translate the strings merged from main and clear release lint
Every locale gets the 77 strings that main added without translations.
The percent HUD labels are marked unformatted, O_CLOEXEC is only used
from API 27, and the top-desktop key route matches the other
RestrictedApi call sites.
2026-11-06 07:35:58 -05:00
maxjivi05 95cb9159de Merge main into playstore for 0.6.9.2 (version code 20)
Brings the main fixes and features since 8a8bb2053 to the Play line.
Main's new code keeps the Play gates: bundled PRoot, AddonPack and
ComponentDownloads gates, the SDK guards, PackageInfoCompat and the
accessibility consent check, which now also covers system-key capture.
The accessibility disclosure is rewritten in every locale to describe
the hardware-keyboard keys it keeps in a session. The MaliBridge server
task uses the AGP NDK provider. Review notes are in tools/playstore/POLICY.md.
2026-11-06 06:15:35 -05:00
maxjivi05 6ea7b10d94 fix: drain archive padding before waiting on decoders 2026-11-04 14:13:56 -05:00
maxjivi05 527fadd3b3 ci: allow cold Play component preparation to finish 2026-11-04 14:04:39 -05:00
maxjivi05 6affa87442 test: stabilize hosted dependency and Vulkan checks 2026-11-04 13:07:41 -05:00
maxjivi05 5d10c8e96d PlayStore: deliver runtime modules and resources on demand
Preserve component file inventories, reuse setup controls, and keep Play-managed resources behind a read-only guest stream. Build version 0.6.9.2 with version code 19 and support large signed release assets.
2026-11-03 13:27:50 -05:00
Kirk d25f6940ee fix: the navigation bar stays dark when the launcher is not fullscreen
From Android 14 the Material theme leaves the navigation bar
transparent, so with launcher fullscreen off a white system bar showed
under the dark launcher. AppTheme now sets the navigation bar to black,
matching the status bar.
2026-11-03 13:25:08 -05:00
Kurt Himebauch d1707dc1b7 fix: Project Zomboid runs on G1 and the C1 JIT instead of ZGC (#2016) 2026-11-03 13:25:08 -05:00
MaxsTechReview 6588185e96 fix(runtime): keep the Linux desktop installed across a runtime update (#2021)
* fix(runtime): keep the Linux desktop installed across a runtime update

A runtime update replaces the whole runtime tree, and the desktop pack
is built against one runtime, so every update removed the installed
desktop. Since the runtime started updating by itself, this happened
without the user doing anything: the next Open showed "Install & open"
and downloaded the whole desktop again.

When the desktop was installed before the update, the same update job
now installs the new runtime's desktop pack right after the runtime,
with the update's progress. If that install fails, the runtime update
still counts as done and Open offers the install as before. A repair
from the session no longer installs the desktop a second time when the
runtime install already did.

* fix(runtime): roll back a failed desktop restore after a runtime update

If putting the desktop back fails after its archive started unpacking,
the install journal stayed behind, and every later launch, Steam
included, tried to resume it and stopped when that failed. The update
now rolls back the partial desktop with the desktop removal, which
restores the runtime's own files and clears the journal. Open then
offers the desktop install as before.

Whether the desktop was installed is now checked on its own, so a
repair of a damaged runtime also restores it.

* fix(session): do not install the desktop again after resuming it

Opening the desktop with an interrupted desktop install resumed that
install and then installed the desktop a second time. The second
install is now skipped when the resume already completed the desktop.
2026-11-03 13:25:08 -05:00
Kurt Himebauch dcd955e175 fix: a stale session's startup failure no longer stops the newer session (#1984)
* fix: a stale session's startup failure no longer stops the newer session

* fix: stale session failures and queued-launch cleanup are checked against the current session under the stop lock
2026-11-03 13:25:08 -05:00
Kurt Himebauch 77a332e615 fix: download monitor polls off the main thread so slow storage can't ANR the session (#2020)
* fix: download monitor polls off the main thread so slow storage can't ANR the session

* fix: cancel the queued download poll before the service shuts its executor down

* fix: ignore download monitor submissions that arrive after the service shut it down
2026-11-03 13:25:08 -05:00
maxjivi05 c59884b42e Use sequential Play version codes for 0.6.9.1 2026-11-03 07:43:48 -05:00
maxjivi05 d2298f4109 Use the pinned system compressor for Play payload preparation 2026-11-03 06:25:55 -05:00
maxjivi05 567ca34a48 Complete pinned Play bundle inputs and guest prerequisite delivery 2026-11-03 00:04:07 -05:00
maxjivi05 2f6e43e3d1 Prepare isolated Play bundle builds and distribution safeguards 2026-11-02 22:28:18 -05:00
maxjivi05 773d37ff21 Add privacy policy for Play distribution 2026-11-02 14:15:24 -05:00
maxjivi05 8a8bb2053c Merge main into playstore and harden SDK compatibility
Bring in the ten upstream commits through 55362d68 without rewriting contributor history.

Guard the bottom-screen accessibility watcher on API 33, remove a duplicate HOME query, translate 22 new strings across 32 locales and clean the extracted candidate when runtime installation is cancelled before replacement. Add cancellation coverage while retaining rollback protection.

Validation: 3,293 JVM tests, 1,623 Python tests (eight skips), sanitized GPU harness, release APK and app bundle builds, full lint with zero errors, runtime assets and native dependency/export checks.

Physical verification of this exact candidate, performance parity and native 16 KiB audio support remain pending.
2026-11-02 08:51:44 -05:00
maxjivi05 b14587baeb Target Android SDK 37 for Play Store review
Integrate current main while preserving the original SDK migration history.
Update the Android toolchain, runtime permissions, foreground service lifecycle, storage access, native execution and platform API compatibility. Complete translations and regression coverage.

Validation: 3,260 JVM tests, 1,620 Python tests (eight skips), 171 Rust tests, six native regression programs and release lint with zero errors. Signed APK upgrade installed on the requested device. Signed app bundle validated with bundletool.

Review candidate: physical before/after feature checks, performance parity and native 16 KiB audio compatibility remain pending. Play Console acceptance has not been verified.
2026-11-02 08:17:51 -05:00
Kurt Himebauch 28a5423629 Merge main and compile and target Android API 36 2026-10-23 10:32:35 -04:00
Kurt Himebauch 74977d1738 Merge current main into PR #564 2026-10-22 13:11:02 -04:00
Kurt Himebauch 184409f33c Merge current main into PR #564 2026-10-22 12:13:46 -04:00
xXJsonDeruloXx 42239e0097 fix: repair Android 36 runtime edge cases 2026-10-22 10:46:06 -04:00
xXJsonDeruloXx c117f6f4c7 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-18 16:14:27 -04:00
xXJsonDeruloXx 4a93663d14 chore: merge main to resolve conflicts with the API 36 target 2026-10-18 16:09:55 -04:00
xXJsonDeruloXx 1ca2d9bd49 Remove stale comments from statx patch 2026-10-13 21:44:00 -04:00
xXJsonDeruloXx c8baca10e6 Merge origin/main into feat/target-sdk-36 2026-10-13 21:38:55 -04:00
xXJsonDeruloXx 9922823090 fix: the Setup permission rows refresh when the app returns to the front 2026-10-11 10:44:19 -04:00
xXJsonDeruloXx a760aea276 perf: relocated Wine code is shared between processes through a per-session cache file 2026-10-11 10:08:10 -04:00
xXJsonDeruloXx e079f9602f fix: a session starts from the last added-games listing and refreshes it in the background 2026-10-11 10:08:10 -04:00
xXJsonDeruloXx dfc0b95557 feat: ask for all files access and offer storage and Bluetooth permissions in Setup 2026-10-11 10:08:10 -04:00
xXJsonDeruloXx 30270ca654 test: pin Robolectric to SDK 35 for the Java 17 build 2026-10-11 02:07:23 -04:00
xXJsonDeruloXx 8c16a04d8d fix: data sync services stop cleanly when Android 15 ends their daily time 2026-10-11 02:07:23 -04:00
xXJsonDeruloXx 0fd50d76b2 feat: target Android API 36 2026-10-11 02:07:23 -04:00
406 changed files with 20452 additions and 705 deletions

No files matched your search

+104 -15
View File
@@ -2,6 +2,17 @@ name: Build APK
run-name: ${{ github.event_name == 'pull_request' && format('building pr {0} on {1}', github.event.pull_request.number, github.event.pull_request.head.sha) || format('building {0} on {1}', github.ref_name, github.sha) }}
on:
workflow_call:
inputs:
play_bundle:
type: boolean
default: false
source_sha:
type: string
default: ''
play_version_code:
type: string
default: ''
push:
branches: [ main ]
paths-ignore:
@@ -32,7 +43,7 @@ permissions:
pull-requests: write
concurrency:
group: build-${{ github.event.pull_request.number || (github.event_name == 'push' && 'main') || github.run_id }}
group: build-${{ github.event.pull_request.number || (github.event_name == 'push' && github.ref_name) || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
jobs:
@@ -47,13 +58,27 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ref: ${{ inputs.source_sha || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Validate Play distribution inputs
if: inputs.play_bundle == true
env:
PLAY_SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
[[ $PLAY_SOURCE_SHA =~ ^[0-9a-f]{40}$ ]]
test "$(git rev-parse HEAD)" = "$PLAY_SOURCE_SHA"
git merge-base --is-ancestor "$PLAY_SOURCE_SHA" origin/playstore
test -f tools/playstore/components.lock.json || {
echo "::error::Production component manifest is incomplete; the Play bundle cannot be built or signed."
exit 1
}
python3 tools/playstore/payloads.py validate --play-only --manifest tools/playstore/components.lock.json
- name: Pin and merge the APK source
id: merge
if: github.event_name == 'pull_request'
@@ -86,7 +111,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ref: ${{ inputs.source_sha || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
fetch-depth: 0
sparse-checkout: |
/*
@@ -145,7 +170,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ref: ${{ inputs.source_sha || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
fetch-depth: 0
sparse-checkout: |
/*
@@ -323,13 +348,13 @@ jobs:
name: Compile APK and Android tests
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 45
timeout-minutes: ${{ inputs.play_bundle && 90 || 45 }}
outputs:
variants: ${{ steps.variants.outputs.list }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ref: ${{ inputs.source_sha || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
fetch-depth: 0
sparse-checkout: |
/*
@@ -338,6 +363,16 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Reserve hosted runner space for Play components
if: inputs.play_bundle == true
run: |
set -euo pipefail
test "$GITHUB_ACTIONS" = true
test "$RUNNER_ENVIRONMENT" = github-hosted
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/share/boost /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h "$GITHUB_WORKSPACE" "$RUNNER_TEMP"
- name: Check human authorship
env:
EVENT_NAME: ${{ github.event_name }}
@@ -453,7 +488,7 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
java-version: '21'
- uses: gradle/actions/setup-gradle@v4
with:
@@ -469,8 +504,8 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BUILD_REF: ${{ github.ref_name }}
BUILD_SHA: ${{ github.sha }}
BUILD_REF: ${{ inputs.play_bundle && 'playstore' || github.ref_name }}
BUILD_SHA: ${{ inputs.source_sha || github.sha }}
run: |
if [[ -n "$(git status --porcelain)" ]]; then
tree_state=dirty
@@ -685,6 +720,7 @@ jobs:
- uses: actions/upload-artifact@v4
id: apk-upload
if: inputs.play_bundle != true
with:
name: droiddeck-apk
path: app/build/outputs/apk/release/*.apk
@@ -692,7 +728,60 @@ jobs:
retention-days: 30
compression-level: 0
- name: Stage verified Play components
if: inputs.play_bundle == true
env:
DROIDDECK_ZSTD: /usr/bin/zstd
run: |
set -euo pipefail
bash tools/ci/install-ubuntu-packages.sh zstd
"$DROIDDECK_ZSTD" --version
python3 tools/playstore/payloads.py stage --play-only --manifest tools/playstore/components.lock.json \
--cache "$RUNNER_TEMP/play-component-cache" --output build/playstore-payloads
- name: Build and check the Play bundle
if: inputs.play_bundle == true
env:
PLAY_VERSION_CODE: ${{ inputs.play_version_code }}
run: |
set -euo pipefail
bash tools/ci/install-ubuntu-packages.sh autoconf automake libtool autopoint gettext pkg-config cmake ninja-build
python3 tools/playstore/build_audio.py build --output app/build/play-audio \
--ndk "$ANDROID_HOME/ndk/$NDK_VERSION" --cache "$RUNNER_TEMP/play-audio-cache"
./gradlew :app:bundleRelease :app:lintRelease :app:testReleaseUnitTest -PplayDistribution=true \
-PndkVersion="$NDK_VERSION" -PplayVersionCode="$PLAY_VERSION_CODE" --tests com.droiddeck.launcher.core.ComponentPayloadStoreTest \
--tests com.droiddeck.launcher.core.PlayDistributionTest \
--tests com.droiddeck.launcher.core.PlayArchiveFramesTest \
--tests com.droiddeck.launcher.PlayUpdatesTest \
--tests com.droiddeck.launcher.ui.BuildWatermarkTest --console=plain
python3 tools/playstore/payloads.py verify-bundle --play-only --manifest tools/playstore/components.lock.json \
--bundle app/build/outputs/bundle/release/app-release.aab
python3 tools/release/check_android_libraries.py app/build/outputs/bundle/release/app-release.aab --page-size 16384
python3 tools/playstore/check_audio_bundle.py --bundle app/build/outputs/bundle/release/app-release.aab \
--output app/build/play-audio --ndk "$ANDROID_HOME/ndk/$NDK_VERSION"
curl --fail --location --retry 3 --output "$RUNNER_TEMP/bundletool.jar" \
https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar
echo "a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29 $RUNNER_TEMP/bundletool.jar" | sha256sum -c -
java -jar "$RUNNER_TEMP/bundletool.jar" validate --bundle=app/build/outputs/bundle/release/app-release.aab
java -jar "$RUNNER_TEMP/bundletool.jar" dump manifest --bundle=app/build/outputs/bundle/release/app-release.aab \
--module=base > app/build/play-manifest.xml
python3 -c 'import xml.etree.ElementTree as E; root=E.parse("app/build/play-manifest.xml").getroot(); assert not any(x.get("{http://schemas.android.com/apk/res/android}name") == "android.permission.REQUEST_INSTALL_PACKAGES" for x in root.findall("uses-permission"))'
- uses: actions/upload-artifact@v4
if: inputs.play_bundle == true
with:
name: droiddeck-play-bundle-review
path: |
app/build/outputs/bundle/release/*.aab
build/playstore-payloads/provenance.json
build/playstore-payloads/index.json
app/build/play-manifest.xml
if-no-files-found: error
retention-days: 30
compression-level: 0
- uses: actions/upload-artifact@v4
if: inputs.play_bundle != true
with:
name: droiddeck-packs
path: app/build/packs/*.tar.zst
@@ -726,7 +815,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ref: ${{ inputs.source_sha || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
fetch-depth: 0
sparse-checkout: |
/*
@@ -780,7 +869,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ref: ${{ inputs.source_sha || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}
fetch-depth: 0
sparse-checkout: |
/*
@@ -852,7 +941,7 @@ jobs:
sign:
needs: [build, checks]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
if: ${{ !inputs.play_bundle && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
permissions:
contents: read
actions: read
@@ -864,7 +953,7 @@ jobs:
packs:
needs: [build, checks]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
if: ${{ !inputs.play_bundle && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
permissions:
contents: read
actions: read
@@ -873,7 +962,7 @@ jobs:
announce:
needs: [build, sign, packs]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
if: ${{ !inputs.play_bundle && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
@@ -923,6 +1012,6 @@ jobs:
catalog:
needs: announce
if: ${{ always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && needs.announce.result != 'skipped' }}
if: ${{ always() && !inputs.play_bundle && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && needs.announce.result != 'skipped' }}
uses: ./.github/workflows/update-catalog.yml
secrets: inherit
+199
View File
@@ -0,0 +1,199 @@
name: Build PlayStore Bundle
run-name: PlayStore ${{ inputs.version || 'branch build' }} from playstore
on:
workflow_call:
inputs:
version:
type: string
required: true
source_branch:
type: string
default: playstore
push:
branches: [playstore]
workflow_dispatch:
inputs:
version:
description: Version for the file names, e.g. 0.1.7
required: true
source_branch:
description: Branch to build
type: choice
options: [playstore]
default: playstore
required: true
permissions:
contents: read
actions: read
pull-requests: write
concurrency:
group: playstore-bundle
cancel-in-progress: false
jobs:
checks:
name: Play distribution checks
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'push' && github.sha || inputs.source_branch }}
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Install test dependencies
run: |
sudo apt-get update
sudo apt-get install -y zstd
python3 -m pip install PyYAML==6.0.3
- name: Test Play distribution safeguards
run: |
python3 -m unittest \
tools.tests.test_playstore_release_assets \
tools.tests.test_playstore_split_archive \
tools.tests.test_playstore_payloads \
tools.tests.test_playstore_manifest \
tools.tests.test_playstore_signing \
tools.tests.test_playstore_workflow \
tools.tests.test_playstore_archive_preparation \
tools.tests.test_playstore_audio_bundle \
tools.tests.test_playstore_emulators \
tools.tests.test_android_page_alignment
source:
runs-on: ubuntu-24.04
outputs:
sha: ${{ steps.source.outputs.sha }}
version: ${{ steps.source.outputs.version }}
version_code: ${{ steps.source.outputs.version_code }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'push' && github.sha || inputs.source_branch }}
persist-credentials: false
- id: source
env:
VERSION: ${{ inputs.version }}
SOURCE_BRANCH: ${{ inputs.source_branch || 'playstore' }}
run: |
set -euo pipefail
test "$SOURCE_BRANCH" = playstore
test "$GITHUB_REF" = refs/heads/playstore
sha=$(git rev-parse HEAD)
name=$(sed -n 's/^ *versionName "\(.*\)"$/\1/p' app/build.gradle)
[[ $name =~ ^[0-9A-Za-z._-]+$ ]]
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
[[ $VERSION =~ ^[0-9A-Za-z._-]+$ ]] || { echo "::error::invalid version"; exit 1; }
[[ $VERSION == "$name" || $VERSION == "$name"-* ]] || {
echo "::error::version must match versionName in app/build.gradle; bump versionName and versionCode first"
exit 1
}
else
VERSION="$name"
fi
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
source_code=$(sed -n 's/^ *versionCode \([0-9][0-9]*\)$/\1/p' app/build.gradle)
[[ $source_code =~ ^[1-9][0-9]*$ ]]
[ "$source_code" -le 2100000000 ]
echo "version_code=$source_code" >> "$GITHUB_OUTPUT"
bundle:
needs: source
uses: ./.github/workflows/build.yml
with:
play_bundle: true
source_sha: ${{ needs.source.outputs.sha }}
play_version_code: ${{ needs.source.outputs.version_code }}
secrets: inherit
sign:
needs: [checks, source, bundle]
runs-on: ubuntu-24.04
environment: signing-playstore
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.source.outputs.sha }}
persist-credentials: false
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: actions/download-artifact@v4
with:
name: droiddeck-play-bundle-review
path: incoming
- name: Sign the verified Play bundle
env:
RELEASE_KEYSTORE_B64: ${{ secrets.RELEASE_KEYSTORE_B64 }}
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
VERSION: ${{ needs.source.outputs.version }}
run: |
set -euo pipefail
test -n "$RELEASE_KEYSTORE_B64"
key="$RUNNER_TEMP/play-release.p12"
(umask 077; printf '%s' "$RELEASE_KEYSTORE_B64" | base64 -d > "$key")
mapfile -t bundles < <(find incoming -type f -name '*.aab')
test "${#bundles[@]}" = 1
mkdir -p signed
python3 tools/playstore/sign_bundle.py "${bundles[0]}" "signed/DroidDeck-$VERSION-PlayStore.aab" \
--keystore "$key"
(cd signed && sha256sum *.aab > SHA256SUMS)
- uses: actions/upload-artifact@v4
with:
name: droiddeck-play-bundle-signed
path: signed/*
if-no-files-found: error
compression-level: 0
publish:
needs: [source, sign]
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.source.outputs.sha }}
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: droiddeck-play-bundle-signed
path: signed
- name: Publish the PlayStore release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.source.outputs.version }}
SOURCE_SHA: ${{ needs.source.outputs.sha }}
VERSION_CODE: ${{ needs.source.outputs.version_code }}
run: |
set -euo pipefail
tag="playstore-$VERSION"
if gh release view "$tag" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::error::PlayStore release $tag already exists"
exit 1
fi
printf 'Signed Android App Bundle for Google Play upload.\n\nSource: `%s` on `playstore`.\nVersion code: `%s`.\n' "$SOURCE_SHA" "$VERSION_CODE" > "$RUNNER_TEMP/play-release.md"
bundles=(signed/*.aab)
[ "${#bundles[@]}" -eq 1 ] && [ -f "${bundles[0]}" ]
python3 tools/playstore/release_assets.py --bundle "${bundles[0]}" --checksums signed/SHA256SUMS --output publish
if compgen -G 'publish/*.part*' >/dev/null; then
name=$(basename "${bundles[0]}")
printf '\nGitHub release assets are split to stay below its per-file limit. Download every part and PART-SHA256SUMS, verify the parts, combine them in numeric order, and verify SHA256SUMS before uploading the resulting AAB to Google Play.\n\nThe complete signed AAB is also available in the droiddeck-play-bundle-signed artifact for this workflow run.\n\n```sh\nsha256sum -c PART-SHA256SUMS\nset -o noclobber\ncat "%s".part??? > "%s"\nsha256sum -c SHA256SUMS\n```\n' "$name" "$name" >> "$RUNNER_TEMP/play-release.md"
fi
gh release create "$tag" -R "$GITHUB_REPOSITORY" publish/* \
--target "$SOURCE_SHA" --title "DroidDeck $VERSION · PlayStore" \
--notes-file "$RUNNER_TEMP/play-release.md" --latest=false
+18 -2
View File
@@ -1,5 +1,5 @@
name: Release APKs
run-name: release ${{ inputs.version }} from main ${{ github.sha }}
run-name: ${{ github.ref == 'refs/heads/playstore' && format('PlayStore {0} from playstore {1}', inputs.version, github.sha) || format('release {0} from main {1}', inputs.version, github.sha) }}
on:
workflow_dispatch:
@@ -13,10 +13,23 @@ permissions:
actions: read
concurrency:
group: release
group: ${{ github.ref == 'refs/heads/playstore' && 'release-playstore' || 'release' }}
cancel-in-progress: false
jobs:
playstore:
name: Build PlayStore Bundle
if: github.ref == 'refs/heads/playstore'
permissions:
contents: write
actions: read
pull-requests: write
uses: ./.github/workflows/playstore.yml
with:
version: ${{ inputs.version }}
source_branch: playstore
secrets: inherit
find:
name: find the main build
if: github.ref == 'refs/heads/main'
@@ -53,6 +66,7 @@ jobs:
echo "Signing Build APK run $id (main ${GITHUB_SHA::7}) as $VERSION: $variants" >> "$GITHUB_STEP_SUMMARY"
sign:
if: github.ref == 'refs/heads/main'
needs: find
uses: ./.github/workflows/sign.yml
with:
@@ -64,6 +78,7 @@ jobs:
secrets: inherit
packs:
if: github.ref == 'refs/heads/main'
needs: find
permissions:
contents: read
@@ -74,6 +89,7 @@ jobs:
secrets: inherit
publish:
if: github.ref == 'refs/heads/main'
name: publish the release
needs: [find, sign, packs]
runs-on: ubuntu-24.04
+5
View File
@@ -1,3 +1,8 @@
# Rules
- Comments should be rare, factual, and explain only non-obvious external constraints or business invariants.
## Google Play distribution
For `playstore` and all changes destined for Google Play, read and enforce [the mandatory distribution policy gate](tools/playstore/POLICY.md) before editing or integrating changes. Refuse known policy violations and hold uncertain changes for authoritative evidence. Review incoming main commits individually; never overwrite Play delivery, permission, updater, signing, versioning, catalog, or release protections. A successful build is not policy clearance.
+50
View File
@@ -0,0 +1,50 @@
# DroidDeck Privacy Policy
Effective date: 3 November 2026
DroidDeck is an open-source game launcher maintained by the Droid-Deck project. A separate DroidDeck online account is not required. DroidDeck does not include advertising analytics or automatically submit usage reports or crash reports to the project maintainers. The maintainers do not operate a central database of users' game libraries or sign-in credentials, and do not sell users' personal data.
## Information kept on your device
DroidDeck stores information needed to run its features locally. Depending on what you use, this includes settings, controller and display configurations, game-library metadata, artwork caches, installed components, game saves, download state, and diagnostic logs. Store integrations and the clients running inside DroidDeck may also retain account names, authentication tokens, cookies, and sign-in sessions. These are used to access the services you choose.
Game performance and racing telemetry can be processed on your device for HUD and secondary-screen displays. This is separate from sending analytics to the project maintainers.
## Permissions and optional features
DroidDeck accesses device information and resources to provide the features you enable:
- File access supports browsing and managing games, saves, libraries, and other files. Broad file access, when granted, allows access outside the app's private storage, including shared and removable storage.
- The launcher reads launchable Android app names, icons, and package identifiers to display and launch those apps.
- The accessibility service handles Home/Guide shortcuts and identifies apps occupying a secondary display. It reads app package names and window bounds locally. This service does not read text, capture screens, or transmit the information it reads.
- Notification access lets the optional notification display read notification titles and content on the device.
- Camera, microphone, motion, accessory, and input data support features such as tracking, glasses, capture, game audio, and controls. Screenshots or recordings you create are saved locally unless you choose to share or synchronize them.
- Bluetooth, USB, Wi-Fi, and local-network access support accessories, network controls, synchronization, streaming, and connections you configure. Android can require location permission for Wi-Fi discovery.
You can manage permissions through Android settings. Turning off a permission or optional feature can limit the functionality that uses it.
## Connections to other services
DroidDeck connects to the stores, game services, cloud-streaming providers, artwork sources, and download services used by your selected features. Those services can receive information such as your IP address, requests, game identifiers, authentication information, or information you enter directly into their sign-in and purchase pages. Steam, games, and other software running through DroidDeck have their own network activity and privacy policies. Their providers determine how they handle that information.
The Google Play version downloads runtime modules and non-executable resource packs through Google Play. Google's Play Feature Delivery and Play Asset Delivery services process device metadata and the application version to deliver the correct files and preserve them across updates. Google documents encrypted processing and a fixed retention period. See [Play Feature Delivery data handling](https://developer.android.com/guide/playcore/feature-delivery#data-safety) and [Play Asset Delivery data handling](https://developer.android.com/guide/playcore/asset-delivery#terms-of-service).
The Google Play version contacts Google Play to check whether an app update is available. Selecting Update opens the app's Google Play listing. Google handles Play delivery and updates under its own [privacy policy](https://policies.google.com/privacy).
Optional Windows prerequisites can be downloaded from the sources identified in the app's pinned component inventory, including GitHub-hosted runtime packages. These providers receive the download request and IP address. DroidDeck verifies the package against the inventory before installing it into the guest runtime.
Optional Syncthing synchronization transfers selected files to the devices you configure and can use discovery and relay services. DroidDeck disables Syncthing usage reporting and crash reporting. Selected games or other guest software may use files and resources available within DroidDeck's runtime.
DroidDeck's privacy policy does not replace the policies of these separate providers. Review the relevant provider's policy when signing in, purchasing content, streaming, installing software, or enabling synchronization.
## Retention, removal, and reports
Local information remains until you clear it, sign out, remove the relevant files or feature data, or use Android's app-data controls. Removing DroidDeck or clearing its app storage removes Android-managed app data; libraries, saves, screenshots, and other files in shared storage or on other devices may remain. Signing out of a store integration does not delete the account held by that store. Manage or delete those accounts through the provider.
Diagnostic logs are kept locally. If you choose to send a report or export logs, the recipient receives what you submit. Public GitHub issues and discussions are public. Review reports and attachments before sharing them, and exclude passwords, authentication tokens, and other sensitive information.
## Contact and updates
For privacy questions, contact the Droid-Deck maintainers through the project's [GitHub issue form](https://github.com/Droid-Deck/DroidDeck/issues/new?title=Privacy%20question). Keep the initial public inquiry free of sensitive information; request a private contact method if necessary. GitHub handles information submitted through its service under its own [privacy statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement).
Updates to this policy will appear on this page with a revised effective date.
+1 -1
View File
@@ -47,7 +47,7 @@ Help, Preview builds and device reports are on [Discord](https://discord.gg/JRGA
## Build
Needs Docker, Java 17, the Android SDK and NDK, Rust via rustup, GitHub CLI, Python 3 and `zstd`.
Needs Docker, Java 21, the Android SDK and NDK, Rust via rustup, GitHub CLI, Python 3 and `zstd`.
```bash
tools/build_local.sh
+112 -26
View File
@@ -1,7 +1,6 @@
plugins {
id 'com.android.application'
}
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
def localBuildOptions = new Properties()
@@ -19,6 +18,7 @@ if (uvcDir) {
if (!new File(uvcDir, required).isFile()) throw new GradleException("Incomplete UVC build: missing ${required}")
}
}
if (vitureSdkDir) {
['android/arm64-v8a/libglasses.so', 'android/arm64-v8a/libcarina_vio.so',
'android/arm64-v8a/libcloud_protocol.so', 'include/viture_glasses_provider.h',
@@ -82,6 +82,9 @@ def buildBranch = providers.environmentVariable('GITHUB_HEAD_REF').getOrNull()?.
?: gitValue(['branch', '--show-current']) ?: ''
def escapeJava = { String value -> value.replace('\\', '\\\\').replace('"', '\\"').replace('\n', ' ').replace('\r', ' ') }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
def playDistribution = providers.gradleProperty('playDistribution').getOrElse('false').toBoolean()
def playManifest = playDistribution ? new groovy.json.JsonSlurper().parse(rootProject.file('tools/playstore/components.lock.json')) : null
def playLegacyDesktop = playManifest?.entries?.find { it.id == 'desktop-legacy-kde' }
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
def commitTime = (gitValue(['show', '-s', '--format=%ct', '--end-of-options', sourceCommit]) ?: '0').with { it.isLong() ? it : '0' }
def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
@@ -124,29 +127,30 @@ def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
android {
namespace 'com.droiddeck.launcher'
compileSdk 34
compileSdk 37
ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724'
defaultConfig {
applicationId "com.droiddeck.launcher"
minSdkVersion 26
targetSdkVersion 28
versionCode 17
versionName "0.6.9"
targetSdkVersion 37
versionCode 20
versionName "0.6.9.2"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
buildConfigField "String", "PR_TITLE", "\"${escapeJava(prTitle)}\""
buildConfigField "String", "BUILD_BRANCH", "\"${escapeJava(buildBranch)}\""
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
buildConfigField "boolean", "PLAY_DISTRIBUTION", "${playDistribution}"
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\""
buildConfigField "String", "SGDB_API_KEY", "\"${System.getenv("SGDB_API_KEY") ?: project.findProperty("sgdbApiKey") ?: ""}\""
buildConfigField "String", "DESKTOP_KDE_TAG", "\"${desktopKde.DESKTOP_KDE_TAG}\""
buildConfigField "String", "DESKTOP_KDE_URL", "\"https://github.com/${desktopKde.DESKTOP_KDE_REPO}/releases/download/${desktopKde.DESKTOP_KDE_TAG}/desktop-kde.tar.zst\""
buildConfigField "String", "DESKTOP_KDE_SHA256", "\"${desktopKde.DESKTOP_KDE_SHA256}\""
buildConfigField "long", "DESKTOP_KDE_SIZE", "${desktopKde.DESKTOP_KDE_SIZE}L"
buildConfigField "String", "DESKTOP_KDE_SHA256", "\"${playLegacyDesktop?.sha256 ?: desktopKde.DESKTOP_KDE_SHA256}\""
buildConfigField "long", "DESKTOP_KDE_SIZE", "${playLegacyDesktop?.size ?: desktopKde.DESKTOP_KDE_SIZE}L"
addonPacks.each { name, pack ->
buildConfigField "String", "PACK_${name.toUpperCase()}_SHA256", "\"${pack.sha256}\""
buildConfigField "long", "PACK_${name.toUpperCase()}_SIZE", "${pack.size}L"
@@ -215,7 +219,7 @@ android {
jniLibs {
useLegacyPackaging = true
pickFirsts += ['**/libtermux.so']
excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
excludes += ['**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
}
}
@@ -233,8 +237,18 @@ android {
outputs.upToDateWhen { false }
outputs.doNotCacheIf('CI executes the test suite on every run') { true }
}
jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED',
'--add-opens=java.base/java.util=ALL-UNNAMED',
'--add-opens=java.base/java.io=ALL-UNNAMED',
'--add-opens=java.base/java.net=ALL-UNNAMED',
'--add-opens=java.base/java.security=ALL-UNNAMED',
'--add-opens=java.base/java.text=ALL-UNNAMED',
'--add-opens=java.base/jdk.internal.access=ALL-UNNAMED',
'--add-opens=java.desktop/java.awt.font=ALL-UNNAMED',
'--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED'
maxHeapSize = '2g'
maxParallelForks = 2
systemProperty 'robolectric.dependency.repo.url', 'https://repo.maven.apache.org/maven2'
def logTarget = providers.systemProperty('robolectric.logging').orNull
if (logTarget != null) systemProperty 'robolectric.logging', logTarget
}
@@ -244,11 +258,37 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
def verifyPlayAudio
if (playDistribution) {
android.buildTypes.debug.signingConfig = null
android.buildTypes.release.signingConfig = null
def requestedCode = providers.gradleProperty('playVersionCode').orNull
if (requestedCode != null && requestedCode != android.defaultConfig.versionCode.toString()) {
throw new GradleException('Play version code must match app/build.gradle')
}
def playAudio = layout.buildDirectory.dir('play-audio')
def playNdk = androidComponents.sdkComponents.ndkDirectory.get().asFile
verifyPlayAudio = tasks.register('verifyPlayAudio', Exec) {
workingDir rootProject.projectDir
commandLine pythonExecutable, 'tools/playstore/build_audio.py', 'verify',
'--output', playAudio.get().asFile, '--ndk', playNdk
}
def stagePlayJni = tasks.register('stagePlayJni', Sync) {
dependsOn(verifyPlayAudio)
from(layout.projectDirectory.dir('src/main/jniLibs')) {
exclude(['libltdl.so', 'libpulseaudio.so', 'libpulse.so', 'libpulsecommon-13.0.so',
'libpulsecore-13.0.so', 'libsndfile.so'].collect { "arm64-v8a/${it}".toString() })
}
from(playAudio.map { it.dir('jni') })
into(layout.buildDirectory.dir('playJni'))
}
android.sourceSets.main.jniLibs.setSrcDirs([layout.buildDirectory.dir('playJni')])
tasks.named('preBuild').configure { dependsOn(stagePlayJni) }
}
// No instrumented tests; the unused androidTest variant breaks the VS Code Kotlin import.
androidComponents {
beforeVariants(selector().all()) { it.androidTest.enable = false }
@@ -266,7 +306,7 @@ if (vitureSdkDir) {
def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
from(rootProject.file('tools/linuxfs/overlay')) {
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**'
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**', 'usr/local/lib/droiddeck/*.py'
}
into(layout.projectDirectory.dir('src/main/assets/linuxfs'))
}
@@ -277,18 +317,34 @@ def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
dependsOn(stageSessionScripts)
from(layout.projectDirectory.dir('src/main/assets')) { exclude 'pulseaudio.tzst' }
from(rootProject.file('tools/linuxfs/innoextract'))
from(layout.buildDirectory.dir('prepared-assets'))
from(layout.buildDirectory.dir('prepared-assets')) {
if (playDistribution) exclude 'pulseaudio.tzst'
}
if (playDistribution) {
dependsOn(verifyPlayAudio)
from(layout.buildDirectory.dir('play-audio/assets'))
}
into(runtimeAssetsDir)
}
android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir])
if (vitureSdkDir) android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('vitureAssets'))
if (playDistribution) {
def payloads = rootProject.file('build/playstore-payloads')
def modules = new groovy.json.JsonSlurper().parse(new File(payloads, 'modules.json'))
android.dynamicFeatures = modules.collect { ":${it}".toString() }.toSet()
def packs = new groovy.json.JsonSlurper().parse(new File(payloads, 'assetpacks.json'))
android.assetPacks = packs.collect { ":${it}".toString() }.toSet()
android.sourceSets.main.assets.srcDir(new File(payloads, 'base/src/main/assets'))
android.sourceSets.debug.manifest.srcFile('src/play/AndroidManifest.xml')
android.sourceSets.release.manifest.srcFile('src/play/AndroidManifest.xml')
}
if (vitureSdkDir) android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('vitureAssets').get().asFile)
if (uvcDir) {
def stageUvcNotices = tasks.register('stageUvcNotices', Copy) {
from(new File(uvcDir, 'licenses'))
into(layout.buildDirectory.dir('uvcAssets/licenses/uvc'))
}
tasks.named('preBuild').configure { dependsOn(stageUvcNotices) }
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('uvcAssets'))
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('uvcAssets').get().asFile)
}
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
@@ -296,15 +352,15 @@ def stageGogNotices = tasks.register('stageGogNotices', Copy) {
from(layout.projectDirectory.dir('src/main/rust/stores/vendor/xdelta3')) { include 'LICENSE', 'README.droiddeck.md' }
into(layout.buildDirectory.dir('gogNotices/licenses/xdelta3'))
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices'))
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices').get().asFile)
tasks.named('preBuild').configure { dependsOn(stageGogNotices) }
if (!ciBuild) {
if (!ciBuild || playDistribution) {
def stageLocalPacks = tasks.register('stageLocalPacks', Sync) {
from(layout.buildDirectory.dir('packs')) { include '*.tar.zst' }
into(layout.buildDirectory.dir('localPackAssets/packs'))
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('localPackAssets'))
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('localPackAssets').get().asFile)
tasks.named('preBuild').configure { dependsOn(stageLocalPacks) }
}
@@ -339,7 +395,7 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
workingDir rustCrateDir
def cargoBin = "${System.getProperty('user.home')}/.cargo/bin"
environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}"
environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath
environment 'ANDROID_NDK_HOME', androidComponents.sdkComponents.ndkDirectory.get().asFile.absolutePath
environment 'CARGO_TERM_COLOR', 'never'
if (!skipRust) environment 'RUSTC', providers.exec {
commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc'
@@ -348,7 +404,7 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
'build', '--release', '--locked'
}
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir.get().asFile)
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
def maliBridgeDir = rootProject.file('external/MaliBridge')
@@ -361,20 +417,30 @@ def buildVkBridgeServer = tasks.register('buildVkBridgeServer', Exec) {
inputs.file(new File(maliBridgeDir, 'scripts/build-server-android.sh'))
outputs.dir(vkbridgeJniLibsDir)
commandLine 'bash', new File(maliBridgeDir, 'scripts/build-server-android.sh').absolutePath,
android.ndkDirectory.absolutePath, vkbridgeJniLibsDir.get().asFile.absolutePath
androidComponents.sdkComponents.ndkDirectory.get().asFile.absolutePath, vkbridgeJniLibsDir.get().asFile.absolutePath
}
android.sourceSets.main.jniLibs.srcDir(vkbridgeJniLibsDir)
android.sourceSets.main.jniLibs.srcDir(vkbridgeJniLibsDir.get().asFile)
tasks.named('preBuild').configure { dependsOn(buildVkBridgeServer) }
dependencies {
implementation 'com.google.android.play:app-update:2.1.0'
implementation 'com.google.android.play:feature-delivery:2.1.0'
implementation 'com.google.android.play:asset-delivery:2.3.0'
implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}"
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
implementation 'com.github.luben:zstd-jni:1.5.7-4@aar'
implementation 'org.apache.commons:commons-compress:1.27.1'
implementation 'org.tukaani:xz:1.10'
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation platform('androidx.compose:compose-bom:2025.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
implementation('androidx.compose.material3:material3') {
version { strictly '1.2.0' }
}
constraints {
implementation('androidx.compose.material3:material3-android') {
version { strictly '1.2.0' }
}
}
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
implementation 'androidx.activity:activity-compose:1.8.2'
@@ -394,7 +460,27 @@ dependencies {
implementation 'com.google.zxing:core:3.5.3'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
testImplementation 'org.robolectric:robolectric:4.17'
testImplementation 'androidx.compose.ui:ui-test-junit4'
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.7-4@jar'
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
androidComponents {
beforeVariants(selector().all()) { variant ->
variant.enableUnitTest = true
}
}
configurations { hostZstd }
dependencies { hostZstd 'com.github.luben:zstd-jni:1.5.7-4@jar' }
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
variant.unitTest?.configureTestTask { test ->
test.classpath = files(configurations.hostZstd) + test.classpath.filter { !it.name.contains("zstd-jni") }
}
}
+33 -8
View File
@@ -3,6 +3,15 @@
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="com.xiaoji.egggame" />
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<package android:name="cn.nubia.gamelauncher" />
<intent>
<action android:name="android.content.pm.action.CONFIRM_INSTALL" />
@@ -10,10 +19,6 @@
<intent>
<action android:name="android.content.pm.action.CONFIRM_PERMISSIONS" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SECONDARY_HOME" />
@@ -22,6 +27,7 @@
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -30,6 +36,10 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
@@ -41,7 +51,8 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
@@ -63,6 +74,9 @@
android:hardwareAccelerated="true"
android:largeHeap="true"
android:extractNativeLibs="true"
android:pageSizeCompat="enabled"
android:requestLegacyExternalStorage="true"
android:enableOnBackInvokedCallback="false"
android:appCategory="game"
android:isGame="true">
@@ -321,7 +335,12 @@
<service
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
android:foregroundServiceType="specialUse|mediaPlayback|microphone"
android:stopWithTask="false">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Runs the user's Linux game session (compositor, Steam client, audio) while the app is not on screen" />
</service>
<!-- The compositor's native state is one per process, so the bottom-screen desktop's compositor runs in its own. -->
<service
android:name=".wayland.BottomDesktopCompositorService"
@@ -329,10 +348,16 @@
android:exported="false" />
<service
android:name=".runtime.RuntimeInstallService"
android:exported="false" />
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".core.WirelessAdbPairingService"
android:exported="false" />
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Waits for the wireless debugging pairing code the user enters from the notification" />
</service>
<service
android:name=".stores.download.StoreDownloadService"
android:exported="false"
@@ -7,8 +7,10 @@ import com.droiddeck.launcher.session.EnhancedSessionLogShare
import com.droiddeck.launcher.session.GpuClockPin
class App : Application() {
override fun attachBaseContext(base: android.content.Context) =
override fun attachBaseContext(base: android.content.Context) {
super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(base))
if (BuildConfig.PLAY_DISTRIBUTION) com.google.android.play.core.splitcompat.SplitCompat.install(this)
}
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
super.onConfigurationChanged(newConfig)
@@ -17,6 +19,7 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
com.droiddeck.launcher.core.ComponentDownloads.init(this)
Thread({ runCatching { EnhancedSessionLogShare.cleanupAbandonedStages(this) } }, "diagnostic-stage-cleanup").start()
com.droiddeck.launcher.ui.Themes.loadGradient(this)
com.droiddeck.launcher.ui.BuildWatermark.install(this)
@@ -68,7 +71,7 @@ class App : Application() {
Thread({
runCatching {
if (com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(app).isDirectory) {
com.droiddeck.launcher.core.AddonPack.PROOT.ensure(app)
if (!com.droiddeck.launcher.runtime.LinuxRuntime.patchedProot(app)) com.droiddeck.launcher.core.AddonPack.PROOT.ensure(app)
com.droiddeck.launcher.core.AddonPack.DIRECTAUDIO.ensure(app)
}
if (com.droiddeck.launcher.stores.gog.GogComet.enabled(app)) com.droiddeck.launcher.core.AddonPack.COMET.ensure(app)
@@ -22,6 +22,7 @@ import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -119,6 +120,7 @@ class FrontEndActivity : ComponentActivity() {
private var frameGenLabel by mutableStateOf("")
private var lossless by mutableStateOf(Lossless.State.NONE)
private var showRemove by mutableStateOf(false)
private var showAccessibilityConsent by mutableStateOf(false)
private var showRuntimeActions by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
private var pendingSwitch by mutableStateOf<Intent?>(null)
private var switchingTo: Intent? = null
@@ -347,6 +349,10 @@ class FrontEndActivity : ComponentActivity() {
SteamIntegration.Feature.BRIGHTNESS -> integrationSettings(SteamIntegration.brightnessSettings(this))
SteamIntegration.Feature.HOME -> if (HomeApp.isDefault(this)) HomeApp.openSystemHomeSettings(this) else offerHomeScreen()
SteamIntegration.Feature.SHORTCUTS -> {
if (!SteamIntegration.accessibilityConsented(this)) {
showAccessibilityConsent = true
return
}
if (!integrationState.shortcuts) shortcutsTried = true
SteamIntegration.setShortcutsListed(this, true)
integrationSettings(SteamIntegration.shortcutSettings(this), android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS)
@@ -584,6 +590,9 @@ class FrontEndActivity : ComponentActivity() {
private var gameDebugLogsNeedsRestart by mutableStateOf(false)
private var vacDiagnostics by mutableStateOf(false)
private var agentCommands by mutableStateOf(false)
private var bluetoothAllowed by mutableStateOf<Boolean?>(null)
private var allFilesAllowed by mutableStateOf<Boolean?>(null)
private var localNetworkAllowed by mutableStateOf<Boolean?>(null)
private var graphicsLogs by mutableStateOf(false)
private var steamGpuDrawing by mutableStateOf(false)
private var bridgeFellBack by mutableStateOf(false)
@@ -646,6 +655,11 @@ class FrontEndActivity : ComponentActivity() {
private var gameSpaceOffered = false
override fun startActivity(intent: Intent?) {
if (BuildConfig.PLAY_DISTRIBUTION && !com.droiddeck.launcher.core.PlayDelivery.state.value.ready &&
intent?.component?.className == SessionActivity::class.java.name) {
com.droiddeck.launcher.core.PlayDelivery.defer(intent)
return
}
val compat = protons.protonBusyId != null || ProtonExtras.installInProgress
if (intent?.component?.className == SessionActivity::class.java.name) {
if (compatLaunch != null) return
@@ -1077,6 +1091,11 @@ class FrontEndActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (BuildConfig.PLAY_DISTRIBUTION) {
@Suppress("DEPRECATION")
val pending = savedInstanceState?.getParcelable<Intent>("play_pending_session")
if (pending?.component?.className == SessionActivity::class.java.name) com.droiddeck.launcher.core.PlayDelivery.defer(pending)
}
pendingSessionLogExport = savedInstanceState?.getString(KEY_PENDING_SESSION_LOG_EXPORT)?.let(::File)
homeChoiceOpen = savedInstanceState?.getBoolean(STATE_HOME_CHOICE_OPEN) == true
gameSpaceOffered = savedInstanceState?.getBoolean(STATE_GAME_SPACE_OFFERED) == true
@@ -1114,6 +1133,27 @@ class FrontEndActivity : ComponentActivity() {
updates.start()
setContent {
DroidDeckTheme(theme, appScale = appScale) {
val delivery by com.droiddeck.launcher.core.PlayDelivery.state.collectAsState()
androidx.compose.runtime.LaunchedEffect(delivery.ready) {
if (delivery.ready) {
com.droiddeck.launcher.core.PlayDelivery.takePendingSession()?.let { startActivity(it) }
if (BuildConfig.PLAY_DISTRIBUTION) {
refresh()
refreshDeliveredComponents(force = true)
}
}
}
if (BuildConfig.PLAY_DISTRIBUTION && !delivery.ready) ConfirmDialog(
title = getString(R.string.app_name),
text = if (delivery.busy && delivery.percent >= 0)
getString(R.string.setup_runtime_progress, getString(R.string.rtinst_downloading), delivery.percent)
else getString(if (delivery.failed) R.string.user_apps_download_failed else R.string.rtinst_downloading),
confirm = getString(if (delivery.busy) R.string.common_working else R.string.stores_dl_retry),
onConfirm = { com.droiddeck.launcher.core.PlayDelivery.start(this) },
onDismiss = { com.droiddeck.launcher.core.PlayDelivery.cancel(); finish() },
progress = if (delivery.busy) delivery.percent else null,
confirmEnabled = !delivery.busy, closeOnConfirm = false,
)
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize().exposeTestTags()) {
if (decky.deckyPendingZip != null) com.droiddeck.launcher.ui.DeckyZipWarningDialog(
onConfirm = { decky.confirmPluginZip() }, onCancel = { decky.cancelPluginZip() },
@@ -1146,6 +1186,7 @@ class FrontEndActivity : ComponentActivity() {
enhancedDiagnosticsArmed = enhancedDiagnosticsArmed,
gameDebugLogsEnabled = gameDebugLogsEnabled, gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart,
vacDiagnostics = vacDiagnostics, agentCommands = agentCommands,
bluetoothAllowed = bluetoothAllowed, allFilesAllowed = allFilesAllowed, localNetworkAllowed = localNetworkAllowed,
graphicsLogs = graphicsLogs, steamGpuDrawing = steamGpuDrawing, bridgeFellBack = bridgeFellBack, bridgeFellBackMemory = bridgeFellBackMemory,
clearCachedApps = clearCachedApps, adbPaired = adbPaired, memoryHud = memoryHud, backgroundAppLimit = backgroundAppLimit,
steamGames = steamGames, steamGamesX64 = steamGamesX64, emulators = emulatorList, running = runningLabel,
@@ -1378,6 +1419,9 @@ class FrontEndActivity : ComponentActivity() {
com.droiddeck.launcher.agent.AgentAccess.setCommandsEnabled(this, !com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this))
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
},
onBluetoothAllowed = { askBluetooth() },
onLocalNetworkAllowed = { com.droiddeck.launcher.core.AppPermissions.requestLocalNetwork(this, 4) },
onAllFilesAllowed = { com.droiddeck.launcher.files.AllFilesAccess.openSettings(this) },
onClearCachedApps = { on -> SessionPrefs.setClearCachedApps(this, on); clearCachedApps = on },
onMemoryHud = { value -> SessionPrefs.setMemoryHud(this, value); memoryHud = value },
onBackgroundAppLimit = { limit ->
@@ -1573,7 +1617,10 @@ class FrontEndActivity : ComponentActivity() {
refreshPhantomStatus()
showPhantomGate = true
},
onStartWirelessAdbPairing = { WirelessAdbPairingService.start(this) },
onStartWirelessAdbPairing = {
if (com.droiddeck.launcher.core.AppPermissions.localNetwork(this)) WirelessAdbPairingService.start(this)
else com.droiddeck.launcher.core.AppPermissions.requestLocalNetwork(this, 5)
},
onStartPairingGuide = { displayId, toggleOnly -> PairingGuideActivity.start(this, displayId, toggleOnly) },
onOpenNotificationSettings = {
startActivity(Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS)
@@ -1604,6 +1651,17 @@ class FrontEndActivity : ComponentActivity() {
page = page,
)
}
if (showAccessibilityConsent) ConfirmDialog(
title = androidx.compose.ui.res.stringResource(R.string.integration_shortcuts),
text = androidx.compose.ui.res.stringResource(R.string.integration_shortcuts_service_description),
confirm = androidx.compose.ui.res.stringResource(R.string.glasses_allow),
onConfirm = {
SteamIntegration.acceptAccessibility(this@FrontEndActivity)
integrationState = readIntegration()
configureIntegration(SteamIntegration.Feature.SHORTCUTS)
},
onDismiss = { showAccessibilityConsent = false },
)
if (showRoms) RomsDialog(
path = romsDir,
onChoose = {
@@ -1693,15 +1751,24 @@ class FrontEndActivity : ComponentActivity() {
}
val wanted = ArrayList<String>()
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
for (permission in arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) wanted.add(permission)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !SessionPrefs.bluetoothAsked(this)
&& checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
wanted.add(Manifest.permission.BLUETOOTH_CONNECT)
SessionPrefs.setBluetoothAsked(this)
}
if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this)
&& checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
wanted.add(Manifest.permission.RECORD_AUDIO)
SessionPrefs.setMicAsked(this)
}
wanted.addAll(com.droiddeck.launcher.core.AppPermissions.startup(this))
if (wanted.isNotEmpty()) requestPermissions(wanted.toTypedArray(), 1)
else askSharedStorage()
Thread({ com.droiddeck.launcher.stores.StoreAccounts.encryptAll(this) }, "store-credentials").start()
if (!SessionState.running) Thread({
com.droiddeck.launcher.session.LogMigration.run(this)
@@ -1716,6 +1783,23 @@ class FrontEndActivity : ComponentActivity() {
}, "finish-abandoned").start()
}
private fun askSharedStorage() {
if (!com.droiddeck.launcher.files.AllFilesAccess.granted() && !SessionPrefs.allFilesAsked(this)) {
Thread({
if (com.droiddeck.launcher.files.AllFilesAccess.usesSharedStorage(this)) runOnUiThread {
if (!SessionPrefs.allFilesAsked(this) && com.droiddeck.launcher.files.AllFilesAccess.ask(this)) SessionPrefs.setAllFilesAsked(this)
}
}, "all-files-access").start()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
refreshPermissionRows()
if (requestCode == 5 && com.droiddeck.launcher.core.AppPermissions.localNetwork(this)) WirelessAdbPairingService.start(this)
askSharedStorage()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
@@ -1807,6 +1891,7 @@ class FrontEndActivity : ComponentActivity() {
}
}
@android.annotation.SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (compatLaunch != null) {
if (event.keyCode == KeyEvent.KEYCODE_BACK || event.keyCode == KeyEvent.KEYCODE_BUTTON_B) {
@@ -1832,6 +1917,27 @@ class FrontEndActivity : ComponentActivity() {
}
}
private fun refreshPermissionRows() {
localNetworkAllowed = if (Build.VERSION.SDK_INT >= 37) com.droiddeck.launcher.core.AppPermissions.localNetwork(this) else null
bluetoothAllowed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED else null
allFilesAllowed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) com.droiddeck.launcher.files.AllFilesAccess.granted() else null
}
private fun askBluetooth() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
val permission = Manifest.permission.BLUETOOTH_CONNECT
val granted = checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
if (granted || (SessionPrefs.bluetoothAsked(this) && !shouldShowRequestPermissionRationale(permission))) {
runCatching {
startActivity(Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, android.net.Uri.parse("package:$packageName")))
}
} else {
SessionPrefs.setBluetoothAsked(this)
requestPermissions(arrayOf(permission), 3)
}
}
override fun dispatchGenericMotionEvent(event: android.view.MotionEvent): Boolean {
if (compatLaunch != null && !compatLaunch!!.loading.ended &&
com.droiddeck.launcher.input.PadBridge.isControllerMotionEvent(event)) return true
@@ -1846,6 +1952,7 @@ class FrontEndActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
com.droiddeck.launcher.core.PlayDelivery.start(this)
if (!homeChoiceOpen) {
HomeApp.releaseUnusedHomeEntry(this)
refreshHomeAppState()
@@ -1859,6 +1966,7 @@ class FrontEndActivity : ComponentActivity() {
requestedOrientation = launcherOrientation
SteamIntegration.syncShortcutsListing(this)
refreshWifiDiscovery()
refreshPermissionRows()
com.droiddeck.launcher.stores.StoresState.refresh(this)
com.droiddeck.launcher.stores.StoresState.libraryListener = { refreshAddedGames(); refresh() }
com.droiddeck.launcher.stores.battlenet.BattleNetClient.opener = { args -> openBattleNet(args) }
@@ -1869,10 +1977,24 @@ class FrontEndActivity : ComponentActivity() {
refreshPhantomStatus()
syncLossless()
updates.onResume()
catalogFresh = false
oscMode = SessionPrefs.oscMode(this)
refreshController()
refreshHomeAppState()
refreshSecondScreenDisplays()
refresh()
decky.deckyInstalled = DeckyManager.installed(this)
DeckyManager.syncCefMarker(this)
decky.deckySupervisor = DeckyManager.supervisorEnabled(this)
refreshAddedGames()
refreshDeliveredComponents()
}
private fun refreshDeliveredComponents(force: Boolean = false) {
if (BuildConfig.PLAY_DISTRIBUTION && !com.droiddeck.launcher.core.PlayDelivery.state.value.ready) return
drivers.checkOnOpen()
resumeInterruptedPackages()
protons.checkUpdates()
catalogFresh = false
protons.checkUpdates(force)
if (!SessionState.running && !componentsQueueBusy && !LinuxRuntimeInstaller.isBusy()) {
componentsQueueBusy = true
Thread({
@@ -1887,15 +2009,6 @@ class FrontEndActivity : ComponentActivity() {
}
}, "components-queue").start()
} else if (showComponents) components.refreshComponents()
oscMode = SessionPrefs.oscMode(this)
refreshController()
refreshHomeAppState()
refreshSecondScreenDisplays()
refresh()
decky.deckyInstalled = DeckyManager.installed(this)
DeckyManager.syncCefMarker(this)
decky.deckySupervisor = DeckyManager.supervisorEnabled(this)
refreshAddedGames()
if (catalog == null) loadDesktopCatalog()
if (!busy) Thread({ checkCatalog() }, "catalog").start()
}
@@ -1919,6 +2032,10 @@ class FrontEndActivity : ComponentActivity() {
@Suppress("OVERRIDE_DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == 7619 && BuildConfig.PLAY_DISTRIBUTION) {
if (resultCode != RESULT_OK) com.droiddeck.launcher.core.PlayDelivery.cancel()
return
}
if (HomeWidgets.onActivityResult(this, requestCode, resultCode)) return
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
@@ -1926,6 +2043,9 @@ class FrontEndActivity : ComponentActivity() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (BuildConfig.PLAY_DISTRIBUTION) com.droiddeck.launcher.core.PlayDelivery.peekPendingSession()?.let {
outState.putParcelable("play_pending_session", it)
}
outState.putBoolean(STATE_HOME_CHOICE_OPEN, homeChoiceOpen)
outState.putBoolean(STATE_GAME_SPACE_OFFERED, gameSpaceOffered)
pendingSessionLogExport?.let { outState.putString(KEY_PENDING_SESSION_LOG_EXPORT, it.absolutePath) }
@@ -1943,6 +2063,7 @@ class FrontEndActivity : ComponentActivity() {
else @Suppress("DEPRECATION") state.getParcelable(key)
override fun onDestroy() {
com.droiddeck.launcher.core.PlayDelivery.detach(this)
gameLaunchRequest.incrementAndGet()
cancelCompatLaunch()
updates.unregister()
@@ -2019,7 +2140,7 @@ class FrontEndActivity : ComponentActivity() {
private fun launchPending(): Boolean = gameLinkReading || (pendingGameLink != null && !gameLinkTried)
private fun launchAndroidApp(app: HomeApp.LaunchableApp, displayId: Int?) {
val onOwnScreen = displayId == null || displayId == (display?.displayId ?: Display.DEFAULT_DISPLAY)
val onOwnScreen = displayId == null || displayId == ((if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay)?.displayId ?: Display.DEFAULT_DISPLAY)
if (onOwnScreen) com.droiddeck.launcher.ui.DualScreenPanel.yieldTo(taskId, true)
try {
HomeApp.launch(this, app, displayId)
@@ -2065,6 +2186,7 @@ class FrontEndActivity : ComponentActivity() {
}
private fun loadDesktopCatalog() {
if (BuildConfig.PLAY_DISTRIBUTION && !com.droiddeck.launcher.core.PlayDelivery.state.value.ready) return
if (catalogLoading || catalog != null) return
catalogLoading = true
Thread({
@@ -200,10 +200,11 @@ object HomeApp {
runCatching {
@Suppress("DEPRECATION")
fun idOf(task: android.app.ActivityManager.AppTask) =
if (Build.VERSION.SDK_INT >= 29) task.taskInfo.taskId else task.taskInfo.persistentId
task.taskInfo?.let { if (Build.VERSION.SDK_INT >= 29) it.taskId else it.persistentId } ?: -1
val tasks = activity.getSystemService(android.app.ActivityManager::class.java).appTasks
val task = tasks.firstOrNull { idOf(it) == activity.taskId } ?: return
val excluded = task.taskInfo.baseIntent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS != 0
val baseIntent = task.taskInfo?.baseIntent ?: return@runCatching
val excluded = baseIntent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS != 0
val step = recentsStep(hidden, id in owned, excluded)
if (step == RecentsStep.NONE) return
if (step != RecentsStep.FORGET) task.setExcludeFromRecents(step == RecentsStep.HIDE)
@@ -579,7 +579,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private val deviceListener = object : InputManager.InputDeviceListener {
override fun onInputDeviceAdded(deviceId: Int) {
logInputDevice("connected", deviceId)
joyConVr?.refresh()
if (Build.VERSION.SDK_INT >= 31) joyConVr?.refresh()
padBridge?.syncConnectedControllers()
refreshControls()
updatePointerCapture()
@@ -592,7 +592,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
dropMouseCursor()
backQamInput.onDeviceRemoved(deviceId)
mouseBackInput.onDeviceRemoved(deviceId)
joyConVr?.refresh()
if (Build.VERSION.SDK_INT >= 31) joyConVr?.refresh()
padBridge?.onDeviceRemoved(deviceId)
if (::padMouse.isInitialized) padMouse.reset()
refreshControls()
@@ -601,7 +601,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
refreshSecondScreenDisplays()
}
override fun onInputDeviceChanged(deviceId: Int) {
joyConVr?.refresh()
if (Build.VERSION.SDK_INT >= 31) joyConVr?.refresh()
refreshControls()
refreshSecondScreenDisplays()
}
@@ -611,6 +611,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (BuildConfig.PLAY_DISTRIBUTION && !com.droiddeck.launcher.core.PlayDelivery.state.value.ready) {
runtimeRemovalBlocked = true
com.droiddeck.launcher.core.PlayDelivery.defer(intent)
startActivity(Intent(this, FrontEndActivity::class.java))
finish()
return
}
if (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isRemoving() ||
com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isMaintaining()) {
runtimeRemovalBlocked = true
@@ -954,7 +961,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
if (!phonePanel && GlassesRendering.usesGlasses(glasses, glassesOutputActive, steamVrStatus == SteamVr.Status.RUNNING) &&
headTracking && !drawerOpen && !pcKeyboardOpen && !loading.visible) com.droiddeck.launcher.ui.GlassesRecenterButton(
controllerSettings?.tint ?: android.graphics.Color.rgb(90, 160, 255), controllerSettings?.opacity ?: 100,
onRecenterJoyCons = if (steamVrStatus == SteamVr.Status.RUNNING &&
onRecenterJoyCons = if (Build.VERSION.SDK_INT >= 31 && steamVrStatus == SteamVr.Status.RUNNING &&
glasses.usesSpatialControllers(glassesSettings) && glassesSettings.joyConTracking)
joyConVr?.let { { it.recenter() } } else null,
) { headTracker?.recenter() }
@@ -1523,7 +1530,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
val pads = com.droiddeck.launcher.input.VrControllers.settings.collectAsState().value
com.droiddeck.launcher.ui.GlassesPhonePanel(glasses.model, battery, com.droiddeck.launcher.ui.GlassesPhoneActions(
onRecenter = if (headTracking) ({ headTracker?.recenter() }) else null,
onRecenterJoyCons = if (vr && glasses.usesSpatialControllers(glassesSettings) && glassesSettings.joyConTracking)
onRecenterJoyCons = if (Build.VERSION.SDK_INT >= 31 && vr && glasses.usesSpatialControllers(glassesSettings) && glassesSettings.joyConTracking)
joyConVr?.let { { it.recenter() } } else null,
onSteamVrMenu = if (vr && SessionState.mode == SessionService.MODE_STEAM) ({ sendSteamGuide() }) else null,
onSwitchSticks = if (vr && pads.emulate != com.droiddeck.launcher.input.VrControllers.Emulate.OFF) ({ switchVrSticks() }) else null,
@@ -2045,7 +2052,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
SessionEvents.transition(SessionPhase.INSTALLING_RUNTIME, "proton.installing", mapOf("component" to "proton"))
val protonProblem = installProton()
if (protonProblem == null) SessionEvents.record("proton.ready")
else SessionEvents.record("proton.skipped", mapOf("reason" to protonProblem))
else {
SessionEvents.record("proton.skipped", mapOf("reason" to protonProblem))
if (BuildConfig.PLAY_DISTRIBUTION) {
problem = protonProblem
failedComponent = "proton"
}
}
}
uiHandler.post {
localInstalling = false
@@ -2064,6 +2077,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
val code = when (failedComponent) {
"runtime" -> "RUNTIME_INSTALL_FAILED"
"package" -> "PACKAGE_RESUME_FAILED"
"proton" -> "PROTON_INSTALL_FAILED"
else -> "DESKTOP_INSTALL_FAILED"
}
SessionEvents.fail(code, problem)
@@ -2149,6 +2163,21 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
val cancellation = com.droiddeck.launcher.core.Downloader.Cancellation { SessionState.stopRequested }
SessionState.installCancellation = cancellation
return try {
if (BuildConfig.PLAY_DISTRIBUTION) {
var last = "" to Int.MIN_VALUE
return com.droiddeck.launcher.session.ProtonExtras.install(
this, com.droiddeck.launcher.session.ProtonExtras.tools.single(), cancellation,
) { message, percent ->
val next = message to percent
if (next != last) {
last = next
uiHandler.post {
loading.percent = percent
loading.say(message, PROTON_LINES.topic, readable = true)
}
}
}
}
val entry = catalog.fetch(catalog.STEAM_SEED_URL, cancellation)?.firstOrNull { it.id == catalog.PROTON_SEED_ID }
?: return if (cancellation.asBoolean) "cancelled" else "catalog unreachable"
catalog.install(this, entry, cancellation, progressFor(PROTON_LINES, entry.size / 1_000_000))
@@ -2520,7 +2549,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
com.droiddeck.launcher.input.HomeButtonShortcuts.cancel(this)
backQamInput.cancel()
padBridge?.releaseAll()
joyConVr?.releaseButtons()
if (Build.VERSION.SDK_INT >= 31) joyConVr?.releaseButtons()
}
private fun focusEndedScreen() {
@@ -2635,6 +2664,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
@android.annotation.SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE -> {
@@ -2759,7 +2789,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
if ((pcKeyboardOpen || achievementsOpen) && fromController) return super.dispatchKeyEvent(event)
if (!fromController && event.keyCode != KeyEvent.KEYCODE_BACK && sendToTopDesktop(event)) return true
if (joyConVr?.onKeyEvent(event) == true) return true
if (Build.VERSION.SDK_INT >= 31 && joyConVr?.onKeyEvent(event) == true) return true
if (fromController && event.keyCode != KeyEvent.KEYCODE_BACK && padMouseMode() && padMouse.onKey(event)) return true
if (event.keyCode != KeyEvent.KEYCODE_BACK && padBridge?.onKeyEvent(event) == true) return true
val keyboardKey = event.keyCode != KeyEvent.KEYCODE_BACK ||
@@ -2772,6 +2802,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
return super.dispatchKeyEvent(event)
}
@android.annotation.SuppressLint("RestrictedApi")
private fun sendToTopDesktop(event: KeyEvent): Boolean {
val key = event.deviceId to event.keyCode
val owned = if (event.action == KeyEvent.ACTION_UP) topDesktopKeys.remove(key) else topDesktopFocused()
@@ -2805,7 +2836,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
if (drawerDirectionKey != KeyEvent.KEYCODE_UNKNOWN) releaseDrawerDirection()
if ((pcKeyboardOpen || achievementsOpen) && PadBridge.isControllerMotionEvent(event)) return super.dispatchGenericMotionEvent(event)
if (joyConVr?.onMotionEvent(event) == true) return true
if (Build.VERSION.SDK_INT >= 31 && joyConVr?.onMotionEvent(event) == true) return true
if (PadBridge.isControllerMotionEvent(event) && padMouseMode() && padMouse.onMotion(event)) return true
if (padBridge?.onMotionEvent(event) == true) return true
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE) && !drawerOpen) {
@@ -2921,6 +2952,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private fun dockedMenu(): SecondScreenPresentation? = secondScreenPresentation?.takeIf { drawerDocked && it.menuShown }
@android.annotation.SuppressLint("RestrictedApi")
private fun dispatchToDrawer(event: KeyEvent): Boolean {
val bottom = dockedMenu()
if (bottom != null && event.keyCode != KeyEvent.KEYCODE_BACK) return bottom.dispatchMenuKey(event)
@@ -4435,7 +4467,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
padBridge?.setStickDeadZone(ControllerPrefs.deadZone(this))
refreshSecondScreenDisplays()
if (SessionState.guestPid > 1) padBridge?.syncConnectedControllers()
joyConVr?.refresh()
if (Build.VERSION.SDK_INT >= 31) joyConVr?.refresh()
onScreenControls?.reload()
controllerSettings = ControllerPrefs.read(this)
resumed = true
@@ -4572,13 +4604,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
glasses.usesSpatialControllers(glassesSettings)
private fun updateJoyConTracking() {
joyConVr?.setActive(steamVrStatus == SteamVr.Status.RUNNING &&
if (Build.VERSION.SDK_INT >= 31) joyConVr?.setActive(steamVrStatus == SteamVr.Status.RUNNING &&
(ControllerPrefs.joyConVr(this) ||
(usesSpatialControllers() && glassesSettings.joyConTracking)))
}
private fun updateHandCamera() {
val joyCons = joyConVr?.holding == true
val joyCons = Build.VERSION.SDK_INT >= 31 && joyConVr?.holding == true
val bare = !joyCons && !controllerConnected()
val want = started && steamVrStatus == SteamVr.Status.RUNNING && (joyCons || bare) &&
!usesSpatialControllers() &&
@@ -4599,7 +4631,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
private fun controllerConnected(): Boolean = InputDevice.getDeviceIds().any { id ->
joyConVr?.owns(id) != true && com.droiddeck.launcher.input.PadBridge.isFromController(InputDevice.getDevice(id))
(Build.VERSION.SDK_INT < 31 || joyConVr?.owns(id) != true) && com.droiddeck.launcher.input.PadBridge.isFromController(InputDevice.getDevice(id))
}
private fun onSteamVrStatus(now: SteamVr.Status) {
@@ -5102,7 +5134,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
headTracker?.stop()
if (::hud.isInitialized) hud.stop()
if (::memoryHud.isInitialized) memoryHud.stop()
joyConVr?.close()
if (Build.VERSION.SDK_INT >= 31) joyConVr?.close()
joyConVr = null
handCamera?.stop()
handCamera = null
@@ -5154,7 +5186,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
val tasks = runCatching { getSystemService(android.app.ActivityManager::class.java)?.appTasks }.getOrNull().orEmpty()
for (task in tasks) {
runCatching {
val info = task.taskInfo
val info = task.taskInfo ?: return@runCatching
@Suppress("DEPRECATION")
val id = if (android.os.Build.VERSION.SDK_INT >= 29) info.taskId else info.persistentId
val base = info.baseIntent
@@ -8,6 +8,7 @@ import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.Handler
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -17,8 +18,12 @@ import com.droiddeck.launcher.ui.UpdatesActions
import com.droiddeck.launcher.ui.UpdatesState
import com.droiddeck.launcher.update.AppUpdates
import com.droiddeck.launcher.update.SelfInstaller
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.UpdateAvailability
internal class UpdatesMenu(private val activity: Activity, private val ui: Handler) {
internal class UpdatesMenu(private val activity: Activity, private val ui: Handler,
private val playManager: () -> AppUpdateManager = { AppUpdateManagerFactory.create(activity) }) {
var catalog by mutableStateOf<AppUpdates.Catalog?>(null)
var follow by mutableStateOf(AppUpdates.Follow(AppUpdates.Channel.STABLE))
var checking by mutableStateOf(false)
@@ -26,12 +31,15 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
var stage by mutableStateOf<String?>(null)
var percent by mutableIntStateOf(-1)
var askPermission by mutableStateOf(false)
var playUpdateAvailable by mutableStateOf<Boolean?>(null)
private var playCheckedAt by mutableStateOf(0L)
private var pending: AppUpdates.Release? = null
private var lastCheck = 0L
private var request = 0
@Volatile private var session = -1
fun state() = UpdatesState(catalog, follow, checking, error, stage, percent, askPermission)
fun state() = UpdatesState(catalog, follow, checking, error, stage, percent, askPermission,
playUpdateAvailable, playCheckedAt)
fun actions() = UpdatesActions(
onCheck = { check(force = true) },
@@ -39,15 +47,22 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
onInstall = ::install,
onAllowInstalls = {
askPermission = false
runCatching { activity.startActivity(SelfInstaller.permissionIntent(activity)) }
.onFailure { error = activity.getString(R.string.upd_unknown_apps_failed); pending = null }
if (!BuildConfig.PLAY_DISTRIBUTION) {
runCatching { activity.startActivity(SelfInstaller.permissionIntent(activity)) }
.onFailure { error = activity.getString(R.string.upd_unknown_apps_failed); pending = null }
}
},
onDismissPermission = { askPermission = false; pending = null },
onOpenPlay = ::openPlay,
)
val hasUpdate: Boolean get() = AppUpdates.hasUpdate(catalog, follow)
val hasUpdate: Boolean get() = if (BuildConfig.PLAY_DISTRIBUTION) playUpdateAvailable == true else AppUpdates.hasUpdate(catalog, follow)
fun onResume() {
if (BuildConfig.PLAY_DISTRIBUTION) {
if (!SessionState.running) check(force = false)
return
}
pending?.let { r ->
if (SelfInstaller.canInstall(activity)) { pending = null; install(r) }
}
@@ -55,6 +70,10 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
}
fun check(force: Boolean) {
if (BuildConfig.PLAY_DISTRIBUTION) {
checkPlay(force)
return
}
if (checking || stage != null) return
val now = System.currentTimeMillis()
if (!force && now - lastCheck < 60_000) return
@@ -75,6 +94,45 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
}, "app-updates").start()
}
private fun checkPlay(force: Boolean) {
if (checking) return
val now = System.currentTimeMillis()
if (!force && now - lastCheck < 60_000) return
lastCheck = now
checking = true
error = null
val id = ++request
fun failed(failure: Throwable) {
if (id != request || activity.isDestroyed) return
checking = false
if (force) error = activity.getString(R.string.upd_couldnt_check, failure.message ?: failure.javaClass.simpleName)
}
runCatching {
playManager().appUpdateInfo.addOnSuccessListener { info ->
if (id != request || activity.isDestroyed) return@addOnSuccessListener
checking = false
val availability = info.updateAvailability()
playUpdateAvailable = when (availability) {
UpdateAvailability.UPDATE_AVAILABLE, UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> true
UpdateAvailability.UPDATE_NOT_AVAILABLE -> false
else -> null
}
playCheckedAt = now
}.addOnFailureListener { failed(it) }
}.onFailure { failed(it) }
}
private fun openPlay() {
if (!BuildConfig.PLAY_DISTRIBUTION) return
val listing = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${activity.packageName}"))
.setPackage("com.android.vending")
runCatching { activity.startActivity(listing) }.onFailure {
runCatching {
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=${activity.packageName}")))
}.onFailure { error = activity.getString(R.string.upd_couldnt_check, it.message ?: it.javaClass.simpleName) }
}
}
private fun choose(f: AppUpdates.Follow) {
AppUpdates.setFollow(activity, f)
follow = f
@@ -82,6 +140,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
}
private fun install(r: AppUpdates.Release) {
if (BuildConfig.PLAY_DISTRIBUTION) { openPlay(); return }
if (stage != null) return
val apk = r.apk ?: run { error = activity.getString(R.string.upd_no_download); return }
AppUpdates.installBlock(r)?.let { error = it.message(activity); return }
@@ -122,7 +181,9 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
}
}
@android.annotation.SuppressLint("UnspecifiedRegisterReceiverFlag")
fun start() {
if (BuildConfig.PLAY_DISTRIBUTION) return
catalog = AppUpdates.cached(activity)
follow = AppUpdates.follow(activity, catalog)
val filter = IntentFilter(SelfInstaller.ACTION_STATUS)
@@ -131,6 +192,8 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
}
fun unregister() {
request++
checking = false
runCatching { activity.unregisterReceiver(receiver) }
}
}
@@ -8,6 +8,7 @@ import com.droiddeck.launcher.core.FileUtils;
import com.droiddeck.launcher.core.SessionPart;
import com.droiddeck.launcher.core.HostProcess;
import com.droiddeck.launcher.core.TarZst;
import com.droiddeck.launcher.BuildConfig;
import java.io.File;
import java.io.InputStream;
@@ -113,7 +114,8 @@ public class PulseAudioComponent extends SessionPart {
env.add("HOME=" + workingDir);
env.add("TMPDIR=" + workingDir);
String command = workingDir.getAbsolutePath() + "/libpulseaudio.so"
String command = HostProcess.LINKER + " " + workingDir.getAbsolutePath() + "/libpulseaudio.so"
+ (BuildConfig.PLAY_DISTRIBUTION ? " --dl-search-path=" + modules.getAbsolutePath() : "")
+ " --system=false --disable-shm=true --fail=false"
+ " -n --file=default.pa --daemonize=false --use-pid-file=false --exit-idle-time=-1"
+ " --log-level=info";
@@ -194,6 +196,7 @@ public class PulseAudioComponent extends SessionPart {
try {
File modules = new File(workingDir, "modules/arm64");
ArrayList<String> cmd = new ArrayList<>();
cmd.add(HostProcess.LINKER);
cmd.add(pactl.getAbsolutePath());
java.util.Collections.addAll(cmd, args);
ProcessBuilder builder = new ProcessBuilder(cmd);
@@ -255,7 +258,7 @@ public class PulseAudioComponent extends SessionPart {
try {
File modules = new File(workingDir, "modules/arm64");
ProcessBuilder builder = new ProcessBuilder(
pactl.getAbsolutePath(), "suspend-" + kind, name, Boolean.toString(suspended));
HostProcess.LINKER, pactl.getAbsolutePath(), "suspend-" + kind, name, Boolean.toString(suspended));
builder.directory(workingDir);
builder.redirectErrorStream(true);
builder.redirectOutput(new File("/dev/null"));
@@ -95,8 +95,8 @@ internal class AddonPack(
}
val root = root(context).apply { mkdirs() }
val archive = File(root, "$sha256.tar.zst")
val downloaded = bundled(context, archive) || urls.any { url ->
Downloader.downloadFile(url, archive, true, sha256) { p -> mutableState.value = State.Downloading(p.coerceIn(0f, 1f)) }
val downloaded = bundled(context, archive) || !BuildConfig.PLAY_DISTRIBUTION && urls.any { url ->
ComponentDownloads.downloadFile(url, archive, true, sha256) { p -> mutableState.value = State.Downloading(p.coerceIn(0f, 1f)) }
.also { if (!it) archive.delete() }
}
if (!downloaded) return false
@@ -0,0 +1,50 @@
package com.droiddeck.launcher.core
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
object AppPermissions {
fun granted(context: Context, permission: String): Boolean =
context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
fun localNetwork(context: Context): Boolean = Build.VERSION.SDK_INT < 37 ||
granted(context, Manifest.permission.ACCESS_LOCAL_NETWORK)
fun startup(activity: Activity): List<String> = buildList {
if (Build.VERSION.SDK_INT >= 37 && !localNetwork(activity) &&
!activity.getSharedPreferences("runtime-permissions", Context.MODE_PRIVATE).getBoolean("local-network", false)) {
add(Manifest.permission.ACCESS_LOCAL_NETWORK)
activity.getSharedPreferences("runtime-permissions", Context.MODE_PRIVATE).edit().putBoolean("local-network", true).apply()
}
if (Build.VERSION.SDK_INT >= 33 && !granted(activity, Manifest.permission.POST_NOTIFICATIONS) &&
!activity.getSharedPreferences("stores", Context.MODE_PRIVATE).getBoolean("asked_notifications", false)) {
add(Manifest.permission.POST_NOTIFICATIONS)
activity.getSharedPreferences("stores", Context.MODE_PRIVATE).edit().putBoolean("asked_notifications", true).apply()
}
}
fun requestLocalNetwork(activity: Activity, requestCode: Int) {
if (Build.VERSION.SDK_INT < 37) return
val prefs = activity.getSharedPreferences("runtime-permissions", Context.MODE_PRIVATE)
if (localNetwork(activity) || prefs.getBoolean("local-network", false) &&
!activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_LOCAL_NETWORK)) {
openSettings(activity)
} else {
prefs.edit().putBoolean("local-network", true).apply()
activity.requestPermissions(arrayOf(Manifest.permission.ACCESS_LOCAL_NETWORK), requestCode)
}
}
fun openSettings(context: Context) {
runCatching {
context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:${context.packageName}"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
}
@@ -0,0 +1,91 @@
package com.droiddeck.launcher.core;
import android.content.Context;
import android.util.Log;
import com.droiddeck.launcher.BuildConfig;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
public final class ComponentDownloads {
private static volatile ComponentPayloadStore store;
private ComponentDownloads() {}
public static void init(Context context) {
if (BuildConfig.PLAY_DISTRIBUTION) {
PlayDelivery.INSTANCE.init(context);
store = new ComponentPayloadStore(path -> {
try {
return PlayDelivery.INSTANCE.open(path);
} catch (IOException error) {
throw new java.io.UncheckedIOException(error);
}
});
}
}
private static ComponentPayloadStore bundled() throws IOException {
ComponentPayloadStore current = store;
if (current == null) throw new IOException("Bundled component delivery is unavailable");
return current;
}
public static String catalogName(String name) {
return BuildConfig.PLAY_DISTRIBUTION ? "play-" + BuildConfig.SOURCE_COMMIT + "-" + name : name;
}
public static String downloadString(String url) {
return downloadString(url, null);
}
public static String downloadString(String url, BooleanSupplier cancelled) {
if (!BuildConfig.PLAY_DISTRIBUTION) return Downloader.downloadString(url, cancelled);
try {
return bundled().read(url, cancelled);
} catch (Exception error) {
Log.w("ComponentDownloads", "Bundled catalog unavailable: " + url, error);
return null;
}
}
public static String downloadDirect(String url) {
return BuildConfig.PLAY_DISTRIBUTION ? downloadString(url) : Downloader.downloadDirect(url);
}
public static long copyBundled(String url, OutputStream output, long limit, Consumer<Float> progress) throws IOException {
if (!BuildConfig.PLAY_DISTRIBUTION) throw new IOException("Bundled delivery requires a Play distribution");
try {
return bundled().copy(url, output, limit, null, null, progress);
} catch (java.io.UncheckedIOException error) {
throw error.getCause();
} catch (Exception error) {
if (error instanceof IOException) throw (IOException) error;
throw new IOException("Invalid bundled component index", error);
}
}
public static boolean downloadFile(String url, File destination, boolean resume, Consumer<Float> progress) {
return downloadFile(url, destination, resume, null, null, progress);
}
public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256, Consumer<Float> progress) {
return downloadFile(url, destination, resume, expectedSha256, null, progress);
}
public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256,
BooleanSupplier cancelled, Consumer<Float> progress) {
if (!BuildConfig.PLAY_DISTRIBUTION) {
return Downloader.downloadFile(url, destination, resume, expectedSha256, cancelled, progress);
}
try {
bundled().install(url, destination, expectedSha256, cancelled, progress);
return true;
} catch (Exception error) {
Log.w("ComponentDownloads", "Bundled component unavailable: " + url, error);
return false;
}
}
}
@@ -0,0 +1,161 @@
package com.droiddeck.launcher.core
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.util.function.BooleanSupplier
import java.util.function.Consumer
internal class ComponentPayloadStore @JvmOverloads constructor(
private val download: (String, File, String, BooleanSupplier?, Consumer<Float>?) -> Boolean = { url, file, digest, cancelled, progress ->
Downloader.downloadCandidates(url, listOf(url), file, false, digest, cancelled, progress)
},
private val assets: (String) -> InputStream,
) {
private val entries by lazy {
val index = readObject("play-components/index.json", 2 * 1024 * 1024)
if (index.getInt("schema") != 1) throw IOException("Unsupported component index")
index.getJSONObject("entries")
}
private fun readObject(path: String, limit: Int = INDEX_LIMIT): JSONObject {
val bytes = assets(path).use { input ->
val output = ByteArrayOutputStream()
val buffer = ByteArray(65536)
while (true) {
val count = input.read(buffer)
if (count < 0) break
if (output.size() + count > limit) throw IOException("Component metadata exceeds its size limit")
output.write(buffer, 0, count)
}
output.toByteArray()
}
return JSONObject(String(bytes, Charsets.UTF_8))
}
fun copy(url: String, output: OutputStream, limit: Long, expectedSha256: String? = null,
cancelled: BooleanSupplier? = null, progress: Consumer<Float>? = null): Long {
checkCancelled(cancelled)
val entry = entries.optJSONObject(url) ?: throw IOException("Component is absent from this build: $url")
val size = entry.getLong("size")
val digest = entry.getString("sha256")
if (size <= 0 || size > limit || !SHA256.matches(digest)) throw IOException("Invalid component metadata")
if (expectedSha256 != null && !digest.equals(expectedSha256, ignoreCase = true)) {
throw IOException("Component version does not match this build")
}
val recipe = "play-components/recipes/$digest.json"
if (entry.getString("recipe") != recipe) throw IOException("Invalid component recipe path")
val chunks = readObject(recipe).getJSONArray("chunks")
if (chunks.length() == 0) throw IOException("Component contains no payload")
val fullDigest = MessageDigest.getInstance("SHA-256")
val buffer = ByteArray(65536)
var total = 0L
var lastPercent = -1
for (index in 0 until chunks.length()) {
checkCancelled(cancelled)
val chunk = chunks.getJSONObject(index)
val path = chunk.getString("asset")
val chunkSize = chunk.getLong("size")
val chunkSha256 = chunk.getString("sha256")
if (!ASSET.matches(path) || chunkSize <= 0 || chunkSize > size - total || !SHA256.matches(chunkSha256)) {
throw IOException("Invalid component chunk metadata")
}
val chunkDigest = MessageDigest.getInstance("SHA-256")
var copied = 0L
assets(path).use { input ->
while (true) {
checkCancelled(cancelled)
val count = input.read(buffer)
if (count < 0) break
if (count.toLong() > chunkSize - copied) throw IOException("Component chunk exceeds pinned size")
chunkDigest.update(buffer, 0, count)
fullDigest.update(buffer, 0, count)
output.write(buffer, 0, count)
copied += count
total += count
val percent = (total * 100 / size).toInt().coerceAtMost(99)
if (percent != lastPercent) {
lastPercent = percent
progress?.accept(percent / 100f)
}
}
}
if (copied != chunkSize || hex(chunkDigest.digest()) != chunkSha256) {
throw IOException("Component chunk failed verification")
}
}
checkCancelled(cancelled)
if (total != size || hex(fullDigest.digest()) != digest) throw IOException("Component failed verification")
progress?.accept(1f)
return total
}
fun read(url: String, cancelled: BooleanSupplier? = null): String {
val output = ByteArrayOutputStream()
copy(url, output, INDEX_LIMIT.toLong(), cancelled = cancelled)
return output.toString(Charsets.UTF_8.name())
}
fun install(url: String, destination: File, expectedSha256: String? = null,
cancelled: BooleanSupplier? = null, progress: Consumer<Float>? = null) {
val parent = destination.absoluteFile.parentFile ?: throw IOException("Component destination has no parent")
Files.createDirectories(parent.toPath())
val temporary = Files.createTempFile(parent.toPath(), destination.name + ".", ".component-part")
try {
val entry = entries.optJSONObject(url) ?: throw IOException("Component is absent from this build: $url")
if (entry.optString("delivery") == "windows-guest") {
val digest = entry.getString("sha256")
val size = entry.getLong("size")
val source = java.net.URI(entry.getString("download_url"))
if (!entry.getString("id").startsWith("windows-") || !SHA256.matches(digest) || size <= 0 ||
source.scheme != "https" || source.host.isNullOrEmpty() || source.userInfo != null || source.fragment != null ||
expectedSha256 != null && !digest.equals(expectedSha256, ignoreCase = true)) {
throw IOException("Invalid Windows prerequisite identity")
}
checkCancelled(cancelled)
if (!download(source.toString(), temporary.toFile(), digest, cancelled, progress) || Files.size(temporary) != size) {
throw IOException("Windows prerequisite download failed verification")
}
val actual = Files.newInputStream(temporary).use { input ->
val hash = MessageDigest.getInstance("SHA-256")
val buffer = ByteArray(65536)
while (true) {
checkCancelled(cancelled)
val count = input.read(buffer)
if (count < 0) break
hash.update(buffer, 0, count)
}
hex(hash.digest())
}
if (actual != digest) throw IOException("Windows prerequisite failed verification")
} else {
Files.newOutputStream(temporary).use { output ->
copy(url, output, Long.MAX_VALUE, expectedSha256, cancelled, progress)
}
}
checkCancelled(cancelled)
Files.move(temporary, destination.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
} finally {
Files.deleteIfExists(temporary)
Files.deleteIfExists(temporary.resolveSibling(temporary.fileName.toString() + ".resume.properties"))
}
}
private fun checkCancelled(cancelled: BooleanSupplier?) {
if (Thread.currentThread().isInterrupted || cancelled?.asBoolean == true) throw IOException("Component copy cancelled")
}
private fun hex(bytes: ByteArray) = bytes.joinToString("") { "%02x".format(it.toInt() and 255) }
companion object {
private const val INDEX_LIMIT = 16 * 1024 * 1024
private val SHA256 = Regex("[0-9a-f]{64}")
private val ASSET = Regex("play-components/[0-9a-f]{64}/[0-9]{4,}")
}
}
@@ -111,7 +111,7 @@ object DeviceReport {
h("App")
runCatching {
val info = context.packageManager.getPackageInfo(context.packageName, 0)
k("Version", "${info.versionName} (${info.longVersionCode})")
k("Version", "${info.versionName} (${androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(info)})")
k("App build", "${com.droiddeck.launcher.BuildConfig.BUILD_LABEL} (source ${com.droiddeck.launcher.BuildConfig.SOURCE_COMMIT})")
}
k("Package", context.packageName)
@@ -139,11 +139,13 @@ public final class Downloader {
java.util.function.Consumer<Float> progress) {
Identity identity = loadIdentity(destination, resume, expectedSha256);
for (int i = 0; i < sources.size(); i++) {
if (Thread.currentThread().isInterrupted()) return false;
String source = sources.get(i);
if (isCancelled(cancelled)) return false;
if (i > 0) Log.i(TAG, "retrying " + original + " via " + source);
Outcome outcome = attempt(source, destination, resume, identity, expectedSha256, cancelled, progress);
if (outcome == Outcome.CANCELLED) return false;
if (Thread.currentThread().isInterrupted()) return false;
if (outcome == Outcome.DONE) {
if (expectedSha256 != null) {
String actualSha256 = Hashes.sha256(destination);
@@ -296,6 +298,7 @@ public final class Downloader {
byte[] buffer = new byte[1 << 16];
long lastReport = 0;
for (int read = in.read(buffer); read > 0; read = in.read(buffer)) {
if (Thread.currentThread().isInterrupted()) return Outcome.RETRY;
out.write(buffer, 0, read);
written += read;
if (isCancelled(cancelled)) return Outcome.CANCELLED;
@@ -0,0 +1,18 @@
package com.droiddeck.launcher.core
import android.app.Notification
import android.app.Service
import android.os.Build
import android.util.Log
object ForegroundServices {
fun start(service: Service, id: Int, notification: Notification, types: Int = 0): Boolean = try {
if (Build.VERSION.SDK_INT >= 29 && types != 0) service.startForeground(id, notification, types)
else service.startForeground(id, notification)
true
} catch (error: RuntimeException) {
if (Build.VERSION.SDK_INT < 31 || error !is android.app.ForegroundServiceStartNotAllowedException) throw error
Log.w("ForegroundServices", "Foreground work cannot start in the current lifecycle", error)
false
}
}
@@ -5,6 +5,8 @@ import java.io.File
import java.util.function.Consumer
object HostProcess {
const val LINKER = "/system/bin/linker64"
private const val TAG = "HostProcess"
@JvmStatic
@@ -53,14 +55,17 @@ object HostProcess {
}
@JvmStatic
fun pidOf(process: Process): Int = try {
val field = process.javaClass.getDeclaredField("pid")
field.isAccessible = true
val pid = field.getInt(process)
field.isAccessible = false
pid
} catch (e: Exception) {
-1
fun pidOf(process: Process): Int {
Regex("""\bpid=(\d+)\b""").find(process.toString())?.groupValues?.get(1)?.toIntOrNull()?.let { return it }
return try {
val field = process.javaClass.getDeclaredField("pid")
field.isAccessible = true
val pid = field.getInt(process)
field.isAccessible = false
pid
} catch (e: Exception) {
-1
}
}
@JvmStatic
@@ -0,0 +1,253 @@
package com.droiddeck.launcher.core
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.droiddeck.launcher.BuildConfig
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.tasks.Task
import java.util.concurrent.TimeUnit
import com.google.android.play.core.assetpacks.AssetPackManagerFactory
import com.google.android.play.core.assetpacks.model.AssetPackStatus
import com.google.android.play.core.splitcompat.SplitCompat
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory
import com.google.android.play.core.splitinstall.SplitInstallRequest
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONObject
import java.io.File
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.ZipFile
internal object PlayDelivery {
data class State(val ready: Boolean = !BuildConfig.PLAY_DISTRIBUTION, val busy: Boolean = false,
val failed: Boolean = false, val percent: Int = -1)
private val mutableState = MutableStateFlow(State())
val state = mutableState.asStateFlow()
private lateinit var app: Context
private val handler = Handler(Looper.getMainLooper())
private val running = AtomicBoolean(false)
private val cancelled = AtomicBoolean(false)
private var activity = WeakReference<Activity>(null)
@Volatile private var session = 0
@Volatile private var confirmation = false
@Volatile private var attempt = 0
private var pendingSession: android.content.Intent? = null
private var server: android.net.LocalServerSocket? = null
private val clients = java.util.concurrent.Semaphore(2)
private val splits by lazy { SplitInstallManagerFactory.create(app) }
private val assets by lazy { AssetPackManagerFactory.getInstance(app) }
private val definition by lazy {
app.assets.open("play-components/delivery.json").bufferedReader().use { JSONObject(it.readText()) }
}
private fun names(key: String): List<String> = definition.getJSONArray(key).let { list ->
(0 until list.length()).map { list.getString(it) }
}
fun init(context: Context) {
if (!BuildConfig.PLAY_DISTRIBUTION) return
app = context.applicationContext
}
@Synchronized
fun defer(intent: android.content.Intent) { pendingSession = android.content.Intent(intent) }
@Synchronized
fun peekPendingSession(): android.content.Intent? = pendingSession?.let { android.content.Intent(it) }
@Synchronized
fun takePendingSession(): android.content.Intent? = pendingSession.also { pendingSession = null }
fun start(owner: Activity) {
if (!BuildConfig.PLAY_DISTRIBUTION) return
activity = WeakReference(owner)
if (mutableState.value.ready) return
if (!running.compareAndSet(false, true)) return
cancelled.set(false)
attempt++
confirmation = false
mutableState.value = State(busy = true)
Thread({
var completed = State(failed = true)
try {
installModules()
confirmation = false
installAssets()
checkCancelled()
startServer()
completed = State(ready = true, percent = 100)
} catch (error: Exception) {
android.util.Log.w("PlayDelivery", "Play component delivery failed", error)
} finally {
if (cancelled.get()) cancel()
running.set(false)
mutableState.value = completed
}
}, "play-components").start()
}
fun detach(owner: Activity) {
if (activity.get() === owner) activity.clear()
}
fun cancel() {
cancelled.set(true)
synchronized(this) { pendingSession = null }
if (session > 0) splits.cancelInstall(session)
if (::app.isInitialized) runCatching { assets.cancel(names("packs")) }
}
private fun <T> await(task: Task<T>): T = Tasks.await(task, 30, TimeUnit.SECONDS)
private fun checkCancelled() {
if (cancelled.get() || Thread.currentThread().isInterrupted) throw IOException("Play delivery cancelled")
}
private fun installModules() {
val missing = names("modules").filterNot { it in splits.installedModules }
if (missing.isEmpty()) return
val previous = await(splits.sessionStates).firstOrNull {
it.moduleNames().containsAll(missing) && it.status() in setOf(
SplitInstallSessionStatus.PENDING, SplitInstallSessionStatus.DOWNLOADING,
SplitInstallSessionStatus.DOWNLOADED, SplitInstallSessionStatus.INSTALLING,
SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION)
}
session = previous?.sessionId() ?: run {
val request = attempt
val task = splits.startInstall(SplitInstallRequest.newBuilder().apply { missing.forEach(::addModule) }.build())
task.addOnSuccessListener { id -> if (request == attempt && cancelled.get()) splits.cancelInstall(id) }
await(task)
}
while (true) {
checkCancelled()
val current = await(splits.getSessionState(session))
val request = attempt
when (current.status()) {
SplitInstallSessionStatus.INSTALLED -> {
if (!SplitCompat.install(app) || !splits.installedModules.containsAll(missing))
throw IOException("Play modules could not be activated")
session = 0
return
}
SplitInstallSessionStatus.FAILED, SplitInstallSessionStatus.CANCELED ->
throw IOException("Play module delivery failed: ${current.errorCode()}")
SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> handler.post {
val owner = activity.get()
if (request == attempt && session == current.sessionId() && running.get() && !cancelled.get() &&
!confirmation && owner != null && !owner.isFinishing && !owner.isDestroyed) {
runCatching { splits.startConfirmationDialogForResult(current, owner, 7619) }
.onSuccess { confirmation = it }.onFailure { cancel() }
}
}
}
val total = current.totalBytesToDownload()
mutableState.value = State(busy = true, percent = if (total > 0)
(current.bytesDownloaded() * 80 / total).toInt().coerceIn(0, 80) else -1)
Thread.sleep(350)
}
}
private fun installAssets() {
val packs = names("packs")
val missing = packs.filter { assets.getPackLocation(it) == null }
if (missing.isEmpty()) return
val requestAttempt = attempt
val fetch = assets.fetch(missing)
fetch.addOnSuccessListener { if (requestAttempt == attempt && cancelled.get()) assets.cancel(missing) }
await(fetch)
while (true) {
checkCancelled()
val states = await(assets.getPackStates(missing)).packStates().values
if (states.any { it.status() == AssetPackStatus.FAILED || it.status() == AssetPackStatus.CANCELED })
throw IOException("Play data delivery failed")
if (missing.all { assets.getPackLocation(it) != null }) return
val request = attempt
if (states.any { it.status() == AssetPackStatus.WAITING_FOR_WIFI || it.status() == AssetPackStatus.REQUIRES_USER_CONFIRMATION })
handler.post {
val owner = activity.get()
if (request == attempt && running.get() && !cancelled.get() &&
!confirmation && owner != null && !owner.isFinishing && !owner.isDestroyed) {
confirmation = true
assets.showConfirmationDialog(owner).addOnSuccessListener { result ->
if (request == attempt && result != Activity.RESULT_OK) cancel()
}.addOnFailureListener { if (request == attempt) cancel() }
}
}
val total = states.sumOf { it.totalBytesToDownload() }
val downloaded = states.sumOf { it.bytesDownloaded() }
mutableState.value = State(busy = true, percent = if (total > 0)
(80 + downloaded * 19 / total).toInt().coerceIn(80, 99) else -1)
Thread.sleep(500)
}
}
@Synchronized
private fun startServer() {
if (server != null) return
val listener = android.net.LocalServerSocket(app.packageName + ".play-components")
server = listener
Thread({
while (true) {
val client = try { listener.accept() } catch (_: IOException) { break }
if (!clients.tryAcquire()) { client.close(); continue }
Thread({
try {
client.use {
client.soTimeout = 30_000
if (client.peerCredentials.uid != android.os.Process.myUid()) return@use
val input = java.io.DataInputStream(client.inputStream)
val length = input.readInt()
if (length !in 1..256) return@use
val path = ByteArray(length).also(input::readFully).toString(Charsets.UTF_8)
if (!Regex("play-components/[0-9a-f]{64}/[0-9]{4,}").matches(path)) return@use
val output = client.outputStream
open(path).use { source ->
output.write(1)
source.copyTo(output, 65536)
}
}
} catch (error: IOException) {
android.util.Log.w("PlayDelivery", "Component stream closed", error)
} finally {
clients.release()
}
}, "play-component-reader").apply { isDaemon = true; start() }
}
}, "play-component-server").apply { isDaemon = true; start() }
}
@Throws(IOException::class)
fun open(path: String): InputStream {
try {
return app.assets.open(path)
} catch (_: java.io.FileNotFoundException) {
val info = app.packageManager.getApplicationInfo(app.packageName, 0)
for (split in info.splitSourceDirs.orEmpty()) {
val archive = ZipFile(split)
val entry = archive.getEntry("assets/$path")
if (entry == null) {
archive.close()
continue
}
return object : FilterInputStream(archive.getInputStream(entry)) {
override fun close() { try { super.close() } finally { archive.close() } }
}
}
for (name in names("packs")) {
val directory = assets.getPackLocation(name)?.assetsPath()?.let(::File) ?: continue
val file = File(directory, path)
if (file.canonicalPath.startsWith(directory.canonicalPath + File.separator) && file.isFile)
return file.inputStream()
}
if (!running.get()) mutableState.value = State(failed = true)
throw IOException("Play component is not installed: $path")
}
}
}
@@ -13,6 +13,7 @@ import com.droiddeck.launcher.input.HomeButtonService
import com.droiddeck.launcher.session.SessionPrefs
object SteamIntegration {
private const val ACCESSIBILITY_CONSENT_VERSION = 1
const val ACTION_SETUP = "com.droiddeck.launcher.STEAM_INTEGRATION"
const val EXTRA_FEATURE = "integration_feature"
enum class Feature { WIFI, BRIGHTNESS, HOME, SHORTCUTS }
@@ -36,7 +37,7 @@ object SteamIntegration {
locationOn = WifiDiscovery.locationEnabled(context),
brightness = Settings.System.canWrite(context),
home = HomeApp.isDefault(context),
shortcuts = context.getSystemService(AccessibilityManager::class.java)
shortcuts = accessibilityConsented(context) && context.getSystemService(AccessibilityManager::class.java)
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK).any {
val service = it.resolveInfo.serviceInfo
ComponentName(service.packageName, service.name) == ComponentName(context, HomeButtonService::class.java)
@@ -56,6 +57,14 @@ object SteamIntegration {
private fun shortcutsComponent(context: Context) = ComponentName(context, HomeButtonService::class.java)
fun accessibilityConsented(context: Context): Boolean =
SessionPrefs.sessionPrefs(context).getInt("accessibility_consent", 0) == ACCESSIBILITY_CONSENT_VERSION
fun acceptAccessibility(context: Context) {
SessionPrefs.sessionPrefs(context).edit().putInt("accessibility_consent", ACCESSIBILITY_CONSENT_VERSION).apply()
HomeButtonService.consentChanged()
}
fun shortcutsChosen(context: Context): Boolean = shortcutsSelection(context) == true
private fun shortcutsSelection(context: Context): Boolean? {
@@ -0,0 +1,23 @@
package com.droiddeck.launcher.core
import android.os.Build
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
object SystemInsets {
fun fit(view: View) {
if (Build.VERSION.SDK_INT < 35) return
val left = view.paddingLeft
val top = view.paddingTop
val right = view.paddingRight
val bottom = view.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(view) { target, insets ->
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or
WindowInsetsCompat.Type.displayCutout() or WindowInsetsCompat.Type.ime())
target.setPadding(left + safe.left, top + safe.top, right + safe.right, bottom + safe.bottom)
insets
}
ViewCompat.requestApplyInsets(view)
}
}
@@ -10,9 +10,10 @@ import com.droiddeck.launcher.session.SessionPrefs
object WifiDiscovery {
val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
fun permissionGranted(context: Context): Boolean = permissions.any {
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
}
fun permissionGranted(context: Context): Boolean =
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) else permissions).any {
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
}
fun locationEnabled(context: Context): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
(context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager)?.isLocationEnabled == true
@@ -60,7 +60,10 @@ class WirelessAdbPairingService : Service() {
pairingPort = null
working = false
publish(Stage.Waiting)
startForeground(NOTIFICATION_ID, notification(Stage.Waiting))
if (!ForegroundServices.start(this, NOTIFICATION_ID, notification(Stage.Waiting))) {
finish(Stage.Failed(getString(R.string.adbpair_title_failed)))
return
}
startDiscovery()
main.removeCallbacks(timeout)
main.postDelayed(timeout, TIMEOUT_MS)
@@ -256,6 +259,10 @@ class WirelessAdbPairingService : Service() {
private fun publish(stage: Stage) { current.value = stage }
fun start(context: Context) {
if (!AppPermissions.localNetwork(context)) {
publish(Stage.Failed(context.getString(R.string.setup_local_network)))
return
}
context.getSystemService(NotificationManager::class.java)?.cancel(NOTIFICATION_ID)
publish(Stage.Waiting)
val intent = Intent(context, WirelessAdbPairingService::class.java)
@@ -1,21 +1,52 @@
package com.droiddeck.launcher.files
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import com.droiddeck.launcher.R
import com.droiddeck.launcher.frontend.AddedExes
import com.droiddeck.launcher.stores.StoreInstallRoot
object AllFilesAccess {
fun missing(sdk: Int = Build.VERSION.SDK_INT, granted: () -> Boolean = { Environment.isExternalStorageManager() }): Boolean =
fun granted(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()
fun usesSharedStorage(context: Context): Boolean {
val own = (listOf(context.filesDir.parentFile) + context.getExternalFilesDirs(null).map { it?.parentFile })
.filterNotNull().map { it.absolutePath + "/" }
fun shared(path: String) = own.none { (path + "/").startsWith(it) }
return AddedExes.list(context).any { shared(it.folder) } ||
StoreInstallRoot.roots(context).any { shared(it.absolutePath) }
}
fun ask(activity: Activity): Boolean {
if (granted() || activity.isFinishing || activity.isDestroyed) return false
AlertDialog.Builder(activity)
.setTitle(R.string.setup_all_files)
.setMessage(R.string.fm_drive_access_text)
.setPositiveButton(R.string.upd_open_settings) { _, _ -> openSettings(activity) }
.setNegativeButton(R.string.common_cancel, null)
.show()
return true
}
fun missing(sdk: Int = Build.VERSION.SDK_INT, granted: () -> Boolean = { AllFilesAccess.granted() }): Boolean =
sdk >= Build.VERSION_CODES.R && !granted()
fun openSettings(activity: Activity) = open(activity)
fun open(context: Context) {
val app = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + context.packageName))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
runCatching { context.startActivity(app) }.recoverCatching {
context.startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
val forApp = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + context.packageName)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(forApp)
} catch (e: ActivityNotFoundException) {
runCatching { context.startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }
}
}
}
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.files
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -22,8 +23,11 @@ class FileManagerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = com.droiddeck.launcher.core.AppUiPrefs.orientation(this)
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
com.droiddeck.launcher.files.AllFilesAccess.ask(this)
} else if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
val initialDir = intent.getStringExtra(EXTRA_START_DIR)?.let(::File)
setContent {
@@ -1210,7 +1210,7 @@ fun FileManagerScreen(
)
}
}
} else
} else {
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
if (canGoUp) item(key = "..") {
ParentFolderRow(compact = compactRows, onTap = { requestNavigation(FileManagerNavigation.UP) })
@@ -1290,6 +1290,7 @@ fun FileManagerScreen(
}
}
}
}
if (pullState.verticalOffset > 0.5f || pullState.isRefreshing) {
PullToRefreshContainer(
state = pullState,
@@ -4,6 +4,7 @@ import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -26,8 +27,11 @@ class FilePickerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = com.droiddeck.launcher.core.AppUiPrefs.orientation(this)
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
com.droiddeck.launcher.files.AllFilesAccess.ask(this)
} else if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
val extensions = intent.getStringArrayExtra(EXTRA_EXTENSIONS)?.toList() ?: emptyList()
@@ -90,7 +90,7 @@ object StorageRoots {
.filter { present(it.state) }
.forEachIndexed { index, volume ->
val uuid = volume.uuid?.takeIf { it.isNotBlank() }
val dirName = volume.directory?.name?.takeIf { it.isNotBlank() }
val dirName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) volume.directory?.name?.takeIf { it.isNotBlank() } else null
val descSlug = volume.getDescription(context)?.trim()
?.takeIf { it.isNotBlank() && !it.equals("android", ignoreCase = true) }
?.replace(Regex("[^A-Za-z0-9._-]+"), "-")
@@ -523,9 +523,12 @@ object AddedGames {
}
}
fun listingFile(context: Context): File = File(context.filesDir, "session/added-games.json")
@Synchronized
fun writeListing(context: Context, games: List<Game>,
extra: List<JSONObject> = listOfNotNull(com.droiddeck.launcher.stores.epic.EpicClient.steamListing(context))): File {
val file = File(context.filesDir, "session/added-games.json").apply { parentFile?.mkdirs() }
val file = listingFile(context).apply { parentFile?.mkdirs() }
val json = StringBuilder("[")
games.forEachIndexed { i, g ->
if (i > 0) json.append(',')
@@ -545,7 +548,13 @@ object AddedGames {
if (games.isNotEmpty() || i > 0) json.append(',')
json.append(entry.toString())
}
file.writeText(json.append(']').toString())
val staged = File(file.path + ".tmp")
staged.writeText(json.append(']').toString())
java.nio.file.Files.move(
staged.toPath(), file.toPath(),
java.nio.file.StandardCopyOption.ATOMIC_MOVE,
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
)
return file
}
@@ -3,7 +3,9 @@ package com.droiddeck.launcher.frontend
import android.content.Context
import android.util.Log
import androidx.annotation.VisibleForTesting
import org.json.JSONArray
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
object AddedGamesPublisher {
private const val TAG = "AddedGamesPublisher"
@@ -13,6 +15,7 @@ object AddedGamesPublisher {
internal var now = false
private val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "added-games-publish") }
private val retries = Executors.newSingleThreadScheduledExecutor { r -> Thread(r, "added-games-publish-retry") }
fun later(context: Context, live: () -> Unit = {}) {
val app = context.applicationContext
@@ -23,6 +26,43 @@ object AddedGamesPublisher {
if (now) work.run() else worker.execute(work)
}
fun refreshDuringSession(context: Context, active: () -> Boolean) {
val app = context.applicationContext
worker.execute {
try {
if (!active()) return@execute
val before = AddedGames.listingFile(app).takeIf { it.isFile }?.readText().orEmpty()
val known = runCatching {
val listing = JSONArray(before)
List(listing.length()) { listing.getJSONObject(it).optString("exe") }.toSet()
}.getOrDefault(emptySet())
val games = AddedGames.scan(app)
if (!active()) return@execute
AddedGames.writeListing(app, games)
val arch = com.droiddeck.launcher.session.SessionPrefs.activeSteamClientArch(app)
LibraryCache.save(app, Library.launchableGames(app, added = games, arch = arch), arch == "x86_64")
val fresh = games.filter { it.guestExe !in known }
if (fresh.isEmpty()) return@execute
Log.i(TAG, "session refresh: ${fresh.size} game(s) not in the listing the session started with")
retries.scheduleWithFixedDelay({
try {
if (!active()) throw java.util.concurrent.CancellationException()
if (com.droiddeck.launcher.stores.SteamLiveShortcuts.sync(app, fresh) { true } ||
!com.droiddeck.launcher.session.SessionState.running) {
throw java.util.concurrent.CancellationException()
}
} catch (e: java.util.concurrent.CancellationException) {
throw e
} catch (e: Exception) {
Log.w(TAG, "session shortcut retry: ${e.message}")
}
}, 5, 5, TimeUnit.SECONDS)
} catch (e: Exception) {
Log.w(TAG, "session refresh: ${e.message}")
}
}
}
fun republish(context: Context) {
val games = AddedGames.scan(context)
AddedGames.writeListing(context, games)
@@ -162,6 +162,7 @@ object Glasses {
private val simulating = java.util.concurrent.atomic.AtomicReference<Thread?>(null)
private val frames = ArrayDeque<String>()
@android.annotation.SuppressLint("UnspecifiedRegisterReceiverFlag")
fun init(context: Context) {
if (::handler.isInitialized) return
app = context.applicationContext
@@ -33,7 +33,7 @@ internal class AndroidXrealConnector(context: Context) : XrealConnector {
companion object {
internal fun hasDirectRoute(properties: LinkProperties, address: InetAddress): Boolean =
properties.routes.any { !it.hasGateway() && it.destination.prefixLength > 0 && it.matches(address) }
properties.routes.any { (it.gateway?.isAnyLocalAddress != false) && it.destination.prefixLength > 0 && it.matches(address) }
}
override fun connect(): XrealChannels {
@@ -1,9 +1,10 @@
package com.droiddeck.launcher.gpu
import com.droiddeck.launcher.core.DownloadSource
import com.droiddeck.launcher.core.Downloader
import com.droiddeck.launcher.core.ComponentDownloads
import com.droiddeck.launcher.core.Hashes
import com.droiddeck.launcher.R
import com.droiddeck.launcher.BuildConfig
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.core.FileUtils
@@ -17,9 +18,9 @@ import java.net.URL
object TurnipReleases {
private const val TAG = "TurnipReleases"
private const val PREFS = "turnip_releases"
private const val KEY_RELEASE = "latest"
private val KEY_RELEASE = ComponentDownloads.catalogName("latest")
private const val KEY_DOWNLOADS = "downloads"
private const val KEY_PICKS = "autoPicks"
private val KEY_PICKS = ComponentDownloads.catalogName("autoPicks")
class Asset(val source: String, val tag: String, val name: String, val url: String, val size: Long, val linux: Boolean, val label: String,
val sha256: String = "", val pair: String = "", val bundle: Boolean = false,
@@ -155,6 +156,8 @@ object TurnipReleases {
}
private fun get(context: Context, source: String, url: String): String {
if (BuildConfig.PLAY_DISTRIBUTION) return ComponentDownloads.downloadString(url)
?: throw IOException("Driver catalog is absent from this build")
val c = URL(source).openConnection() as HttpURLConnection
c.connectTimeout = 15_000
c.readTimeout = 20_000
@@ -248,7 +251,7 @@ object TurnipReleases {
fun download(context: Context, asset: Asset, progress: (Int) -> Unit): File {
if (asset.sha256.isEmpty()) throw IOException(context.getString(R.string.turnip_list_predates_checksums))
val target = File(context.cacheDir, asset.name)
val ok = Downloader.downloadFile(asset.url, target, true, asset.sha256) { f -> progress(if (f < 0) -1 else (f * 100).toInt()) }
val ok = ComponentDownloads.downloadFile(asset.url, target, true, asset.sha256) { f -> progress(if (f < 0) -1 else (f * 100).toInt()) }
if (!ok) throw IOException(context.getString(R.string.turnip_download_failed))
if (!asset.sha256.equals(Hashes.sha256(target), ignoreCase = true)) {
FileUtils.delete(target)
@@ -37,8 +37,11 @@ internal object TrackingPack {
if (nativeLoaded) return true
if (!pack.installed(context)) return false
return runCatching {
System.load(file(context, OPENCV).absolutePath)
System.load(file(context, MEDIAPIPE).absolutePath)
for (name in listOf(OPENCV, MEDIAPIPE)) {
val library = file(context, name)
check(library.isFile && library.setReadOnly()) { "Tracking library cannot be made read-only: $name" }
System.load(library.absolutePath)
}
nativeLoaded = true
true
}.onFailure { Log.e(TAG, "native libraries failed to load", it) }.getOrDefault(false)
@@ -42,6 +42,7 @@ object BottomScreenGuests {
sdk >= 33 && DualScreen.yieldsBottom(model)
}
@androidx.annotation.RequiresApi(33)
class BottomScreenWatch(private val service: AccessibilityService) {
private class Arrival(val pkg: String)
@@ -65,7 +65,8 @@ class DesktopKeyboardButton(context: Context, private val onTap: () -> Unit) : V
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
val cutout = if (Build.VERSION.SDK_INT >= 28) insets.displayCutout else null
place(cutout?.safeInsetLeft ?: 0, cutout?.safeInsetBottom ?: 0)
if (Build.VERSION.SDK_INT >= 28) place(cutout?.safeInsetLeft ?: 0, cutout?.safeInsetBottom ?: 0)
else place(0, 0)
return super.onApplyWindowInsets(insets)
}
@@ -9,6 +9,8 @@ import android.os.Looper
import android.view.KeyEvent
import android.view.accessibility.AccessibilityEvent
import com.droiddeck.launcher.HomeApp
import com.droiddeck.launcher.core.SteamIntegration
import java.lang.ref.WeakReference
object HomeButtonShortcuts {
private var owner: Any? = null
@@ -103,26 +105,38 @@ object SystemKeyCapture {
class HomeButtonService : AccessibilityService(), InputManager.InputDeviceListener {
private var bottomScreen: BottomScreenWatch? = null
private var consented = false
override fun onServiceConnected() {
current = WeakReference(this)
getSystemService(InputManager::class.java).registerInputDeviceListener(this, Handler(Looper.getMainLooper()))
if (bottomScreen == null && BottomScreenGuests.supported()) bottomScreen = BottomScreenWatch(this).also { it.start() }
updateConsent()
}
private fun updateConsent() {
consented = SteamIntegration.accessibilityConsented(this)
if (consented && android.os.Build.VERSION.SDK_INT >= 33 && bottomScreen == null && BottomScreenGuests.supported()) {
bottomScreen = BottomScreenWatch(this).also { it.start() }
}
if (!consented) stopBottomScreen()
}
private fun stopBottomScreen() {
bottomScreen?.stop()
if (android.os.Build.VERSION.SDK_INT >= 33) bottomScreen?.stop()
bottomScreen = null
}
override fun onKeyEvent(event: KeyEvent) =
SystemKeyCapture.onKeyEvent(event) || HomeButtonShortcuts.onKeyEvent(this, event)
override fun onAccessibilityEvent(event: AccessibilityEvent?) { event?.let { bottomScreen?.onEvent(it) } }
consented && (SystemKeyCapture.onKeyEvent(event) || HomeButtonShortcuts.onKeyEvent(this, event))
override fun onAccessibilityEvent(event: AccessibilityEvent?) { if (android.os.Build.VERSION.SDK_INT >= 33) event?.let { bottomScreen?.onEvent(it) } }
override fun onInterrupt() { HomeButtonShortcuts.release() }
override fun onInputDeviceAdded(deviceId: Int) {}
override fun onInputDeviceChanged(deviceId: Int) {}
override fun onInputDeviceRemoved(deviceId: Int) { HomeButtonShortcuts.onDeviceRemoved(deviceId) }
override fun onUnbind(intent: Intent?): Boolean {
if (current?.get() === this) current = null
consented = false
HomeButtonShortcuts.release()
stopBottomScreen()
com.droiddeck.launcher.core.SteamIntegration.syncShortcutsListing(this)
@@ -130,9 +144,16 @@ class HomeButtonService : AccessibilityService(), InputManager.InputDeviceListen
}
override fun onDestroy() {
if (current?.get() === this) current = null
HomeButtonShortcuts.release()
stopBottomScreen()
getSystemService(InputManager::class.java).unregisterInputDeviceListener(this)
super.onDestroy()
}
companion object {
private var current: WeakReference<HomeButtonService>? = null
fun consentChanged() { current?.get()?.updateConsent() }
}
}
@@ -211,7 +211,7 @@ class OnScreenControls(
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
val cutout = if (Build.VERSION.SDK_INT >= 28) insets.displayCutout else null
val fresh = Rect(cutout?.safeInsetLeft ?: 0, cutout?.safeInsetTop ?: 0, cutout?.safeInsetRight ?: 0, cutout?.safeInsetBottom ?: 0)
val fresh = if (Build.VERSION.SDK_INT >= 28) Rect(cutout?.safeInsetLeft ?: 0, cutout?.safeInsetTop ?: 0, cutout?.safeInsetRight ?: 0, cutout?.safeInsetBottom ?: 0) else Rect()
if (fresh != safe) { safe = fresh; relayout() }
return super.onApplyWindowInsets(insets)
}
@@ -302,10 +302,10 @@ class ScreenKeys(context: Context, private val sendKey: (Int, Boolean) -> Unit)
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
val cutout = if (Build.VERSION.SDK_INT >= 28) insets.displayCutout else null
val fresh = Rect(
val fresh = if (Build.VERSION.SDK_INT >= 28) Rect(
cutout?.safeInsetLeft ?: 0, cutout?.safeInsetTop ?: 0,
cutout?.safeInsetRight ?: 0, cutout?.safeInsetBottom ?: 0,
)
) else Rect()
if (fresh != safe) {
safe = fresh
placeToolbar()
@@ -52,6 +52,11 @@ class SteamControllerBle(private val context: Context) {
private fun connect() {
if (!running || gatt != null) return
if (android.os.Build.VERSION.SDK_INT >= 31 &&
!com.droiddeck.launcher.core.AppPermissions.granted(context, android.Manifest.permission.BLUETOOTH_CONNECT)) {
handler.postDelayed(::connect, RETRY_MS)
return
}
if (SessionState.suspended) {
handler.postDelayed(::connect, RETRY_MS)
return
@@ -86,8 +91,8 @@ class SteamControllerBle(private val context: Context) {
outputReports.clear()
connected = false
FakeInputWriter.setTritonConnected(SLOT, false)
g.disconnect()
g.close()
runCatching { g.disconnect() }
runCatching { g.close() }
release()
Log.i(TAG, "disconnected: $why")
listener?.invoke()
@@ -197,7 +197,7 @@ object SteamTouchConfig {
}
return try {
val text = file.readText()
val kv = KeyValues.parse(text.removePrefix(""))
val kv = KeyValues.parse(text.removePrefix("\uFEFF"))
val mappings = kv.child("controller_mappings") ?: kv
val layouts = mappings.string("touch_layout")?.let { hex -> decodeLayouts(hexToBytes(hex)) }
Log.i(TAG, "steam touch: app $appId uses ${file.path} (${layouts?.layouts?.size ?: 0} layouts)")
@@ -991,7 +991,7 @@ class SteamTouchControls(
BIG_PICTURE -> "Steam"
else -> gameNames[appId] ?: run {
val app = appId
gameNames[app] = context.getString(R.string.touch_app_id, Integer.toUnsignedLong(app))
gameNames[app] = context.getString(R.string.touch_app_id, Integer.toUnsignedLong(app).toString())
loader.execute {
val name = SteamTouchConfig.gameName(context, app) ?: return@execute
handler.post { gameNames[app] = name; invalidate() }
@@ -352,6 +352,7 @@ object BwrapSpawner {
val verbose = plan.env["DROIDDECK_PROOT_VERBOSE"]?.toIntOrNull()
if (verbose != null) { cmd.add(1, "-v"); cmd.add(2, verbose.toString()) }
plan.binds.forEach { cmd.add("-b"); cmd.add(it.spec) }
LinuxRuntime.bindComponentBundles(context, plan.root, cmd)
cmd.add(plan.envTool)
cmd.add("-i")
plan.env.forEach { (k, v) -> cmd.add("$k=$v") }
@@ -4,7 +4,7 @@ import com.droiddeck.launcher.core.Hashes
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.R
import com.droiddeck.launcher.core.Downloader
import com.droiddeck.launcher.core.ComponentDownloads
import com.droiddeck.launcher.session.SessionState
import com.droiddeck.launcher.core.ArchivePaths
import java.io.BufferedInputStream
@@ -101,7 +101,7 @@ object DeckyManager {
}
fun releaseChannels(context: Context): ReleaseChannels {
val body = Downloader.downloadString(RELEASES) ?: return ReleaseChannels(emptyList(), emptyList())
val body = ComponentDownloads.downloadString(RELEASES) ?: return ReleaseChannels(emptyList(), emptyList())
return try {
val array = JSONArray(body)
val stable = mutableListOf<Release>()
@@ -141,11 +141,11 @@ object DeckyManager {
target.parentFile?.mkdirs()
val temp = File(target.parentFile, "PluginLoader.download")
val downloading = context.getString(R.string.user_apps_downloading, release.tag)
val ok = Downloader.downloadFile(release.url, temp, true, release.digest?.substringAfter(':')) { f -> progress(downloading, if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }
val ok = ComponentDownloads.downloadFile(release.url, temp, true, release.digest?.substringAfter(':')) { f -> progress(downloading, if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }
if (!ok) { temp.delete(); return context.getString(R.string.deckymgr_download_failed) }
if (temp.length() != release.size) { temp.delete(); return context.getString(R.string.deckymgr_size_mismatch) }
val expected = release.digest?.substringAfter(':') ?: release.shaUrl?.let { checksumUrl ->
val checksumBody = Downloader.downloadDirect(checksumUrl) ?: run { temp.delete(); return context.getString(R.string.deckymgr_checksum_download_failed) }
val checksumBody = ComponentDownloads.downloadDirect(checksumUrl) ?: run { temp.delete(); return context.getString(R.string.deckymgr_checksum_download_failed) }
Regex("(?i)\\b[0-9a-f]{64}\\b").find(checksumBody)?.value
} ?: run { temp.delete(); return context.getString(R.string.deckymgr_checksum_missing) }
if (!expected.matches(Regex("(?i)[0-9a-f]{64}"))) { temp.delete(); return context.getString(R.string.deckymgr_checksum_invalid) }
@@ -270,7 +270,7 @@ object DeckyManager {
downloadingBinary = true
progress(downloading, -1)
val downloaded = try {
Downloader.downloadFile(url, temp, true, hash) { fraction ->
ComponentDownloads.downloadFile(url, temp, true, hash) { fraction ->
progress(downloading, if (fraction < 0) -1 else (fraction * 100).toInt().coerceIn(0, 100))
}
} finally {
@@ -6,6 +6,7 @@ import android.util.Log
import com.droiddeck.launcher.BuildConfig
import com.droiddeck.launcher.R
import com.droiddeck.launcher.core.Downloader
import com.droiddeck.launcher.core.ComponentDownloads
import com.droiddeck.launcher.core.FileUtils
import org.json.JSONObject
import java.io.File
@@ -22,8 +23,10 @@ object DesktopCatalog {
val icon: String, val category: String,
)
internal fun offered(entry: Entry): Boolean = !BuildConfig.PLAY_DISTRIBUTION || entry.id == DESKTOP_ID || entry.id == "desktop"
fun fetch(url: String = CATALOG_URL, cancelled: java.util.function.BooleanSupplier? = null): List<Entry>? {
val body = Downloader.downloadString(url, cancelled) ?: return null
val body = ComponentDownloads.downloadString(url, cancelled) ?: return null
return try {
val json = JSONObject(body)
val arr = json.getJSONArray("packages")
@@ -39,7 +42,7 @@ object DesktopCatalog {
file.optLong("size", 0L), o.optString("notes", ""),
o.optString("icon", "applications-games"), o.optString("category", "Game"),
)
}
}.filter(::offered)
} catch (e: Exception) {
Log.w(TAG, "catalog: $e"); null
}
@@ -51,7 +54,7 @@ object DesktopCatalog {
private fun journal(root: File, id: String) = File(root, ".droiddeck-pkg-$id$JOURNAL_SUFFIX")
fun interrupted(context: Context): List<Entry> = interrupted(LinuxRuntime.rootDir(context))
fun interrupted(context: Context): List<Entry> = interrupted(LinuxRuntime.rootDir(context)).filter(::offered)
fun interrupted(root: File): List<Entry> =
root.listFiles { f -> f.name.startsWith(".droiddeck-pkg-") && f.name.endsWith(JOURNAL_SUFFIX) }.orEmpty()
@@ -63,7 +66,7 @@ object DesktopCatalog {
private fun downloadFile(context: Context, entry: Entry) = File(context.cacheDir, "pkg-${entry.id}-${entry.sha256.take(12)}.download")
fun pruneDownloads(context: Context) = synchronized(installLock) {
val keep = interrupted(context).map { downloadFile(context, it).name }.toSet()
val keep = interrupted(LinuxRuntime.rootDir(context)).map { downloadFile(context, it).name }.toSet()
context.cacheDir.listFiles { f -> f.name.startsWith("pkg-") && f.name.removeSuffix(".resume.properties").endsWith(".download") }
?.filter { it.name.removeSuffix(".resume.properties") !in keep }?.forEach { it.delete() }
}
@@ -107,9 +110,13 @@ object DesktopCatalog {
fun installed(context: Context, id: String): String? =
FileUtils.readString(marker(context, id))?.trim()?.takeIf { it.isNotEmpty() }
fun protonSeedNeeded(context: Context): Boolean =
installed(context, PROTON_SEED_ID) == null &&
fun protonSeedNeeded(context: Context): Boolean {
if (BuildConfig.PLAY_DISTRIBUTION) return com.droiddeck.launcher.session.ProtonExtras.installed(
context, com.droiddeck.launcher.session.ProtonExtras.tools.single(),
) == null
return installed(context, PROTON_SEED_ID) == null &&
!File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps/appmanifest_4427310.acf").isFile
}
const val DESKTOP_ID = "desktop-kde"
const val EMULATORS_ID = "emulators"
@@ -170,8 +177,11 @@ object DesktopCatalog {
installLocked(context, entry, cancelled, listener)
}
private fun installLocked(context: Context, entry: Entry, cancelled: Downloader.Cancellation?,
private fun installLocked(context: Context, requested: Entry, cancelled: Downloader.Cancellation?,
listener: LinuxRuntimeInstaller.ProgressListener?): String? {
val current = if (BuildConfig.PLAY_DISTRIBUTION && requested.id == DESKTOP_ID) desktopEntry(context) else null
val entry = current?.takeIf { it.version == requested.version } ?: requested
if (!offered(entry)) return context.getString(R.string.deskpkg_install_failed)
val root = LinuxRuntime.rootDir(context)
if (!root.isDirectory) return context.getString(R.string.deskpkg_runtime_missing)
if (entry.id == EMULATORS_ID && desktopFromPack(root)) return installEmulators(context, entry, cancelled, listener)
@@ -195,16 +205,16 @@ object DesktopCatalog {
report.onProgress(LinuxRuntimeInstaller.Step.DOWNLOADING, downloading, if (f < 0) -1 else Math.round(f * 100f))
}
val ok = if (cancelled == null) {
Downloader.downloadFile(entry.url, download, true, entry.sha256, progress)
ComponentDownloads.downloadFile(entry.url, download, true, entry.sha256, progress)
} else {
Downloader.downloadFile(entry.url, download, true, entry.sha256, cancelled, progress)
ComponentDownloads.downloadFile(entry.url, download, true, entry.sha256, cancelled, progress)
}
if (!ok || cancelled?.getAsBoolean() == true) return context.getString(R.string.user_apps_download_failed)
report.onProgress(LinuxRuntimeInstaller.Step.VERIFYING, context.getString(R.string.rtinst_verifying), -1)
val actual = Hashes.sha256(download)
if (!entry.sha256.equals(actual, ignoreCase = true)) return context.getString(R.string.deskpkg_checksum_mismatch)
}
if (cancelled?.getAsBoolean() == true) return context.getString(R.string.user_apps_download_failed)
if (Thread.currentThread().isInterrupted || cancelled?.getAsBoolean() == true) return context.getString(R.string.user_apps_download_failed)
report.onProgress(context.getString(R.string.deskpkg_installing, entry.name), -1)
when (entry.kind) {
"appimage" -> {
@@ -10,6 +10,7 @@ import com.droiddeck.launcher.session.SessionPaths
import com.droiddeck.launcher.session.SessionPrefs
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import java.util.function.BooleanSupplier
object GuestCommand {
fun run(context: Context, argv: List<String>, fakeRoot: Boolean = false, logName: String? = null,
@@ -22,14 +23,19 @@ object GuestCommand {
}
log?.println("== ${java.util.Date()} ${argv.joinToString(" ")}")
try {
return runLogged(context, argv, fakeRoot, linkDir, extraBinds, fastPath, cancelled) { line -> log?.println(line); log?.flush(); onLine(line) }
return runLogged(context, argv, fakeRoot, linkDir, extraBinds, fastPath,
cancelled?.let { flag -> BooleanSupplier { flag.get() } }) { line -> log?.println(line); log?.flush(); onLine(line) }
} finally {
log?.close()
}
}
fun runCancellable(context: Context, argv: List<String>, cancelled: BooleanSupplier?, onLine: (String) -> Unit): Int =
runLogged(context, argv, false, null, null, false, cancelled, onLine)
private fun runLogged(context: Context, argv: List<String>, fakeRoot: Boolean, linkDir: File?, extraBinds: List<String>?,
fastPath: Boolean, cancelled: AtomicBoolean?, onLine: (String) -> Unit): Int {
fastPath: Boolean, cancelled: BooleanSupplier?, onLine: (String) -> Unit): Int {
if (cancelled?.asBoolean == true || Thread.currentThread().isInterrupted) return 130
val root = LinuxRuntime.rootDir(context)
LinuxRuntime.writeAccounts(context)
SessionFiles.stage(context, root)
@@ -65,15 +71,17 @@ object GuestCommand {
fastPathKey?.let { ProotFastPath.hostEnv(it).let { (k, v) -> put(k, v) } }
LinuxRuntime.prootLibraryPath(context).takeIf { it.isNotEmpty() }?.let { put("LD_LIBRARY_PATH", it) }
}
if (cancelled?.get() == true) return 130
if (cancelled?.asBoolean == true) return 130
val process = builder.start()
val pid = HostProcess.pidOf(process)
OrphanReaper.keep(pid)
val watcher = cancelled?.let { flag ->
val caller = Thread.currentThread()
val watcher = run {
val flag = cancelled
Thread({
try {
while (process.isAlive && !flag.get()) Thread.sleep(200)
if (flag.get()) {
while (process.isAlive && flag?.asBoolean != true && !caller.isInterrupted) Thread.sleep(200)
if (flag?.asBoolean == true || caller.isInterrupted) {
if (pid > 0) {
runCatching { android.system.Os.kill(pid, android.system.OsConstants.SIGQUIT) }
} else {
@@ -186,7 +186,7 @@ class LinuxNetworkLinkComponent(
val state = JSONObject().apply {
put("transport", transport)
put("interface", properties?.interfaceName ?: "android")
put("mtu", properties?.mtu?.takeIf { it > 0 } ?: DEFAULT_MTU)
put("mtu", (if (android.os.Build.VERSION.SDK_INT >= 29) properties?.mtu else null)?.takeIf { it > 0 } ?: DEFAULT_MTU)
put("connected", properties != null && capabilities != null)
put("validated", capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true)
put("captivePortal", capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL) == true)
@@ -1,6 +1,8 @@
package com.droiddeck.launcher.runtime;
import com.droiddeck.launcher.core.AddonPack;
import com.droiddeck.launcher.BuildConfig;
import com.droiddeck.launcher.core.ArchivePaths;
import android.content.Context;
import android.os.Process;
import android.system.ErrnoException;
@@ -65,6 +67,7 @@ public final class LinuxRuntime {
private static final String HOST_DIR = "opt/android-host";
public static final String PROOT_PACK = "add-on pack";
public static final String PROOT_BUNDLED = "APK";
public static final String PROOT_ROOTFS = "rootfs fallback";
public static final String PROOT_MISSING = "missing";
@@ -80,17 +83,31 @@ public final class LinuxRuntime {
return rootfsProot(context).isFile() && rootfsLoader(context).isFile();
}
private static File bundledProot(Context context, String name) {
return new File(context.getApplicationInfo().nativeLibraryDir, name);
}
private static boolean bundledProotReady(Context context) {
return context.getApplicationInfo().nativeLibraryDir != null &&
bundledProot(context, "libproot.so").isFile() && bundledProot(context, "libproot-loader.so").isFile();
}
public static boolean patchedProot(Context context) {
return AddonPack.PROOT.installed(context);
if (BuildConfig.PLAY_DISTRIBUTION) return bundledProotReady(context);
return bundledProotReady(context) || AddonPack.PROOT.installed(context);
}
public static File prootBinary(Context context) {
if (BuildConfig.PLAY_DISTRIBUTION) return bundledProot(context, "libproot.so");
if (bundledProotReady(context)) return bundledProot(context, "libproot.so");
if (AddonPack.PROOT.installed(context)) return AddonPack.PROOT.file(context, "libproot.so");
if (SessionPrefs.allowStockProot(context)) return rootfsProot(context);
return AddonPack.PROOT.file(context, "libproot.so");
}
public static File prootLoader(Context context) {
if (BuildConfig.PLAY_DISTRIBUTION) return bundledProot(context, "libproot-loader.so");
if (bundledProotReady(context)) return bundledProot(context, "libproot-loader.so");
if (AddonPack.PROOT.installed(context)) return AddonPack.PROOT.file(context, "libproot-loader.so");
if (SessionPrefs.allowStockProot(context)) return rootfsLoader(context);
return AddonPack.PROOT.file(context, "libproot-loader.so");
@@ -99,6 +116,7 @@ public final class LinuxRuntime {
public static String prootSource(Context context) {
File bin = prootBinary(context);
if (!bin.isFile()) return PROOT_MISSING;
if (bundledProotReady(context)) return PROOT_BUNDLED;
if (AddonPack.PROOT.installed(context)
&& bin.getPath().equals(AddonPack.PROOT.file(context, "libproot.so").getPath())) return PROOT_PACK;
return PROOT_ROOTFS;
@@ -278,6 +296,7 @@ public final class LinuxRuntime {
}
bindAdrenoStats(cmd, extraBinds);
bindCpuTemps(cmd, root);
bindComponentBundles(context, root, cmd);
if (extraBinds != null) {
for (String spec : extraBinds) bind(cmd, spec);
}
@@ -286,6 +305,32 @@ public final class LinuxRuntime {
return specs;
}
public static void bindComponentBundles(Context context, File root, List<String> command) {
if (!BuildConfig.PLAY_DISTRIBUTION) return;
java.util.LinkedHashSet<File> directories = new java.util.LinkedHashSet<>();
directories.add(new File(context.getApplicationInfo().sourceDir).getParentFile());
String[] splits;
try {
splits = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).splitSourceDirs;
} catch (android.content.pm.PackageManager.NameNotFoundException error) {
throw new IllegalStateException(error);
}
if (splits != null) for (String split : splits) directories.add(new File(split).getParentFile());
int index = 0;
for (File directory : directories) {
File target;
try {
target = ArchivePaths.inside(root, "opt/droiddeck-bundles/" + index++);
} catch (IOException error) {
throw new IllegalStateException("Installed component bundle mount is invalid", error);
}
if (directory == null || !directory.isDirectory() || target == null || !target.isDirectory() && !target.mkdirs()) {
throw new IllegalStateException("Installed component bundles cannot be mounted");
}
bind(command, directory.getPath() + ":/" + root.toPath().relativize(target.toPath()).toString());
}
}
private static void bindPowerVrGpuNode(Context context, List<String> cmd) {
if (new File("/dev/dri").list() != null) return;
String node = SystemVulkanDriver.renderNodePath();
@@ -8,7 +8,7 @@ import android.util.Log;
import com.droiddeck.launcher.R;
import com.droiddeck.launcher.session.SessionPrefs;
import com.droiddeck.launcher.core.ArchivePaths;
import com.droiddeck.launcher.core.Downloader;
import com.droiddeck.launcher.core.ComponentDownloads;
import com.droiddeck.launcher.core.Hashes;
import com.droiddeck.launcher.core.FileUtils;
@@ -23,6 +23,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@@ -70,6 +71,8 @@ public final class LinuxRuntimeInstaller {
volatile Step step = Step.OTHER;
volatile int percent = -1;
volatile boolean ok;
volatile Thread worker;
volatile boolean cancelled;
final boolean removal;
boolean maintenance;
Job(boolean removal, String stage) { this.removal = removal; this.stage = stage; }
@@ -101,7 +104,7 @@ public final class LinuxRuntimeInstaller {
}
public static Release fetchRelease() {
String body = Downloader.downloadString(catalogUrl());
String body = ComponentDownloads.downloadString(catalogUrl());
if (body == null || body.isEmpty()) return null;
try {
JSONObject json = new JSONObject(body);
@@ -126,6 +129,7 @@ public final class LinuxRuntimeInstaller {
owner = running == null;
if (owner) { running = new Job(false, context.getString(R.string.user_apps_starting)); removalError = null; }
job = running;
if (owner) job.worker = Thread.currentThread();
if (listener != null) job.listeners.add(listener);
}
if (!owner) return join(job, listener);
@@ -187,6 +191,15 @@ public final class LinuxRuntimeInstaller {
synchronized (JOB_LOCK) { return running != null; }
}
public static void cancelActiveOperation() {
synchronized (JOB_LOCK) {
if (running != null) {
running.cancelled = true;
if (running.worker != null) running.worker.interrupt();
}
}
}
public static boolean isRemoving() {
synchronized (JOB_LOCK) { return running != null && running.removal; }
}
@@ -216,8 +229,11 @@ public final class LinuxRuntimeInstaller {
private Maintenance(Job job) { this.job = job; }
public boolean run(ProgressListener listener, MaintenanceWork work) {
if (!started.compareAndSet(false, true)) return join(job, listener);
job.worker = Thread.currentThread();
if (job.cancelled) job.worker.interrupt();
if (listener != null) job.listeners.add(listener);
try {
requireNotInterrupted();
removalError = work.run((stage, percent) -> {
job.stage = stage;
job.percent = percent;
@@ -298,7 +314,7 @@ public final class LinuxRuntimeInstaller {
&& !SessionPrefs.allowStockProot(context))
throw new IOException("Cannot download proot");
if (!AddonPack.DIRECTAUDIO.await(context)) throw new IOException("Cannot download DirectAudio");
boolean ok = Downloader.downloadFile(release.url, archive, true, release.sha256, (fraction) -> {
boolean ok = ComponentDownloads.downloadFile(release.url, archive, true, release.sha256, (fraction) -> {
if (listener != null) {
listener.onProgress(Step.DOWNLOADING, downloading,
fraction < 0 ? -1 : Math.round(fraction * 100f));
@@ -309,6 +325,7 @@ public final class LinuxRuntimeInstaller {
keepPartial = true;
return false;
}
requireNotInterrupted();
if (listener != null) listener.onProgress(Step.VERIFYING, context.getString(R.string.rtinst_verifying), -1);
String actual = Hashes.sha256(archive);
@@ -327,6 +344,12 @@ public final class LinuxRuntimeInstaller {
RuntimeFileTree.delete(staging, null);
return false;
}
try {
requireNotInterrupted();
} catch (InterruptedIOException e) {
discard(staging, null);
throw e;
}
FileUtils.writeString(new File(staging, VERSION_FILE), release.version);
if (!discard(old, null)) {
@@ -339,6 +362,11 @@ public final class LinuxRuntimeInstaller {
RuntimeFileTree.delete(staging, null);
return false;
}
if (Thread.currentThread().isInterrupted()) {
if (old.isDirectory()) old.renameTo(root);
RuntimeFileTree.delete(staging, null);
return false;
}
File carryMarker = new File(staging, CARRY_MARKER);
java.util.List<String> encodedPaths = new java.util.ArrayList<>();
for (String rel : carryPaths) encodedPaths.add(java.net.URLEncoder.encode(rel, "UTF-8"));
@@ -366,7 +394,7 @@ public final class LinuxRuntimeInstaller {
return false;
}
if (!staging.renameTo(root)) {
if (Thread.currentThread().isInterrupted() || !staging.renameTo(root)) {
if (old.isDirectory()) {
try {
RuntimeFileTree.carryBack(staging, old, carried);
@@ -489,6 +517,8 @@ public final class LinuxRuntimeInstaller {
public boolean run(ProgressListener listener) {
if (!started.compareAndSet(false, true)) return join(job, listener);
job.worker = Thread.currentThread();
if (job.cancelled) job.worker.interrupt();
if (listener != null) job.listeners.add(listener);
try {
requireStopped();
@@ -558,6 +588,7 @@ public final class LinuxRuntimeInstaller {
TarArchiveInputStream tar = new TarArchiveInputStream(in)) {
TarArchiveEntry entry;
while ((entry = tar.getNextTarEntry()) != null) {
requireNotInterrupted();
File file = ArchivePaths.inside(destination, entry.getName());
if (file == null) {
Log.w(TAG, "skipping entry outside the rootfs: " + entry.getName());
@@ -596,7 +627,10 @@ public final class LinuxRuntimeInstaller {
try (OutputStream out = new FileOutputStream(part)) {
byte[] buffer = new byte[1 << 16];
int read;
while ((read = tar.read(buffer)) != -1) out.write(buffer, 0, read);
while ((read = tar.read(buffer)) != -1) {
requireNotInterrupted();
out.write(buffer, 0, read);
}
}
if ((entry.getMode() & 0111) != 0) part.setExecutable(true, false);
place(part, file);
@@ -626,6 +660,10 @@ public final class LinuxRuntimeInstaller {
}
}
private static void requireNotInterrupted() throws InterruptedIOException {
if (Thread.currentThread().isInterrupted()) throw new InterruptedIOException("Runtime installation was interrupted");
}
static File hardLinkTarget(File destination, String name) throws IOException {
java.nio.file.Path path = java.nio.file.Paths.get(name).normalize();
java.nio.file.Path last = path.getFileName();
@@ -13,7 +13,7 @@ object ProotFastPath {
const val MAX_BINDS = 256
fun allows(preference: Boolean, noSeccomp: Boolean, source: String): Boolean =
preference && !noSeccomp && source == LinuxRuntime.PROOT_PACK
preference && !noSeccomp && (source == LinuxRuntime.PROOT_PACK || source == LinuxRuntime.PROOT_BUNDLED)
fun enabled(context: Context): Boolean {
val preference = SessionPrefs.prootFastPath(context)
@@ -145,9 +145,9 @@ object RootSpawner {
else runCatching { process.outputStream.close() }
}
if (streams[1].redirect == null) pumps.add(pump(process.inputStream,
streams[1].fd?.let { FileOutputStream(it) } ?: OutputStream.nullOutputStream(), "root-$id-out"))
streams[1].fd?.let { FileOutputStream(it) } ?: FileOutputStream("/dev/null"), "root-$id-out"))
if (streams[2].redirect == null) pumps.add(pump(process.errorStream,
streams[2].fd?.let { FileOutputStream(it) } ?: OutputStream.nullOutputStream(), "root-$id-err"))
streams[2].fd?.let { FileOutputStream(it) } ?: FileOutputStream("/dev/null"), "root-$id-err"))
Thread({
try {
while (true) {
@@ -13,44 +13,67 @@ import com.droiddeck.launcher.FrontEndActivity
import com.droiddeck.launcher.R
class RuntimeInstallService : Service() {
@Volatile private var activeWork: Thread? = null
@Volatile private var generation = 0L
override fun attachBaseContext(newBase: android.content.Context) =
super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(newBase))
override fun onBind(intent: Intent?): IBinder? = null
override fun onTimeout(startId: Int, fgsType: Int) {
generation++
LinuxRuntimeInstaller.cancelActiveOperation()
activeWork?.interrupt()
packageWorkers.keys.forEach { it.interrupt() }
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_PACKAGE) return holdPackage(startId)
val removal = intent?.action == ACTION_REMOVE
val release = intent?.let(::releaseFrom)
startForeground(NOTIFICATION_ID, notification(getString(if (LinuxRuntimeInstaller.isMaintaining()) R.string.desktop_removing else if (removal) R.string.rtinst_removing else R.string.user_apps_starting), -1))
if (!com.droiddeck.launcher.core.ForegroundServices.start(this, NOTIFICATION_ID,
notification(getString(if (LinuxRuntimeInstaller.isMaintaining()) R.string.desktop_removing else if (removal) R.string.rtinst_removing else R.string.user_apps_starting), -1))) {
stopSelf(startId)
return START_NOT_STICKY
}
if (release == null && !removal) {
stopSelf(startId)
return START_NOT_STICKY
}
Thread({
val workGeneration = ++generation
activeWork = Thread({
val manager = getSystemService(NotificationManager::class.java)
var shown = ""
val progress = LinuxRuntimeInstaller.ProgressListener { stage, percent ->
if ("$stage:$percent" != shown) {
if (workGeneration == generation && "$stage:$percent" != shown) {
shown = "$stage:$percent"
manager?.notify(NOTIFICATION_ID, notification(stage, percent))
}
}
if (removal) LinuxRuntimeInstaller.attach(progress)
else LinuxRuntimeInstaller.install(applicationContext, release!!, progress)
@Suppress("DEPRECATION")
stopForeground(true)
stopSelf(startId)
}, "runtime-install").start()
if (workGeneration == generation) {
@Suppress("DEPRECATION")
stopForeground(true)
stopSelf(startId)
}
}, "runtime-install").also { it.start() }
return START_NOT_STICKY
}
private fun holdPackage(startId: Int): Int {
startForeground(NOTIFICATION_ID, notification(packageStage, packagePercent, packageTitle))
if (!com.droiddeck.launcher.core.ForegroundServices.start(this, NOTIFICATION_ID, notification(packageStage, packagePercent, packageTitle))) {
packageWorkers.keys.forEach { it.interrupt() }
stopSelf(startId)
return START_NOT_STICKY
}
val workGeneration = ++generation
Thread({
val manager = getSystemService(NotificationManager::class.java)
var shown = ""
while (packageHolds.get() > 0) {
while (packageHolds.get() > 0 && workGeneration == generation) {
val now = "$packageStage:$packagePercent"
if (now != shown) {
shown = now
@@ -58,9 +81,11 @@ class RuntimeInstallService : Service() {
}
Thread.sleep(500)
}
@Suppress("DEPRECATION")
stopForeground(true)
stopSelf(startId)
if (workGeneration == generation) {
@Suppress("DEPRECATION")
stopForeground(true)
stopSelf(startId)
}
}, "package-install").start()
return START_NOT_STICKY
}
@@ -95,6 +120,7 @@ class RuntimeInstallService : Service() {
private const val ACTION_REMOVE = "com.droiddeck.launcher.REMOVE_RUNTIME"
private const val ACTION_PACKAGE = "com.droiddeck.launcher.INSTALL_PACKAGE"
private val packageHolds = java.util.concurrent.atomic.AtomicInteger()
private val packageWorkers = java.util.concurrent.ConcurrentHashMap<Thread, java.util.concurrent.atomic.AtomicInteger>()
@Volatile private var packageTitle = ""
@Volatile private var packageStage = ""
@Volatile private var packagePercent = -1
@@ -122,11 +148,18 @@ class RuntimeInstallService : Service() {
packageStage = title
packagePercent = -1
packageHolds.incrementAndGet()
val worker = Thread.currentThread()
packageWorkers.compute(worker) { _, count -> (count ?: java.util.concurrent.atomic.AtomicInteger()).also { it.incrementAndGet() } }
val intent = Intent(context, RuntimeInstallService::class.java).setAction(ACTION_PACKAGE)
runCatching { if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent) }
.onFailure { android.util.Log.w("RuntimeInstallService", "package install runs without a foreground service: $it") }
val released = java.util.concurrent.atomic.AtomicBoolean()
return { if (released.compareAndSet(false, true)) packageHolds.decrementAndGet() }
return {
if (released.compareAndSet(false, true)) {
packageHolds.decrementAndGet()
packageWorkers.computeIfPresent(worker) { _, count -> if (count.decrementAndGet() == 0) null else count }
}
}
}
fun packageProgress(stage: String, percent: Int) {
@@ -4,7 +4,7 @@ import com.droiddeck.launcher.core.Hashes
import com.droiddeck.launcher.R
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.core.Downloader
import com.droiddeck.launcher.core.ComponentDownloads
import com.droiddeck.launcher.core.FileUtils
import com.droiddeck.launcher.core.GameEnvironment
import com.droiddeck.launcher.gpu.AppVulkan
@@ -89,7 +89,7 @@ object ComponentsManager {
private fun packagesDir(context: Context) = File(dataDir(context), "packages").apply { mkdirs() }
private fun originalsDir(context: Context) = File(dataDir(context), "originals").apply { mkdirs() }
private fun stateFile(context: Context) = File(dataDir(context), "state.json")
private fun catalogFile(context: Context) = File(dataDir(context), "catalog.json")
private fun catalogFile(context: Context) = File(dataDir(context), ComponentDownloads.catalogName("catalog.json"))
private const val LAUNCH_DIR = "root/.local/share/droiddeck-components"
private const val LEGACY_LAUNCH_DIR = "root/.local/share/bannerlator-components"
private fun launchDir(context: Context): File {
@@ -289,6 +289,7 @@ object ComponentsManager {
for ((base, valve) in bases) {
val entries = base.listFiles()?.sortedBy { it.name } ?: continue
for (dir in entries) {
if (com.droiddeck.launcher.BuildConfig.PLAY_DISTRIBUTION && (valve || !dir.name.startsWith("DroidDeck-Proton"))) continue
if (!File(dir, WINE).isDirectory || !File(dir, "proton").isFile) continue
val real = runCatching { dir.canonicalPath }.getOrDefault(dir.path)
if (found.containsKey(real)) continue
@@ -968,7 +969,7 @@ object ComponentsManager {
if (!force) return Catalog(emptyList(), 0)
val items = JSONArray()
for ((repo, tag) in SOURCES) {
val body = Downloader.downloadString("https://api.github.com/repos/$repo/releases/tags/$tag") ?: continue
val body = ComponentDownloads.downloadString("https://api.github.com/repos/$repo/releases/tags/$tag") ?: continue
val assets = runCatching { JSONObject(body).optJSONArray("assets") }.getOrNull() ?: continue
for (i in 0 until assets.length()) {
val a = assets.getJSONObject(i)
@@ -1183,7 +1184,7 @@ object ComponentsManager {
require(Hashes.isGithubSha256(item.digest)) { context.getString(R.string.cmgr_list_predates_checksums) }
val dest = File(packagesDir(context), safeName(item.file))
val part = File(dest.parentFile, dest.name + ".part")
check(Downloader.downloadFile(item.url, part, true, item.digest.substringAfter(':')) { f -> progress(if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }) {
check(ComponentDownloads.downloadFile(item.url, part, true, item.digest.substringAfter(':')) { f -> progress(if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }) {
context.getString(if (item.release == DD_NIGHTLY) R.string.cmgr_nightly_replaced else R.string.user_apps_download_failed)
}
if (!Hashes.sha256(part).equals(item.digest.substringAfter(':'), true)) { part.delete(); error(context.getString(R.string.cmgr_checksum_mismatch)) }
@@ -513,7 +513,8 @@ internal object EnhancedSessionLogShare {
private fun openDescriptor(path: String): java.io.FileDescriptor? = runCatching {
Os.open(
path,
OsConstants.O_RDONLY or OsConstants.O_CLOEXEC or OsConstants.O_NOFOLLOW or OsConstants.O_NONBLOCK,
OsConstants.O_RDONLY or OsConstants.O_NOFOLLOW or OsConstants.O_NONBLOCK or
if (android.os.Build.VERSION.SDK_INT >= 27) OsConstants.O_CLOEXEC else 0,
0,
)
}.getOrNull()
@@ -2,6 +2,8 @@ package com.droiddeck.launcher.session
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.BuildConfig
import com.droiddeck.launcher.core.ComponentDownloads
import com.droiddeck.launcher.core.DownloadSource
import com.droiddeck.launcher.core.FileUtils
import com.droiddeck.launcher.core.Hashes
@@ -861,8 +863,17 @@ object EsyncPacks {
return null
}
private fun fetch(url: String, limit: Long, out: OutputStream, progress: ((Long) -> Unit)?): Boolean =
fetcher(url, limit, out, progress)
private fun fetch(url: String, limit: Long, out: OutputStream, progress: ((Long) -> Unit)?): Boolean {
if (!BuildConfig.PLAY_DISTRIBUTION) return fetcher(url, limit, out, progress)
return try {
val bytes = ComponentDownloads.copyBundled(url, out, limit, null)
progress?.invoke(bytes)
true
} catch (error: IOException) {
Log.w(TAG, "Bundled synchronization component unavailable: $url", error)
false
}
}
private fun httpFetch(url: String, limit: Long, out: OutputStream, progress: ((Long) -> Unit)?): Boolean {
var connection: HttpURLConnection? = null
@@ -3,7 +3,7 @@ package com.droiddeck.launcher.session
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.core.DownloadSource
import com.droiddeck.launcher.core.Downloader
import com.droiddeck.launcher.core.ComponentDownloads
import org.json.JSONObject
import java.io.File
@@ -20,7 +20,7 @@ object FexCatalog {
@Volatile private var refreshedAt = 0L
private fun file(context: Context) = File(context.filesDir, "fex-catalog.json")
private fun file(context: Context) = File(context.filesDir, ComponentDownloads.catalogName("fex-catalog.json"))
internal fun parse(text: String): Build? = runCatching {
val o = JSONObject(text)
@@ -42,7 +42,7 @@ object FexCatalog {
fun refresh(context: Context, now: Long = System.currentTimeMillis()): Build? {
if (now - refreshedAt < REFRESH_MS) return cached(context)
refreshedAt = now
val text = Downloader.downloadDirect(CATALOG_URL) ?: return cached(context)
val text = ComponentDownloads.downloadDirect(CATALOG_URL) ?: return cached(context)
val build = parse(text) ?: run {
Log.w(TAG, "fex.json not usable; keeping the last one")
return cached(context)
@@ -47,6 +47,7 @@ object GameThreadHints {
worker = null
}
@androidx.annotation.RequiresApi(31)
private fun run(hm: PerformanceHintManager, pm: PowerManager) {
val scanner = Scanner()
var session: PerformanceHintManager.Session? = null
@@ -88,6 +89,7 @@ object GameThreadHints {
}
}
@androidx.annotation.RequiresApi(31)
private fun update(hm: PerformanceHintManager, session: PerformanceHintManager.Session?, tids: Set<Int>): PerformanceHintManager.Session? {
session?.close()
if (tids.isEmpty()) return null
@@ -2,13 +2,15 @@ package com.droiddeck.launcher.session
import com.droiddeck.launcher.core.Hashes
import com.droiddeck.launcher.R
import com.droiddeck.launcher.BuildConfig
import android.content.Context
import android.os.StatFs
import android.util.Log
import com.droiddeck.launcher.core.Downloader
import com.droiddeck.launcher.core.ComponentDownloads
import com.droiddeck.launcher.core.FileUtils
import com.droiddeck.launcher.runtime.LinuxRuntime
import java.io.File
import java.util.function.BooleanSupplier
import org.json.JSONArray
import org.json.JSONObject
@@ -30,17 +32,19 @@ object ProtonExtras {
const val OWN = "droiddeck"
val tools = listOf(
internal val sources = listOf(
Tool("ge", "GE-Proton", "GE-Proton", "GloriousEggroll/proton-ge-custom", Regex("aarch64\\.tar\\.(gz|xz)$")),
Tool("cachyos", "proton-cachyos", "proton-cachyos", "CachyOS/proton-cachyos", Regex("arm64\\.tar\\.(gz|xz)$")),
Tool("droiddeck", "DroidDeck-Proton", "DroidDeck-Proton", "Droid-Deck/DroidDeck-Proton", Regex("aarch64\\.tar\\.(gz|xz)$")),
Tool("droiddeck", "DroidDeck-Proton", "DroidDeck-Proton", "Droid-Deck/DroidDeck-Proton", Regex(if (BuildConfig.PLAY_DISTRIBUTION) "aarch64\\.tar\\.(gz|xz|zst)$" else "aarch64\\.tar\\.(gz|xz)$")),
)
val tools = sources.filter { !BuildConfig.PLAY_DISTRIBUTION || it.id == OWN }
private fun home(context: Context) = File(LinuxRuntime.rootDir(context), "root")
private fun requests(context: Context) = File(home(context), ".bl-proton-extra")
private fun toolsDir(context: Context) = File(home(context), ".local/share/Steam/compatibilitytools.d")
private fun releasesFile(context: Context) = File(context.filesDir, "proton-extras.json")
private fun latestFile(context: Context) = File(context.filesDir, "proton-latest.json")
private fun latestFile(context: Context) = File(context.filesDir, ComponentDownloads.catalogName("proton-latest.json"))
fun installed(context: Context, tool: Tool): String? = installedIn(toolsDir(context), tool).lastOrNull()
@@ -200,13 +204,17 @@ object ProtonExtras {
}
@Synchronized
fun install(context: Context, tool: Tool, onProgress: (String, Int) -> Unit): String? {
fun install(context: Context, tool: Tool, onProgress: (String, Int) -> Unit): String? = install(context, tool, null, onProgress)
@Synchronized
fun install(context: Context, tool: Tool, cancelled: BooleanSupplier?, onProgress: (String, Int) -> Unit): String? {
if (installInProgress) return context.getString(R.string.pextra_busy)
installInProgress = true
return try { installNow(context, tool, onProgress) } finally { installInProgress = false }
return try { installNow(context, tool, cancelled, onProgress) } finally { installInProgress = false }
}
private fun installNow(context: Context, tool: Tool, onProgress: (String, Int) -> Unit): String? {
private fun installNow(context: Context, tool: Tool, cancelled: BooleanSupplier?, onProgress: (String, Int) -> Unit): String? {
if (cancelled?.asBoolean == true || Thread.currentThread().isInterrupted) return context.getString(R.string.pextra_interrupted)
if (SessionState.running) return context.getString(R.string.pextra_stop_session)
if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required)
@@ -223,10 +231,12 @@ object ProtonExtras {
onProgress(downloading, 0)
var downloaded = false
for (attempt in 0 until 5) {
downloaded = Downloader.downloadFile(asset.url, archive, true, asset.sha256) { fraction ->
if (cancelled?.asBoolean == true || Thread.currentThread().isInterrupted) return context.getString(R.string.pextra_interrupted)
downloaded = ComponentDownloads.downloadFile(asset.url, archive, true, asset.sha256, cancelled) { fraction ->
onProgress(downloading, if (fraction < 0) -1 else (fraction * 100f).toInt().coerceIn(0, 100))
}
if (downloaded) break
if (cancelled?.asBoolean == true) return context.getString(R.string.pextra_interrupted)
if (attempt < 4) {
onProgress(context.getString(R.string.pextra_retrying, attempt + 2, 5), -1)
try { Thread.sleep(3_000) } catch (_: InterruptedException) {
@@ -248,7 +258,7 @@ object ProtonExtras {
val verified = when {
asset.sha256 != null -> asset.sha256.equals(Hashes.sha256(archive), ignoreCase = true)
asset.sha512 != null -> {
val expected = Downloader.downloadDirect(asset.sha512)?.let { Regex("(?i)\\b[0-9a-f]{128}\\b").find(it)?.value }
val expected = ComponentDownloads.downloadDirect(asset.sha512)?.let { Regex("(?i)\\b[0-9a-f]{128}\\b").find(it)?.value }
?: return context.getString(R.string.pextra_checksum_unreadable)
expected.equals(Hashes.sha512(archive), ignoreCase = true)
}
@@ -263,38 +273,23 @@ object ProtonExtras {
}
if (SessionState.running) return context.getString(R.string.pextra_session_started)
if (cancelled?.asBoolean == true || Thread.currentThread().isInterrupted) return context.getString(R.string.pextra_interrupted)
val installing = context.getString(R.string.pextra_installing, tool.name)
onProgress(installing, -1)
return try {
val root = LinuxRuntime.rootDir(context)
LinuxRuntime.writeAccounts(context)
com.droiddeck.launcher.session.SessionFiles.stage(context, root)
val runtimeDir = File(context.filesDir, ".proton-install-rt").apply { mkdirs() }
val guest = listOf(
"/usr/bin/env", "-i", "HOME=/root", "USER=root", "PATH=/usr/local/bin:/usr/bin:/bin",
"LANG=C.UTF-8", "XDG_DATA_HOME=/root/.local/share",
"/usr/local/bin/droiddeck-proton-extra", "/root/.local/share/Steam", archive.absolutePath,
)
val command = LinuxRuntime.command(context, null, runtimeDir, null, guest)
val process = ProcessBuilder(command)
.directory(root)
.redirectErrorStream(true)
val hostEnv = process.environment()
hostEnv["PROOT_LOADER"] = LinuxRuntime.prootLoader(context).path
hostEnv["PROOT_TMP_DIR"] = LinuxRuntime.prootTempDir(context).path
LinuxRuntime.prootLibraryPath(context).takeIf { it.isNotEmpty() }?.let { hostEnv["LD_LIBRARY_PATH"] = it }
val child = process.start()
var placed: String? = null
child.inputStream.bufferedReader().useLines { lines ->
lines.forEach { line ->
Log.i(TAG, line)
line.substringAfter("droiddeck-proton-extra: installed ", "").takeIf { it.isNotEmpty() }?.let { placed = it }
if (line.contains("unpacking", ignoreCase = true) || line.contains("adopted", ignoreCase = true)) {
onProgress(installing, -1)
}
val status = com.droiddeck.launcher.runtime.GuestCommand.runCancellable(context, guest, cancelled) { line ->
Log.i(TAG, line)
line.substringAfter("droiddeck-proton-extra: installed ", "").takeIf { it.isNotEmpty() }?.let { placed = it }
if (line.contains("unpacking", ignoreCase = true) || line.contains("adopted", ignoreCase = true)) {
onProgress(installing, -1)
}
}
val status = child.waitFor()
if (cancelled?.asBoolean == true || Thread.currentThread().isInterrupted) return context.getString(R.string.pextra_interrupted)
if (status != 0) context.getString(R.string.pextra_install_failed, tool.name, status)
else {
archive.delete()
@@ -318,7 +313,7 @@ object ProtonExtras {
}
private fun findLatestAsset(tool: Tool): Asset? {
val releases = Downloader.downloadString(RELEASES.format(tool.repo)) ?: return null
val releases = ComponentDownloads.downloadString(RELEASES.format(tool.repo)) ?: return null
return try {
pickAsset(tool, JSONArray(releases))
} catch (e: Exception) {
@@ -341,7 +336,9 @@ object ProtonExtras {
}?.optString("browser_download_url")?.takeIf { it.startsWith("http") }
return Asset(
release.optString("tag_name"), name,
archive.optString("browser_download_url").takeIf { it.startsWith("http") } ?: continue, checksum,
archive.optString("browser_download_url").takeIf {
it.startsWith("http") || BuildConfig.PLAY_DISTRIBUTION && it.startsWith("droiddeck-bundle://")
} ?: continue, checksum,
archive.optLong("size", 0L),
Hashes.githubSha256(archive.optString("digest")),
)
@@ -47,6 +47,7 @@ object SessionFiles {
"usr/local/bin/droiddeck-steam-compat" to "usr/local/bin/droiddeck-steam-compat",
"usr/local/bin/droiddeck-steam-taskset" to "usr/local/libexec/droiddeck-steam/taskset",
"usr/local/bin/droiddeck-fex" to "usr/local/bin/droiddeck-fex",
"usr/local/lib/droiddeck/component_delivery.py" to "usr/local/lib/droiddeck/component_delivery.py",
"libblsession.so" to "usr/local/lib/libblsession.so",
"libfakeinput.so" to "usr/local/lib/libfakeinput.so",
"libblfastpath.so" to "usr/local/lib/libblfastpath.so",
@@ -591,6 +591,18 @@ object SessionPrefs {
prefs(context).edit().putBoolean("micAsked", true).apply()
}
fun allFilesAsked(context: Context): Boolean = prefs(context).getBoolean("allFilesAsked", false)
fun setAllFilesAsked(context: Context) {
prefs(context).edit().putBoolean("allFilesAsked", true).apply()
}
fun bluetoothAsked(context: Context): Boolean = prefs(context).getBoolean("bluetoothAsked", false)
fun setBluetoothAsked(context: Context) {
prefs(context).edit().putBoolean("bluetoothAsked", true).apply()
}
fun setMicEnabled(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("mic", on).apply()
}
@@ -1,5 +1,7 @@
package com.droiddeck.launcher.session
import com.droiddeck.launcher.BuildConfig
import com.droiddeck.launcher.core.AddonPack
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
@@ -329,7 +331,20 @@ class SessionService : Service() {
stopSelf(startId)
return START_NOT_STICKY
}
startForeground(NOTIFICATION_ID, buildNotification())
if (Build.VERSION.SDK_INT >= 34) {
var types = android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE or
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
if (SessionPrefs.micEnabled(this) && checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
types = types or android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
}
if (!com.droiddeck.launcher.core.ForegroundServices.start(this, NOTIFICATION_ID, buildNotification(), types)) {
stopSelf(startId)
return START_NOT_STICKY
}
} else if (!com.droiddeck.launcher.core.ForegroundServices.start(this, NOTIFICATION_ID, buildNotification())) {
stopSelf(startId)
return START_NOT_STICKY
}
if (SessionState.running) return START_NOT_STICKY
if (SessionState.stopRequested) {
SessionState.running = true
@@ -621,12 +636,19 @@ class SessionService : Service() {
.onFailure { Log.w(TAG, "Epic install directory setup failed", it) }
}
if (steamHere || noSteamGame) {
val added = com.droiddeck.launcher.frontend.AddedGames.scan(this)
val listing = com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added)
val cached = com.droiddeck.launcher.frontend.AddedGames.listingFile(this)
val listing = if (steamHere && cached.isFile) {
com.droiddeck.launcher.frontend.AddedGamesPublisher.refreshDuringSession(this, stillStarting)
Log.i(TAG, "added games: the last listing, refreshed in the background")
cached
} else {
val added = com.droiddeck.launcher.frontend.AddedGames.scan(this)
if (added.isNotEmpty()) Log.i(TAG, "added games: " + added.joinToString { "${it.name} (${it.exe.name})" })
com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added)
}
guest.add("BL_ADDED_GAMES=" + listing.path)
if (OfflineMode.enabled(this)) guest.add("BL_STEAM_OFFLINE=1")
if (com.droiddeck.launcher.gpu.Lossless.owned(this)) guest.add("BL_LOSSLESS_OWNED=1")
if (added.isNotEmpty()) Log.i(TAG, "added games: " + added.joinToString { "${it.name} (${it.exe.name})" })
}
guest.add("BL_LAUNCH_DIR=" + sessionRoot.path)
GameStorage.effective(this)?.let { guest.add("BL_LIBRARY_LABEL=" + it.label.replace('"', ' ')) }
@@ -949,7 +971,8 @@ class SessionService : Service() {
SessionArtifacts.markSteamLogs(it, File(SessionPrefs.steamRoot(this, arch), "logs"))
}
}
FexCatalog.cached(this)?.let { guest.addAll(FexCatalog.environment(it)) }
(if (BuildConfig.PLAY_DISTRIBUTION) FexCatalog.refresh(this) else FexCatalog.cached(this))
?.let { guest.addAll(FexCatalog.environment(it)) }
if (SessionState.mode == MODE_STEAM) {
guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0"))
}
@@ -4,7 +4,7 @@ import android.content.Context
import android.util.AtomicFile
import android.util.Log
import com.droiddeck.launcher.core.ArchivePaths
import com.droiddeck.launcher.core.Downloader
import com.droiddeck.launcher.core.ComponentDownloads
import com.droiddeck.launcher.core.FileUtils
import com.droiddeck.launcher.runtime.GuestCommand
import com.droiddeck.launcher.runtime.LinuxRuntime
@@ -87,13 +87,13 @@ object WinComponents {
private val SNAPSHOT_COVERS = INSTALLER_STEPS + setOf("download_archive", "archive_extract", "copy_dll", "copy_file") + OFFLINE_STEPS
fun fetch(): List<Component>? = Downloader.downloadString(CATALOG_URL)?.let { parse(it) }
fun fetch(): List<Component>? = ComponentDownloads.downloadString(CATALOG_URL)?.let { parse(it) }
private const val CATALOG_CACHE = "wincomponents-catalog.json"
private val CATALOG_CACHE = ComponentDownloads.catalogName("wincomponents-catalog.json")
fun fetch(context: Context): List<Component>? {
val cache = File(context.filesDir, CATALOG_CACHE)
Downloader.downloadString(CATALOG_URL)?.let { body ->
ComponentDownloads.downloadString(CATALOG_URL)?.let { body ->
parse(body)?.let { runCatching { write(cache, body) }; return it }
}
return runCatching { parse(cache.readText()) }.getOrNull()
@@ -258,7 +258,7 @@ object WinComponents {
val byRuntime = name.lowercase().let { it.endsWith(".exe") || it.endsWith(".cab") }
val file = if (byRuntime) File(root, "$MSI_CACHE/${archives.size}-$name").apply { parentFile?.mkdirs() }
else File(work, "${archives.size}-$name")
val ok = Downloader.downloadFile(url, file, false) { f -> onProgress(Progress(c.name, name, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }
val ok = ComponentDownloads.downloadFile(url, file, false) { f -> onProgress(Progress(c.name, name, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }
if (!ok) return "download failed: $name"
step.str("file_checksum").takeIf { it.length == 32 }?.let { md5 ->
if (!digest(file, "MD5").equals(md5, ignoreCase = true)) return "checksum mismatch: $name"
@@ -385,7 +385,7 @@ object WinComponents {
val root = LinuxRuntime.rootDir(context)
val name = c.snapshot.substringAfterLast('/')
val json = File(work, "${c.name}.snapshot.json")
if (!Downloader.downloadFile(c.snapshot, json, false) { f -> onProgress(Progress(c.name, name, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
if (!ComponentDownloads.downloadFile(c.snapshot, json, false) { f -> onProgress(Progress(c.name, name, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
return null to "download failed: $name"
}
val snapshot = runCatching { JSONObject(json.readText()) }.getOrNull() ?: return null to "the recording could not be read"
@@ -401,7 +401,7 @@ object WinComponents {
val url = c.snapshot.substringBeforeLast('/') + "/" + archiveName
val file = File(work, "${c.name}.files.tar.xz")
val sha = archive.optString("sha256").takeIf { it.matches(Regex("(?i)[0-9a-f]{64}")) }
if (!Downloader.downloadFile(url, file, false, sha) { f -> onProgress(Progress(c.name, archiveName, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
if (!ComponentDownloads.downloadFile(url, file, false, sha) { f -> onProgress(Progress(c.name, archiveName, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
return null to "download failed: $archiveName"
}
archive.optString("sha256").takeIf { it.length == 64 }?.let { sha ->
@@ -420,7 +420,7 @@ object WinComponents {
val installer = File(cache, "${c.name}-snapshot-src.bin")
var unpacked: File? = null
try {
if (!Downloader.downloadFile(url, installer, false) { f -> onProgress(Progress(c.name, installerName, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
if (!ComponentDownloads.downloadFile(url, installer, false) { f -> onProgress(Progress(c.name, installerName, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
return null to "download failed: $installerName"
}
unpacked = unpackInRuntime(context, c, installer, "snapshot", onProgress) ?: return null to "could not open $installerName"
@@ -496,7 +496,7 @@ object WinComponents {
val ext = if (listOf(step.str("file_name"), name).any { it.endsWith(".exe", ignoreCase = true) }) "exe" else "msi"
val file = File(cache, "${c.name}-${c.steps.indexOf(step)}.$ext")
try {
if (!Downloader.downloadFile(url, file, false) { f -> onProgress(Progress(c.name, name, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
if (!ComponentDownloads.downloadFile(url, file, false) { f -> onProgress(Progress(c.name, name, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
return null to "download failed: $name"
}
step.str("file_checksum").takeIf { it.length == 32 }?.let { md5 ->
@@ -19,9 +19,10 @@ object SteamLiveShortcuts {
private const val EVAL_TIMEOUT_MS = 20_000L
private const val THAW_WAIT_MS = 15_000L
fun sync(context: Context, games: List<AddedGames.Game>, include: (AddedGames.Game) -> Boolean = { it.source != Library.ADDED }) {
fun sync(context: Context, games: List<AddedGames.Game>, include: (AddedGames.Game) -> Boolean = { it.source != Library.ADDED }): Boolean {
val wanted = games.filter(include)
if (wanted.isEmpty() || !clientRunning()) return
if (wanted.isEmpty()) return true
if (!clientRunning()) return false
val list = JSONArray()
for (g in wanted) list.put(JSONObject().put("name", g.name).put("exe", g.guestExe).put("dir", g.guestDir).put("appid", g.appId and 0xFFFFFFFFL).put("launch", g.launchOptions))
val js = """
@@ -35,9 +36,10 @@ object SteamLiveShortcuts {
return added;
})()
""".trimIndent()
val result = evaluate(context, js) ?: return
val result = evaluate(context, js) ?: return false
Log.i(TAG, "live shortcuts: $result added")
if (result != "0") StoresState.logLine("Steam: $result shortcut(s) added to the running client")
return true
}
fun ensure(context: Context, name: String, exe: String, dir: String, compat: Boolean, appId: Long? = null): Long? {
@@ -102,6 +102,7 @@ class StoreLoginActivity : ComponentActivity() {
if (flow == null) { finish(); return }
val chrome = buildChrome()
setContentView(chrome)
com.droiddeck.launcher.core.SystemInsets.fit(chrome)
if (flooded) {
chrome.alpha = 0f
chrome.translationY = dp(14).toFloat()
@@ -39,7 +39,10 @@ class StoreDownloadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
instance = this
startForegroundCompat(build())
if (!startForegroundCompat(build())) {
onTimeout(startId, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
return START_NOT_STICKY
}
val key = intent?.getStringExtra(EXTRA_KEY)
when (intent?.action) {
ACTION_PAUSE -> key?.let {
@@ -70,6 +73,15 @@ class StoreDownloadService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onTimeout(startId: Int, fgsType: Int) {
Log.w(TAG, "data sync time used up; pausing downloads")
StoresState.latestDownloads.filter { it.state == DownloadState.RUNNING }.forEach {
DownloadQueue.pause(it.key)
pausedNote(this, it.key)
}
stopNow()
}
private fun pending(action: String, key: String, code: Int): PendingIntent {
val i = Intent(this, StoreDownloadService::class.java).setAction(action).putExtra(EXTRA_KEY, key)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) PendingIntent.getForegroundService(this, code, i, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
@@ -146,10 +158,9 @@ class StoreDownloadService : Service() {
wakeAt = 0L
}
private fun startForegroundCompat(n: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) startForeground(ID, n, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
else startForeground(ID, n)
}
private fun startForegroundCompat(n: Notification): Boolean =
com.droiddeck.launcher.core.ForegroundServices.start(this, ID, n,
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
private fun stopNow() {
releaseLocks()
@@ -33,7 +33,7 @@ class XboxCloudActivity : ComponentActivity() {
private var customCallback: WebChromeClient.CustomViewCallback? = null
private lateinit var root: FrameLayout
@SuppressLint("SetJavaScriptEnabled")
@SuppressLint("SetJavaScriptEnabled", "RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -117,6 +117,7 @@ class XboxCloudActivity : ComponentActivity() {
if (hasFocus) hideBars()
}
@android.annotation.SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
val fromController = event.source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
@@ -252,6 +252,7 @@ class XboxCloudPlayerActivity : ComponentActivity() {
web?.evaluateJavascript("window.__ddButton && window.__ddButton('Nexus', $value)", null)
}
@android.annotation.SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (menu != null) {
if (isGuideKey(event)) {
@@ -8,6 +8,7 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@@ -45,7 +46,7 @@ class XboxCloudSettingsActivity : ComponentActivity() {
}.getOrDefault(emptyList())
}
}
Column(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).verticalScroll(rememberScrollState())) {
Column(Modifier.fillMaxSize().systemBarsPadding().background(MaterialTheme.colorScheme.background).verticalScroll(rememberScrollState())) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
OutlinedButton(onClick = { finish() }, modifier = Modifier.focusGlide(CircleShape)) { Text("Back") }
Text("Xbox Cloud Gaming", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold,
@@ -42,6 +42,7 @@ class XboxLoginActivity : ComponentActivity() {
host.addView(status)
host.addView(Button(this).apply { text = getString(com.droiddeck.launcher.R.string.stores_login_close); setOnClickListener { finish() } })
setContentView(host)
com.droiddeck.launcher.core.SystemInsets.fit(host)
if (android.os.Build.VERSION.SDK_INT < 28 || !WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) ||
!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
status.text = "Update Android System WebView to sign in to Xbox."
@@ -418,6 +418,7 @@ class XboxNativePlayerActivity : ComponentActivity() {
root.postDelayed({ pad.overrideButtons = pad.overrideButtons and 2.inv() }, 200)
}
@android.annotation.SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (menu != null) {
if (isGuide(event)) { if (event.action == KeyEvent.ACTION_UP) { if (swallowGuideUp) swallowGuideUp = false else closeMenu() }; return true }
@@ -95,11 +95,13 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p
}
override fun onFrame(frame: VideoFrame) {
if (android.os.Build.VERSION.SDK_INT < 29) return
val w = frame.rotatedWidth; val h = frame.rotatedHeight
synchronized(lock) { if (w != readerW || h != readerH) startReader(w, h) }
egl.onFrame(frame)
}
@androidx.annotation.RequiresApi(29)
private fun startReader(w: Int, h: Int) {
val old = reader
if (old != null) egl.releaseEglSurface { }
@@ -114,6 +116,7 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p
log("native video ${w}x$h")
}
@androidx.annotation.RequiresApi(29)
private fun onImage(r: ImageReader) {
val image = runCatching { r.acquireLatestImage() }.getOrNull() ?: return
synchronized(lock) {
@@ -72,6 +72,7 @@ object Syncthing {
@Volatile private var onWifi = true
private var visible = 0
private var foreground = false
private var backgroundBudgetAvailable = true
private val watchers = AtomicInteger()
private val pauseFailures = AtomicInteger()
private val pauseLock = Any()
@@ -102,7 +103,10 @@ object Syncthing {
override fun onActivityStarted(activity: Activity) {
visible++
main.removeCallbacks(leaveForeground)
if (!foreground) { foreground = true; evaluate() }
val resume = !foreground || !backgroundBudgetAvailable
backgroundBudgetAvailable = true
foreground = true
if (resume) evaluate()
}
override fun onActivityStopped(activity: Activity) {
visible = (visible - 1).coerceAtLeast(0)
@@ -137,9 +141,15 @@ object Syncthing {
fun evaluate() {
if (Looper.myLooper() != Looper.getMainLooper()) { main.post { evaluate() }; return }
val enabled = SyncPrefs.enabled(app)
val background = SyncPrefs.runMode(app) == SyncPrefs.RUN_BACKGROUND
val background = SyncPrefs.runMode(app) == SyncPrefs.RUN_BACKGROUND && backgroundBudgetAvailable
if (enabled && background) SyncthingService.start(app) else SyncthingService.stop(app)
if (enabled && (background || foreground)) start() else stop()
if (enabled && (foreground || background && backgroundBudgetAvailable)) start() else stop()
}
fun backgroundServiceUnavailable() {
if (Looper.myLooper() != Looper.getMainLooper()) { main.post { backgroundServiceUnavailable() }; return }
backgroundBudgetAvailable = false
evaluate()
}
fun setEnabled(context: Context, on: Boolean, forget: Boolean = false) {
@@ -16,6 +16,13 @@ import com.droiddeck.launcher.stores.download.StoreDownloadService
class SyncthingService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onTimeout(startId: Int, fgsType: Int) {
running = false
Syncthing.backgroundServiceUnavailable()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_PAUSE -> Syncthing.setPaused(this, !Syncthing.userPaused)
@@ -26,14 +33,20 @@ class SyncthingService : Service() {
return START_NOT_STICKY
}
val n = notification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) startForeground(ID, n, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
else startForeground(ID, n)
if (!com.droiddeck.launcher.core.ForegroundServices.start(this, ID, n,
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)) {
running = false
Syncthing.backgroundServiceUnavailable()
stopSelf()
return START_NOT_STICKY
}
Syncthing.onChange = { getSystemService(NotificationManager::class.java)?.notify(ID, notification()) }
Syncthing.evaluate()
return START_STICKY
return START_NOT_STICKY
}
override fun onDestroy() {
running = false
Syncthing.onChange = null
super.onDestroy()
}
@@ -92,7 +105,7 @@ class SyncthingService : Service() {
fun start(context: Context) {
if (running) return
running = true
runCatching { context.startForegroundService(Intent(context, SyncthingService::class.java)) }.onFailure { running = false }
runCatching { context.startForegroundService(Intent(context, SyncthingService::class.java)) }.onFailure { running = false; Syncthing.backgroundServiceUnavailable() }
}
fun stop(context: Context) {
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -63,7 +64,7 @@ class SyncthingWebActivity : ComponentActivity() {
DisposableEffect(target) { onDispose { target?.close() } }
LaunchedEffect(Syncthing.phase) { if (Syncthing.phase == Syncthing.Phase.OFF) finish() }
BackHandler { web?.takeIf { it.canGoBack() }?.goBack() ?: finish() }
Column(Modifier.fillMaxSize().background(colors.background)) {
Column(Modifier.fillMaxSize().systemBarsPadding().background(colors.background)) {
Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth().background(colors.surface).padding(horizontal = 16.dp, vertical = 8.dp),
@@ -26,9 +26,11 @@ object BuildWatermark {
commit: String = BuildConfig.SOURCE_COMMIT,
title: String = BuildConfig.PR_TITLE,
branch: String = BuildConfig.BUILD_BRANCH,
play: Boolean = BuildConfig.PLAY_DISTRIBUTION,
): List<String>? {
if (ci && pr <= 0) return null
val hash = commit.take(7).ifEmpty { "unknown" }
if (play) return listOf(hash)
if (ci && pr <= 0) return null
val head = if (pr > 0) "PR-$pr | $hash" else "LOCAL | $hash"
val detail = (if (pr > 0) title else branch).trim()
return if (detail.isEmpty()) listOf(head) else listOf(head, truncate(detail))
@@ -173,7 +173,7 @@ private fun DesktopDock(
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val narrow = LocalNarrowPane.current
val available = s.emulators.filter { !it.installed }
val available = if (com.droiddeck.launcher.BuildConfig.PLAY_DISTRIBUTION) emptyList() else s.emulators.filter { !it.installed }
val flatpaks = com.droiddeck.launcher.store.StoreState.installed.filter { f -> apps.none { it.key == "flatpak:${f.id}" } }
val keys = installed.map { "emu:${it.id}" } + apps.map { "user:${it.key}" } + flatpaks.map { "flatpak:${it.id}" }
var lastAt by remember { mutableStateOf<Rect?>(null) }
@@ -194,7 +194,7 @@ object DualScreenPanel {
runCatching {
manager.appTasks.firstOrNull {
@Suppress("DEPRECATION")
(if (android.os.Build.VERSION.SDK_INT >= 29) it.taskInfo.taskId else it.taskInfo.persistentId) == task
it.taskInfo?.let { info -> if (android.os.Build.VERSION.SDK_INT >= 29) info.taskId else info.persistentId } == task
}?.moveToFront() ?: manager.moveTaskToFront(task, 0)
}
}
@@ -221,6 +221,11 @@ class DualScreenActivity : ComponentActivity() {
finishAndRemoveTask()
return
}
onBackPressedDispatcher.addCallback(this, object : androidx.activity.OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (DualScreenPanel.typingShown) DualScreenPanel.finishTyping()
}
})
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
applyImmersiveMode()
setContent {
@@ -286,11 +291,7 @@ class DualScreenActivity : ComponentActivity() {
} else if (unfocusPending && !DualScreenPanel.typingShown) dropFocus()
}
@Suppress("OVERRIDE_DEPRECATION")
override fun onBackPressed() {
if (DualScreenPanel.typingShown) DualScreenPanel.finishTyping()
}
@android.annotation.SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (DualScreenPanel.typingShown && event.keyCode == KeyEvent.KEYCODE_BUTTON_B) {
if (event.action == KeyEvent.ACTION_UP && !event.isCanceled) DualScreenPanel.finishTyping()
@@ -107,6 +107,9 @@ class FrontEndState(
val gameDebugLogsNeedsRestart: Boolean = false,
val vacDiagnostics: Boolean = false,
val agentCommands: Boolean = false,
val bluetoothAllowed: Boolean? = null,
val allFilesAllowed: Boolean? = null,
val localNetworkAllowed: Boolean? = null,
val graphicsLogs: Boolean = false,
val steamGpuDrawing: Boolean = false,
val bridgeFellBack: Boolean = false,
@@ -239,6 +242,9 @@ class FrontEndActions(
val onGameDebugLogs: () -> Unit = {},
val onVacDiagnostics: () -> Unit = {},
val onAgentCommands: () -> Unit = {},
val onBluetoothAllowed: () -> Unit = {},
val onAllFilesAllowed: () -> Unit = {},
val onLocalNetworkAllowed: () -> Unit = {},
val onGraphicsLogs: () -> Unit = {},
val onSteamGpuDrawing: () -> Unit = {},
val onClearCachedApps: (Boolean) -> Unit = {},
@@ -410,6 +410,11 @@ internal fun SetupPanel(
}
SettingsGroup(stringResource(R.string.setup_runtime)) { runtimeCheck(false) }
SteamIntegrationGroup(s.integration, s.integrationFocus, a.onIntegration, a.onIntegrationAppInfo, a.onIntegrationLocation)
if (com.droiddeck.launcher.BuildConfig.PLAY_DISTRIBUTION) {
SecondaryButton(stringResource(R.string.setup_privacy_policy), compact = true) {
openUrl(ctx, "https://github.com/Droid-Deck/DroidDeck/blob/playstore/PRIVACY.md")
}
}
}
1 -> {
val controller = s.controller
@@ -573,6 +578,15 @@ internal fun SetupPanel(
) { a.onGameDebugLogs() }
ToggleRow(host, "vacdiag", stringResource(R.string.setup_vac_diag), stringResource(R.string.setup_vac_diag_hint), s.vacDiagnostics) { a.onVacDiagnostics() }
ToggleRow(host, "agent-commands", stringResource(R.string.setup_agent_commands), null, s.agentCommands) { a.onAgentCommands() }
s.allFilesAllowed?.let { on ->
ToggleRow(host, "all-files", stringResource(R.string.setup_all_files), null, on) { a.onAllFilesAllowed() }
}
s.localNetworkAllowed?.let { on ->
ToggleRow(host, "local-network", stringResource(R.string.setup_local_network), null, on) { a.onLocalNetworkAllowed() }
}
s.bluetoothAllowed?.let { on ->
ToggleRow(host, "bluetooth", stringResource(R.string.mode_controller_steam), null, on) { a.onBluetoothAllowed() }
}
ToggleRow(host, "graphics-logs", stringResource(R.string.setup_graphics_logs), stringResource(R.string.setup_graphics_logs_hint), s.graphicsLogs) { a.onGraphicsLogs() }
if (!s.adreno && com.droiddeck.launcher.session.SessionPrefs.steamGpuCompositingAllowed()) ToggleRow(host, "steam-gpu-drawing", stringResource(R.string.setup_steam_gpu_drawing), stringResource(R.string.setup_steam_gpu_drawing_hint), s.steamGpuDrawing) { a.onSteamGpuDrawing() }
ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs,
@@ -57,12 +57,22 @@ fun ChooseAppDisplayDialog(
}
@Composable
fun ConfirmDialog(title: String, text: String, confirm: String, onConfirm: () -> Unit, onDismiss: () -> Unit) {
fun ConfirmDialog(title: String, text: String, confirm: String, onConfirm: () -> Unit, onDismiss: () -> Unit,
progress: Int? = null, confirmEnabled: Boolean = true, closeOnConfirm: Boolean = true) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { Text(text) },
confirmButton = { TextButton(onClick = { onDismiss(); onConfirm() }, modifier = Modifier.focusGlide(CircleShape)) { Text(confirm) } },
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(text)
if (progress != null) {
if (progress >= 0) androidx.compose.material3.LinearProgressIndicator(
progress = { progress / 100f }, modifier = Modifier.fillMaxWidth().height(4.dp))
else androidx.compose.material3.LinearProgressIndicator(modifier = Modifier.fillMaxWidth().height(4.dp))
}
}
},
confirmButton = { TextButton(enabled = confirmEnabled, onClick = { if (closeOnConfirm) onDismiss(); onConfirm() }, modifier = Modifier.focusGlide(CircleShape)) { Text(confirm) } },
dismissButton = { TextButton(onClick = onDismiss, modifier = Modifier.focusGlide(CircleShape)) { Text(stringResource(R.string.common_cancel)) } },
)
}
@@ -1,5 +1,7 @@
package com.droiddeck.launcher.ui
import com.droiddeck.launcher.BuildConfig
import com.droiddeck.launcher.R
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
@@ -77,8 +79,10 @@ class UpdatesState(
val stage: String? = null,
val percent: Int = -1,
val askPermission: Boolean = false,
val playUpdateAvailable: Boolean? = null,
val playCheckedAt: Long = 0L,
) {
val hasUpdate: Boolean get() = AppUpdates.hasUpdate(catalog, follow)
val hasUpdate: Boolean get() = if (BuildConfig.PLAY_DISTRIBUTION) playUpdateAvailable == true else AppUpdates.hasUpdate(catalog, follow)
}
class UpdatesActions(
@@ -87,6 +91,7 @@ class UpdatesActions(
val onInstall: (Release) -> Unit = {},
val onAllowInstalls: () -> Unit = {},
val onDismissPermission: () -> Unit = {},
val onOpenPlay: () -> Unit = {},
)
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@@ -106,13 +111,17 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie
maxLines = 1, modifier = Modifier.weight(1f)
.basicMarquee(iterations = Int.MAX_VALUE, initialDelayMillis = 700, velocity = 40.dp),
)
val checked = u.catalog?.checkedAt?.takeIf { it > 0 }
if (!LocalNarrowPane.current) Text(
val checked = (if (BuildConfig.PLAY_DISTRIBUTION) u.playCheckedAt else u.catalog?.checkedAt)?.takeIf { it > 0 }
if (!BuildConfig.PLAY_DISTRIBUTION && !LocalNarrowPane.current) Text(
if (u.checking) stringResource(R.string.common_checking) else if (checked != null) stringResource(R.string.upd_checked, ago(checked).replaceFirstChar { it.lowercase() }) else stringResource(R.string.upd_not_checked),
fontSize = 13.sp, color = colors.onSurfaceVariant,
)
ToolIcon(Icons.Outlined.Refresh, stringResource(R.string.store_check_updates), busy = u.checking, enabled = !u.checking && u.stage == null, onClick = ua.onCheck)
}
if (BuildConfig.PLAY_DISTRIBUTION) {
StatusPanel(s, u, ua, me)
return@Column
}
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
val stacked = LocalNarrowPane.current || maxWidth < 600.dp
Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = 16.dp)) {
@@ -131,7 +140,7 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie
}
}
}
if (u.askPermission) AlertDialog(
if (!BuildConfig.PLAY_DISTRIBUTION && u.askPermission) AlertDialog(
onDismissRequest = ua.onDismissPermission,
title = { Text(stringResource(R.string.upd_perm_title)) },
text = {
@@ -156,6 +165,14 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m
val installBlock = release?.let { AppUpdates.installBlock(it, me) }
class Look(val tint: Color, val status: String, val headline: String, val detail: String?)
val look = when {
BuildConfig.PLAY_DISTRIBUTION -> Look(
if (u.playUpdateAvailable == true) AttentionAmber else colors.onSurfaceVariant,
stringResource(when {
u.checking -> R.string.common_checking
u.playUpdateAvailable == true -> R.string.upd_available
u.playUpdateAvailable == false -> R.string.upd_current_status
else -> R.string.upd_not_checked
}), stringResource(R.string.upd_title), null)
!me.updatable -> Look(colors.onSurfaceVariant, stringResource(R.string.upd_signed_status), stringResource(R.string.upd_signed_headline),
stringResource(R.string.upd_signed_detail))
catalog == null -> Look(colors.onSurfaceVariant, if (u.checking) stringResource(R.string.common_checking) else stringResource(R.string.upd_not_checked), stringResource(R.string.upd_title), null)
@@ -189,6 +206,9 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m
val button = release?.apk?.size?.takeIf { it > 0 }?.let { " · ${megabytes(it)}" }.orEmpty()
Box(Modifier.padding(top = 14.dp)) {
when {
BuildConfig.PLAY_DISTRIBUTION && u.playUpdateAvailable == true ->
PrimaryButton(stringResource(R.string.common_update), onClick = ua.onOpenPlay)
BuildConfig.PLAY_DISTRIBUTION -> Unit
u.stage != null -> Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(if (u.percent >= 0) stringResource(R.string.upd_stage_percent, u.stage, u.percent) else stringResource(R.string.upd_stage, u.stage), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground)
if (u.percent >= 0) LinearProgressIndicator(progress = { u.percent / 100f }, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
@@ -155,6 +155,7 @@ object AppUpdates {
}
fun hasUpdate(catalog: Catalog?, follow: Follow): Boolean {
if (BuildConfig.PLAY_DISTRIBUTION) return false
if (catalog == null) return false
val me = installed()
val r = release(catalog, follow) ?: return false
@@ -255,6 +256,7 @@ object AppUpdates {
}
fun refresh(context: Context): Catalog {
check(!BuildConfig.PLAY_DISTRIBUTION) { "Application updates are managed by Google Play" }
val checkedAt = System.currentTimeMillis()
val published = JSONObject(get(context, "$CATALOG_URL?checked=$checkedAt"))
val catalog = try {
@@ -21,12 +21,13 @@ object SelfInstaller {
const val ACTION_STATUS = "com.droiddeck.launcher.update.INSTALL_STATUS"
fun canInstall(context: Context): Boolean =
Build.VERSION.SDK_INT < 26 || context.packageManager.canRequestPackageInstalls()
!BuildConfig.PLAY_DISTRIBUTION && (Build.VERSION.SDK_INT < 26 || context.packageManager.canRequestPackageInstalls())
fun permissionIntent(context: Context): Intent =
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}"))
fun download(context: Context, apk: AppUpdates.Apk, progress: (Int) -> Unit): File {
check(!BuildConfig.PLAY_DISTRIBUTION) { "Application updates are managed by Google Play" }
val dir = File(context.cacheDir, "updates")
dir.listFiles()?.forEach { if (it.name != apk.name) FileUtils.delete(it) }
dir.mkdirs()
@@ -91,6 +92,7 @@ object SelfInstaller {
}
fun install(context: Context, apk: File, onSession: (Int) -> Unit = {}) {
check(!BuildConfig.PLAY_DISTRIBUTION) { "Application updates are managed by Google Play" }
val installer = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
setAppPackageName(context.packageName)
+127 -4
View File
@@ -679,9 +679,9 @@
<string name="hud_group_metrics">القراءات</string>
<string name="hud_metric_graph">مخطط زمن الإطار</string>
<string name="hud_metric_decimal">معدل الإطارات بخانة عشرية</string>
<string name="hud_metric_low1">أدنى 1%</string>
<string name="hud_metric_low01">أدنى 0.1%</string>
<string name="hud_metric_low001">أدنى 0.01%</string>
<string name="hud_metric_low1" formatted="false">أدنى 1%</string>
<string name="hud_metric_low01" formatted="false">أدنى 0.1%</string>
<string name="hud_metric_low001" formatted="false">أدنى 0.01%</string>
<string name="hud_metric_engine">محرك الرسومات</string>
<string name="hud_metric_gpu_model">اسم GPU</string>
<string name="hud_metric_gpu">حمل GPU</string>
@@ -3705,7 +3705,7 @@
<string name="integration_shortcuts_maybe_restricted">غير متاح في Android؟ معلومات التطبيق → ⋮ → السماح بالإعدادات المحظورة</string>
<string name="integration_shortcuts_restricted">محظور من Android · معلومات التطبيق → ⋮ → السماح بالإعدادات المحظورة</string>
<string name="integration_shortcuts_service">مفاتيح نظام DroidDeck</string>
<string name="integration_shortcuts_service_description">إبقاء Alt+Tab ومفتاح Win وزر الرئيسية داخل جلسة DroidDeck النشطة بدلاً من Android.</string>
<string name="integration_shortcuts_service_description">يُبقي Alt+Tab ومفتاح Win وزر الصفحة الرئيسية/الدليل داخل جلسة DroidDeck النشطة، ويكتشف التطبيقات على الشاشة السفلية. أثناء تركيز الجلسة يقرأ المفاتيح من لوحة مفاتيح فعلية، ويقرأ أسماء حزم التطبيقات وحدود النوافذ على هذا الجهاز فقط. لا يقرأ النصوص ولا يلتقط الشاشة ولا يرسل هذه البيانات.</string>
<string name="integration_brightness_needed">فعّل «السطوع» من الإعداد → تكامل Steam</string>
<string name="setup_gpu_bridge_off">جسر Mali: متوقف</string>
<string name="setup_gpu_bridge_on">جسر Mali: يعمل</string>
@@ -3803,6 +3803,52 @@
<string name="gog_repair_action">إصلاح الملفات</string>
<string name="gog_refresh_action">تحديث التثبيت</string>
<string name="gog_select_patch">اختيار</string>
<string name="setup_all_files">الوصول إلى جميع الملفات</string>
<string name="setup_local_network">الشبكة المحلية</string>
<string name="libraries_title">مكتبات Steam إضافية</string>
<string name="libraries_hint">مجلدات يمكن لـ Steam تثبيت الألعاب فيها وتشغيلها منها، ولكل منها اسم مستقل. إزالتها تبقي الألعاب على القرص.</string>
<string name="libraries_add">إضافة مكتبة</string>
<string name="libraries_rename_title">اسم المكتبة</string>
<string name="libraries_pick">اختر مجلدًا للمكتبة</string>
<string name="libraries_full">بحد أقصى %1$d مكتبات إضافية</string>
<string name="libraries_overlap">هذا المجلد مكتبة بالفعل أو موجود داخل مكتبة</string>
<string name="libraries_missing">%1$s · غير متاحة</string>
<string name="libraries_applies_next">يسري من جلسة Steam التالية</string>
<string name="bottom_desktop_start">تشغيل سطح المكتب</string>
<string name="bottom_desktop_mode_touch">لمس مباشر</string>
<string name="bottom_desktop_trackpad">لوحة لمس. انقر للتبديل إلى اللمس المباشر</string>
<string name="bottom_desktop_direct_touch">لمس مباشر. انقر للتبديل إلى لوحة اللمس</string>
<string name="bottom_desktop_starting">جارٍ تشغيل سطح مكتب KDE…</string>
<string name="bottom_desktop_ended">أُغلق سطح المكتب.</string>
<string name="bottom_desktop_failed">تعذر تشغيل سطح المكتب. سجلاته في مجلد bottom-desktop لهذه الجلسة.</string>
<string name="bottom_desktop_off">سطح المكتب مغلق.</string>
<string name="libraries_rename">إعادة التسمية</string>
<string name="second_mode_desktop">سطح المكتب</string>
<string name="bottom_desktop_mode_trackpad">لوحة اللمس</string>
<string name="bottom_desktop_title">سطح المكتب · %1$s</string>
<string name="setup_dual_screen_launcher">المشغّل على شاشتين</string>
<string name="setup_dual_screen_launcher_hint">يضع تلقائي أقسام المشغّل (Steam والمكتبة والمتاجر وغيرها) على الشاشة الثانية المدمجة. يستخدم تشغيل أي شاشة ثانية. يبقي إيقاف المشغّل على شاشة واحدة.</string>
<string name="setup_dual_screen_sessions">الجلسات على شاشتين</string>
<string name="setup_dual_screen_sessions_hint">يضع تلقائي قائمة اللعبة على الشاشة الثانية المدمجة بدل تراكبها. يستخدم تشغيل أي شاشة ثانية. يبقي إيقاف القائمة على شاشة اللعبة؛ ويمكنك اختيار شاشة ثانية منها.</string>
<string name="stats_hud_frametime">زمن الإطار</string>
<string name="stats_hud_last_seconds">آخر %1$d ث</string>
<string name="stats_hud_avg_frame">متوسط الإطار</string>
<string name="stats_hud_low" formatted="false">أدنى 1 %</string>
<string name="stats_hud_slow_frames">إطارات تتجاوز 33 مللي ثانية</string>
<string name="stats_hud_per_minute">%1$d / دقيقة</string>
<string name="stats_hud_playing_for">وقت اللعب: %1$s</string>
<string name="stats_hud_core_group">%1$d أنوية، حتى %2$s غيغاهرتز</string>
<string name="stats_hud_core_group_unknown">%1$d أنوية</string>
<string name="stats_hud_fps_of">إطارات في الثانية عند %1$d هرتز</string>
<string name="stats_hud_red_mark">تشير العلامة الحمراء إلى الموضع الذي تعرض فيه لوحة المعلومات درجة الحرارة بالأحمر.</string>
<string name="stats_hud_whole_device">الجهاز بالكامل</string>
<string name="bottom_hud_group">لوحة معلومات الشاشة السفلية</string>
<string name="bottom_strip_tabs">شريط في كل تبويب</string>
<string name="bottom_strip_lows" formatted="false">أدنى 1 % و0.1 %</string>
<string name="bottom_strip_cpu_temp">درجة حرارة المعالج</string>
<string name="stats_hud_battery_temp">البطارية</string>
<string name="stats_hud_game_cores">لعبة</string>
<string name="setup_privacy_policy">سياسة الخصوصية</string>
<string name="stores_gamejolt_not_owned">ليس في مكتبة Game Jolt الخاصة بك</string>
<string name="stores_gamejolt_no_build">لا يوجد تنزيل لنظام Windows لهذه اللعبة</string>
<string name="stores_gamejolt_no_exe">لم يتم العثور على لعبة Windows في التنزيل</string>
@@ -3832,4 +3878,81 @@
<string name="stores_gamejolt_filter_status">الحالة</string>
<string name="stores_gamejolt_filter_partners">الشركاء</string>
<string name="stores_gamejolt_followed">الألعاب المتابَعة</string>
<string name="cmgr_powervr_dxvk_managed">يتوفر DXVK من بيئة تشغيل PowerVR الخاصة بالنظام. يمكن تخزين حزم DXVK الأخرى لكنها لا تُطبَّق مع هذا التعريف.</string>
<string name="cmgr_powervr_dxvk_queued">في الانتظار: %1$s (بانتظار تعريف تشغيل مختلف)</string>
<string name="diagnostics_share_all">كل الحزم</string>
<string name="diagnostics_share_games">الألعاب والتوافق</string>
<string name="diagnostics_share_graphics">الرسومات</string>
<string name="diagnostics_share_session">الجلسة والجهاز</string>
<string name="diagnostics_share_title">مشاركة حزم التشخيص</string>
<string name="drivers_built_on">بُني %1$s</string>
<string name="drivers_mali_bridge_auto">لا يوجد تعريف مفتوح المصدر يدعم هذا الـ Mali بعد: جسر Mali قيد التشغيل.</string>
<string name="drivers_mali_bridge_fallback">%1$s لم يُنشر بعد: جسر Mali قيد التشغيل إلى أن يُنشر.</string>
<string name="drivers_mali_no_pair_auto">لا يوجد تعريف مفتوح المصدر يدعم هذا الـ Mali بعد: يُستخدم %1$s على تعريف الهاتف الأصلي.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s غير متاح، وتعريف هذا الهاتف يُبلغ عن Vulkan %2$s (يحتاج Venus إلى 1.2): يُستخدم جسر Mali.</string>
<string name="drivers_mali_unpublished_auto">%1$s لم يُنشر بعد: يُستخدم %2$s إلى أن يُنشر.</string>
<string name="drivers_pair_updated">تم تحديث %1$s إلى أحدث إصدار</string>
<string name="drivers_powervr_builtin">تم اختيار تعريف PowerVR الخاص بالنظام لبيئة التشغيل والعرض.</string>
<string name="drivers_route_bridge">جسر Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">يستخدم التعريفات المضمّنة لوحدة GPU هذه</string>
<string name="gpu_system_powervr">PowerVR النظام</string>
<string name="gpuinfo_bridge">يعمل عبر جسر Mali، على تعريف Vulkan الأصلي للهاتف</string>
<string name="gpuinfo_bridge_off">غير مدعوم مع إيقاف جسر Mali: يحتاج Turnip إلى وحدة GPU من نوع Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">تعريف PowerVR الخاص بالنظام: يختلف التوافق حسب الجهاز</string>
<string name="gpuinfo_xclipse">تجريبي: RADV أو تعريف Xclipse الخاص بالنظام</string>
<string name="libraries_access_allow">سماح</string>
<string name="libraries_access_hint">اسمح بالوصول إلى كل الملفات ليتمكن DroidDeck من رؤية مكتبات Steam التي تحفظها تطبيقات أخرى في وحدة التخزين الداخلية</string>
<string name="libraries_access_title">البحث عن المكتبات في وحدة التخزين الداخلية</string>
<string name="libraries_add_found">إضافة</string>
<string name="libraries_found_hint">عُثر عليها على هذا الجهاز · %1$s · %2$s</string>
<string name="libraries_used_inner">تمت إضافة %1$s، مكتبة Steam الموجودة داخل المجلد الذي اخترته</string>
<string name="loadstate_steam_update_applying">جارٍ تطبيق تحديث عميل Steam…</string>
<string name="pckb_group_arrows">مفاتيح الأسهم</string>
<string name="pckb_group_function">Esc وصف مفاتيح الوظائف</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">المفاتيح المخفية</string>
<string name="pckb_hidden_keys_hint">المفاتيح التي نادرًا ما تستخدمها تبقى خارج لوحة المفاتيح.</string>
<string name="pckb_opacity">الشفافية</string>
<string name="pckb_opacity_hint">خفّضها لترى اللعبة خلف لوحة المفاتيح.</string>
<string name="pckb_size">الحجم على الشاشة</string>
<string name="pckb_size_hint">حصة الشاشة التي تشغلها لوحة المفاتيح، مع الترويسة.</string>
<string name="session_end_present_fault">تعذّر على الجلسة الرسم على الشاشة: %1$s. تعرض هذه الشاشة السبب بدلًا من شاشة سوداء؛ وتجد التفاصيل في سجلات الجلسة.</string>
<string name="session_killed_for_memory">انتهت الجلسة الأخيرة لأن Android أغلق DroidDeck لتحرير الذاكرة، وليس بسبب عطل. أغلق التطبيقات الأخرى أو خفّض إعدادات الخامات في اللعبة، ثم حاول مجددًا.</string>
<string name="setup_enhanced_diagnostics">تشخيص محسّن للجلسة التالية</string>
<string name="setup_enhanced_diagnostics_hint">يلتقط سجلات مفصلة لجلسة واحدة. قد يقلل الأداء ويستهلك مساحة تخزين أكبر. يتوقف تلقائيًا عند بدء الجلسة؛ راجع السجلات قبل المشاركة.</string>
<string name="setup_gpu_bridge_fell_back">فشل جسر Mali في اختباره الذاتي على %1$s في الجلسة الأخيرة (الصور أو الرسم)، فاستخدمت الجلسة زوج تعريفات PanVK بدلًا منه.</string>
<string name="setup_gpu_bridge_no_memory_sharing">لم يجد جسر Mali طريقة لمشاركة الذاكرة مع التعريف على %1$s في الجلسة الأخيرة، فاستخدمت الجلسة زوج تعريفات PanVK بدلًا منه. لا يمكن للجسر العمل على هذا الهاتف؛ وإيقافه يُبقي زوج PanVK.</string>
<string name="setup_gpu_bridge_unusable">لا يستطيع %1$s استخدام جسر Mali (يحتاج إلى وحدة Mali GPU دون مسار تعريف النظام)، لذا لا تأثير لهذا المفتاح هنا.</string>
<string name="setup_gpu_bridged">GPU عبر جسر Mali</string>
<string name="setup_gpu_venus_host_too_old">يُبلغ التعريف على %1$s عن Vulkan %2$s. يحتاج Mali (Venus) إلى Vulkan 1.2 أو أحدث (لا يعمل gamescope على الأقدم)، لذا تتخطى الجلسات Venus وتستخدم زوج تعريفات PanVK بدلًا منه.</string>
<string name="setup_gpu_venus_off">Mali (Venus): متوقف</string>
<string name="setup_gpu_venus_on">Mali (Venus): قيد التشغيل</string>
<string name="setup_graphics_logs">سجلات الرسومات المفصلة</string>
<string name="setup_graphics_logs_hint">سجلات Proton وDXVK والتعريف لكل لعبة ضمن سجلات الجلسة. تعمل دائمًا عندما لا تكون وحدة GPU من نوع Adreno.</string>
<string name="setup_log_current">الجلسة الحالية</string>
<string name="setup_log_delete">حذف</string>
<string name="setup_log_delete_failed">تعذّر حذف سجلات الجلسة.</string>
<string name="setup_log_delete_message">حذف %1$s؟ لا يمكن التراجع عن ذلك.</string>
<string name="setup_log_delete_title">حذف سجلات الجلسة؟</string>
<string name="setup_log_empty">لا توجد سجلات جلسات محفوظة بعد.</string>
<string name="setup_log_export_failed">تعذّر حفظ سجلات الجلسة.</string>
<string name="setup_log_export_hint">تتضمن ملفات ZIP المشاركة والمحفوظة الجلسة المحددة بالإضافة إلى تشخيصات Steam والمتجر والأدوات وBattle.net ذات الصلة.</string>
<string name="setup_log_export_saved">تم حفظ سجلات الجلسة.</string>
<string name="setup_log_save">حفظ على الجهاز</string>
<string name="setup_log_sessions">سجلات الجلسات المتاحة</string>
<string name="setup_log_share">مشاركة</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">واجهة Steam: الرسم بوحدة GPU</string>
<string name="setup_steam_gpu_drawing_hint">مع زوج تعريفات Xclipse، يرسم Steam صفحاته برمجيًا (أظهر الرسم بـ GPU مربعات معطوبة). فعّله للحصول على واجهة Steam أسرع إن بدت سليمة على هذا الهاتف.</string>
<string name="swap_game_screen">عرض اللعبة على</string>
<string name="swap_game_screen_bottom">الشاشة السفلية</string>
<string name="swap_game_screen_group">شاشة اللعبة</string>
<string name="swap_game_screen_top">الشاشة العلوية</string>
<string name="swap_screens">تبديل الشاشتين</string>
<string name="swap_screens_unavailable">الشاشة السفلية غير متاحة حاليًا</string>
<string name="upd_block_non_adreno_test">هذه نسخة الاختبار لغير Snapdragon: النسخ الرسمية هنا تعمل على Adreno فقط، لذلك لا تُثبَّت فوقها. تجد نسخ الاختبار الجديدة على github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">التحديث في الانتظار.</string>
<string name="wincomp_repair_required">يحتاج هذا التنزيل إلى تحديث.</string>
<string name="wincomp_retry">إعادة محاولة التحديث</string>
</resources>
+129 -6
View File
@@ -673,9 +673,9 @@
<string name="hud_group_metrics">Lecturas</string>
<string name="hud_metric_graph">Gráfico de tiempos de fotograma</string>
<string name="hud_metric_decimal">Tasa de fotogramas con decimal</string>
<string name="hud_metric_low1">1% más bajo</string>
<string name="hud_metric_low01">0.1% más bajo</string>
<string name="hud_metric_low001">0.01% más bajo</string>
<string name="hud_metric_low1" formatted="false">1% más bajo</string>
<string name="hud_metric_low01" formatted="false">0.1% más bajo</string>
<string name="hud_metric_low001" formatted="false">0.01% más bajo</string>
<string name="hud_metric_engine">Motor gráfico</string>
<string name="hud_metric_gpu_model">Nombre de la GPU</string>
<string name="hud_metric_gpu">Carga de la GPU</string>
@@ -1769,7 +1769,7 @@
<item quantity="other">%1$d elementos copiados</item>
<item quantity="many">%1$d elementos copiados</item>
</plurals>
<string name="fm_already_exists">\\"%1$s\\" ya existe</string>
<string name="fm_already_exists">\"%1$s\" ya existe</string>
<string name="fm_op_renaming">Renombrando...</string>
<string name="fm_rename_failed">No se pudo renombrar</string>
<string name="fm_op_creating_folder">Creando carpeta...</string>
@@ -1851,7 +1851,7 @@
<string name="fm_rail_session_logs">Registros de sesión</string>
<string name="fm_rail_pictures">Imágenes</string>
<string name="fm_favorite_added">Se agregó \"%1$s\" a Favoritos</string>
<string name="fm_favorite_removed">Se quitó \\"%1$s\\" de Favoritos</string>
<string name="fm_favorite_removed">Se quitó \"%1$s\" de Favoritos</string>
<string name="fm_empty_directory">Carpeta vacía</string>
<string name="fm_up_one_level">Subir un nivel</string>
<string name="fm_choose_folder">Elige una carpeta</string>
@@ -3564,7 +3564,7 @@
<string name="integration_shortcuts_maybe_restricted">¿Aparece en gris en Android? Información de la app → ⋮ → Permitir configuración restringida</string>
<string name="integration_shortcuts_restricted">Bloqueado por Android · Información de la app → ⋮ → Permitir configuración restringida</string>
<string name="integration_shortcuts_service">Teclas del sistema de DroidDeck</string>
<string name="integration_shortcuts_service_description">Mantiene Alt+Tab, la tecla Win y el botón Inicio dentro de una sesión activa de DroidDeck en lugar de Android.</string>
<string name="integration_shortcuts_service_description">Mantiene Alt+Tab, la tecla Win y el botón Inicio/Guía dentro de una sesión de DroidDeck activa y detecta apps en la pantalla inferior. Mientras la sesión está activa lee las teclas de un teclado físico, y lee nombres de paquetes de apps y límites de ventanas solo en este dispositivo. No lee texto, no captura la pantalla ni envía estos datos.</string>
<string name="integration_brightness_needed">Activa Brillo en Configuración → Integración con Steam</string>
<string name="setup_gpu_bridge_off">Puente Mali: desactivado</string>
<string name="setup_gpu_bridge_on">Puente Mali: activado</string>
@@ -3659,6 +3659,52 @@
<string name="gog_repair_action">Reparar archivos</string>
<string name="gog_refresh_action">Renovar la instalación</string>
<string name="gog_select_patch">Seleccionar</string>
<string name="setup_all_files">Acceso a todos los archivos</string>
<string name="setup_local_network">Red local</string>
<string name="libraries_title">Más bibliotecas de Steam</string>
<string name="libraries_hint">Carpetas donde Steam puede instalar y ejecutar juegos, cada una con su propio nombre. Quitarlas conserva los juegos en el disco.</string>
<string name="libraries_add">Agregar biblioteca</string>
<string name="libraries_rename_title">Nombre de la biblioteca</string>
<string name="libraries_pick">Elige una carpeta para la biblioteca</string>
<string name="libraries_full">Como máximo %1$d bibliotecas adicionales</string>
<string name="libraries_overlap">Esa carpeta ya es una biblioteca o está dentro de una</string>
<string name="libraries_missing">%1$s · no disponible</string>
<string name="libraries_applies_next">Se aplica a partir de la próxima sesión de Steam</string>
<string name="bottom_desktop_start">Iniciar el escritorio</string>
<string name="bottom_desktop_mode_touch">Toque directo</string>
<string name="bottom_desktop_trackpad">Panel táctil. Toca para usar el toque directo</string>
<string name="bottom_desktop_direct_touch">Toque directo. Toca para usar el panel táctil</string>
<string name="bottom_desktop_starting">Iniciando el escritorio KDE…</string>
<string name="bottom_desktop_ended">El escritorio se cerró.</string>
<string name="bottom_desktop_failed">El escritorio no pudo iniciarse. Sus registros están en la carpeta bottom-desktop de esta sesión.</string>
<string name="bottom_desktop_off">El escritorio está cerrado.</string>
<string name="libraries_rename">Renombrar</string>
<string name="second_mode_desktop">Escritorio</string>
<string name="bottom_desktop_mode_trackpad">Trackpad</string>
<string name="bottom_desktop_title">Escritorio · %1$s</string>
<string name="setup_dual_screen_launcher">Lanzador en dos pantallas</string>
<string name="setup_dual_screen_launcher_hint">Automático coloca las secciones del lanzador (Steam, Biblioteca, Tiendas y otras) en la segunda pantalla integrada. Activado usa cualquier segunda pantalla. Desactivado mantiene el lanzador en una sola pantalla.</string>
<string name="setup_dual_screen_sessions">Sesiones en dos pantallas</string>
<string name="setup_dual_screen_sessions_hint">Automático coloca el menú del juego en la segunda pantalla integrada en lugar de superponerlo. Activado usa cualquier segunda pantalla. Desactivado mantiene el menú en la pantalla del juego; aún puedes elegir una segunda pantalla desde él.</string>
<string name="stats_hud_frametime">Tiempo de fotograma</string>
<string name="stats_hud_last_seconds">últimos %1$d s</string>
<string name="stats_hud_avg_frame">fotograma medio</string>
<string name="stats_hud_low" formatted="false">1 % más bajo</string>
<string name="stats_hud_slow_frames">fotogramas de más de 33 ms</string>
<string name="stats_hud_per_minute">%1$d / min</string>
<string name="stats_hud_playing_for">Tiempo de juego: %1$s</string>
<string name="stats_hud_core_group">%1$d núcleos, hasta %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d núcleos</string>
<string name="stats_hud_fps_of">FPS de %1$d Hz</string>
<string name="stats_hud_red_mark">La marca roja indica dónde el HUD muestra la temperatura en rojo.</string>
<string name="stats_hud_whole_device">todo el dispositivo</string>
<string name="bottom_hud_group">HUD de la pantalla inferior</string>
<string name="bottom_strip_tabs">Franja en cada pestaña</string>
<string name="bottom_strip_lows" formatted="false">1 % y 0,1 % más bajos</string>
<string name="bottom_strip_cpu_temp">Temperatura de CPU</string>
<string name="stats_hud_battery_temp">Batería</string>
<string name="stats_hud_game_cores">Juego</string>
<string name="setup_privacy_policy">Política de privacidad</string>
<string name="stores_gamejolt_not_owned">No está en tu biblioteca de Game Jolt</string>
<string name="stores_gamejolt_no_build">Este juego no tiene descarga para Windows</string>
<string name="stores_gamejolt_no_exe">No se encontró un juego de Windows en la descarga</string>
@@ -3688,4 +3734,81 @@
<string name="stores_gamejolt_filter_status">Estado</string>
<string name="stores_gamejolt_filter_partners">Socios</string>
<string name="stores_gamejolt_followed">Juegos seguidos</string>
<string name="cmgr_powervr_dxvk_managed">DXVK lo proporciona el runtime PowerVR del sistema. Se pueden guardar otros paquetes de DXVK, pero no se aplican con este controlador.</string>
<string name="cmgr_powervr_dxvk_queued">En cola: %1$s (esperando a otro controlador de runtime)</string>
<string name="diagnostics_share_all">Todos los paquetes</string>
<string name="diagnostics_share_games">Juegos y compatibilidad</string>
<string name="diagnostics_share_graphics">Gráficos</string>
<string name="diagnostics_share_session">Sesión y dispositivo</string>
<string name="diagnostics_share_title">Compartir paquetes de diagnóstico</string>
<string name="drivers_built_on">compilado %1$s</string>
<string name="drivers_mali_bridge_auto">Aún no hay un controlador abierto para esta Mali: el puente Mali está activado.</string>
<string name="drivers_mali_bridge_fallback">%1$s aún no está publicado: el puente Mali está activado hasta que lo esté.</string>
<string name="drivers_mali_no_pair_auto">Aún no hay un controlador abierto para esta Mali: se usa %1$s, con el controlador propio del teléfono.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s no está disponible y el controlador de este teléfono indica Vulkan %2$s (Venus necesita 1.2): se usa el puente Mali.</string>
<string name="drivers_mali_unpublished_auto">%1$s aún no está publicado: se usa %2$s hasta que lo esté.</string>
<string name="drivers_pair_updated">%1$s actualizado a la compilación más reciente</string>
<string name="drivers_powervr_builtin">El controlador PowerVR del sistema está seleccionado para el runtime y la pantalla.</string>
<string name="drivers_route_bridge">el puente Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Usa los controladores integrados para esta GPU</string>
<string name="gpu_system_powervr">PowerVR del sistema</string>
<string name="gpuinfo_bridge">Funciona mediante el puente Mali, con el controlador Vulkan propio del teléfono</string>
<string name="gpuinfo_bridge_off">No compatible con el puente Mali desactivado: Turnip necesita una GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">Controlador PowerVR del sistema: la compatibilidad varía según el dispositivo</string>
<string name="gpuinfo_xclipse">Experimental: RADV o el controlador Xclipse del sistema</string>
<string name="libraries_access_allow">Permitir</string>
<string name="libraries_access_hint">Permite el acceso a todos los archivos para que DroidDeck vea las bibliotecas de Steam que otras apps guardan en el almacenamiento interno</string>
<string name="libraries_access_title">Buscar bibliotecas en el almacenamiento interno</string>
<string name="libraries_add_found">Agregar</string>
<string name="libraries_found_hint">Encontrada en este dispositivo · %1$s · %2$s</string>
<string name="libraries_used_inner">Se agregó %1$s, la biblioteca de Steam dentro de la carpeta elegida</string>
<string name="loadstate_steam_update_applying">Aplicando la actualización del cliente de Steam…</string>
<string name="pckb_group_arrows">Teclas de flecha</string>
<string name="pckb_group_function">Esc y teclas de función</string>
<string name="pckb_group_nav">Ins / Supr / Inicio / Fin / Re Pág / Av Pág</string>
<string name="pckb_hidden_keys">Teclas ocultas</string>
<string name="pckb_hidden_keys_hint">Las teclas que casi no usas quedan fuera del teclado.</string>
<string name="pckb_opacity">Opacidad</string>
<string name="pckb_opacity_hint">Redúcela para ver el juego detrás del teclado.</string>
<string name="pckb_size">Tamaño en pantalla</string>
<string name="pckb_size_hint">Parte de la pantalla que ocupa el teclado, con el encabezado incluido.</string>
<string name="session_end_present_fault">La sesión no pudo dibujar en la pantalla: %1$s. Esta pantalla muestra el motivo en lugar de una negra; los registros de la sesión tienen los detalles.</string>
<string name="session_killed_for_memory">La última sesión terminó porque Android cerró DroidDeck para liberar memoria, no por un fallo. Cierra otras apps o baja los ajustes de texturas del juego y vuelve a intentarlo.</string>
<string name="setup_enhanced_diagnostics">Diagnóstico avanzado para la próxima sesión</string>
<string name="setup_enhanced_diagnostics_hint">Captura registros detallados de una sesión. Puede reducir el rendimiento y usar más almacenamiento. Se desactiva solo al iniciar la sesión; revisa los registros antes de compartirlos.</string>
<string name="setup_gpu_bridge_fell_back">El puente Mali no superó su autoprueba en %1$s la última sesión (imágenes o dibujado), así que la sesión usó el par de controladores PanVK.</string>
<string name="setup_gpu_bridge_no_memory_sharing">El puente Mali no encontró ninguna forma de compartir memoria con el controlador en %1$s la última sesión, así que la sesión usó el par de controladores PanVK. El puente no puede funcionar en este teléfono; si lo desactivas, se mantiene el par PanVK.</string>
<string name="setup_gpu_bridge_unusable">%1$s no puede usar el puente Mali (necesita una GPU Mali sin ruta por controlador del sistema), así que este interruptor no tiene efecto aquí.</string>
<string name="setup_gpu_bridged">GPU mediante puente Mali</string>
<string name="setup_gpu_venus_host_too_old">El controlador de %1$s indica Vulkan %2$s. Mali (Venus) necesita Vulkan 1.2 o posterior (gamescope no inicia con versiones anteriores), así que las sesiones omiten Venus y usan el par de controladores PanVK.</string>
<string name="setup_gpu_venus_off">Mali (Venus): desactivado</string>
<string name="setup_gpu_venus_on">Mali (Venus): activado</string>
<string name="setup_graphics_logs">Registros gráficos detallados</string>
<string name="setup_graphics_logs_hint">Los registros de Proton, DXVK y del controlador de cada juego en los registros de la sesión. Siempre activado si la GPU no es una Adreno.</string>
<string name="setup_log_current">Sesión actual</string>
<string name="setup_log_delete">Eliminar</string>
<string name="setup_log_delete_failed">No se pudieron eliminar los registros de la sesión.</string>
<string name="setup_log_delete_message">¿Eliminar %1$s? Esta acción no se puede deshacer.</string>
<string name="setup_log_delete_title">¿Eliminar los registros de la sesión?</string>
<string name="setup_log_empty">Aún no hay registros de sesión guardados.</string>
<string name="setup_log_export_failed">No se pudieron guardar los registros de la sesión.</string>
<string name="setup_log_export_hint">Los ZIP compartidos y guardados incluyen la sesión seleccionada y los diagnósticos relacionados de Steam, la tienda, las herramientas y Battle.net.</string>
<string name="setup_log_export_saved">Registros de la sesión guardados.</string>
<string name="setup_log_save">Guardar en el dispositivo</string>
<string name="setup_log_sessions">Registros de sesión disponibles</string>
<string name="setup_log_share">Compartir</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Interfaz de Steam: dibujado por GPU</string>
<string name="setup_steam_gpu_drawing_hint">Con el par de controladores Xclipse, Steam dibuja sus páginas por software (el dibujado por GPU mostraba mosaicos rotos). Actívalo para una interfaz de Steam más rápida si se ve bien en este teléfono.</string>
<string name="swap_game_screen">Mostrar el juego en</string>
<string name="swap_game_screen_bottom">Pantalla inferior</string>
<string name="swap_game_screen_group">Pantalla del juego</string>
<string name="swap_game_screen_top">Pantalla superior</string>
<string name="swap_screens">Intercambiar pantallas</string>
<string name="swap_screens_unavailable">La pantalla inferior no está libre ahora mismo</string>
<string name="upd_block_non_adreno_test">Esta es la compilación de prueba sin Snapdragon: las compilaciones oficiales solo funcionan aquí en Adreno, así que no se instalan sobre ella. Las nuevas compilaciones de prueba están en github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Actualización en cola.</string>
<string name="wincomp_repair_required">Esta descarga necesita una actualización.</string>
<string name="wincomp_retry">Reintentar actualización</string>
</resources>
+127 -4
View File
@@ -671,9 +671,9 @@
<string name="hud_group_metrics">Показания</string>
<string name="hud_metric_graph">Графика на времето на кадрите</string>
<string name="hud_metric_decimal">Честота на кадрите с десетичен знак</string>
<string name="hud_metric_low1">1% най-ниски</string>
<string name="hud_metric_low01">0.1% най-ниски</string>
<string name="hud_metric_low001">0.01% най-ниски</string>
<string name="hud_metric_low1" formatted="false">1% най-ниски</string>
<string name="hud_metric_low01" formatted="false">0.1% най-ниски</string>
<string name="hud_metric_low001" formatted="false">0.01% най-ниски</string>
<string name="hud_metric_engine">Графичен двигател</string>
<string name="hud_metric_gpu_model">Име на GPU</string>
<string name="hud_metric_gpu">Натоварване на GPU</string>
@@ -3553,7 +3553,7 @@
<string name="integration_shortcuts_maybe_restricted">Неактивно в Android? Информация за приложението → ⋮ → Разрешаване на ограничените настройки</string>
<string name="integration_shortcuts_restricted">Блокирано от Android · Информация за приложението → ⋮ → Разрешаване на ограничените настройки</string>
<string name="integration_shortcuts_service">Системни клавиши на DroidDeck</string>
<string name="integration_shortcuts_service_description">Задържа Alt+Tab, клавиша Win и бутона Начало в активната сесия на DroidDeck вместо в Android.</string>
<string name="integration_shortcuts_service_description">Задържа Alt+Tab, клавиша Win и бутона Начало/Guide в активна сесия на DroidDeck и открива приложения на долния екран. Докато сесията е активна, чете клавиши от физическа клавиатура, както и имена на пакети и граници на прозорци, само на това устройство. Не чете текст, не заснема екрана и не изпраща тези данни.</string>
<string name="integration_brightness_needed">Включете „Яркост“ в Настройка → Интеграция със Steam</string>
<string name="setup_gpu_bridge_off">Mali мост: изкл.</string>
<string name="setup_gpu_bridge_on">Mali мост: вкл.</string>
@@ -3647,6 +3647,52 @@
<string name="gog_repair_action">Поправка на файловете</string>
<string name="gog_refresh_action">Обновяване на инсталацията</string>
<string name="gog_select_patch">Избор</string>
<string name="setup_all_files">Достъп до всички файлове</string>
<string name="setup_local_network">Локална мрежа</string>
<string name="libraries_title">Още библиотеки на Steam</string>
<string name="libraries_hint">Папки, в които Steam може да инсталира и стартира игри, всяка със собствено име. Премахването запазва игрите на диска.</string>
<string name="libraries_add">Добавяне на библиотека</string>
<string name="libraries_rename_title">Име на библиотеката</string>
<string name="libraries_pick">Изберете папка за библиотеката</string>
<string name="libraries_full">Най-много %1$d допълнителни библиотеки</string>
<string name="libraries_overlap">Тази папка вече е библиотека или се намира в такава</string>
<string name="libraries_missing">%1$s · недостъпна</string>
<string name="libraries_applies_next">Прилага се от следващата сесия на Steam</string>
<string name="bottom_desktop_start">Стартиране на работния плот</string>
<string name="bottom_desktop_mode_touch">Директно докосване</string>
<string name="bottom_desktop_trackpad">Тракпад. Докоснете за директно докосване</string>
<string name="bottom_desktop_direct_touch">Директно докосване. Докоснете за тракпад</string>
<string name="bottom_desktop_starting">Стартиране на работния плот KDE…</string>
<string name="bottom_desktop_ended">Работният плот се затвори.</string>
<string name="bottom_desktop_failed">Работният плот не можа да стартира. Журналите са в папката bottom-desktop на тази сесия.</string>
<string name="bottom_desktop_off">Работният плот е затворен.</string>
<string name="libraries_rename">Преименуване</string>
<string name="second_mode_desktop">Работен плот</string>
<string name="bottom_desktop_mode_trackpad">Тракпад</string>
<string name="bottom_desktop_title">Работен плот · %1$s</string>
<string name="setup_dual_screen_launcher">Стартер на два екрана</string>
<string name="setup_dual_screen_launcher_hint">Автоматично поставя разделите на стартера (Steam, Библиотека, Магазини и други) на вградения втори екран. Включено използва всеки втори екран. Изключено оставя стартера на един екран.</string>
<string name="setup_dual_screen_sessions">Сесии на два екрана</string>
<string name="setup_dual_screen_sessions_hint">Автоматично поставя менюто на играта на вградения втори екран вместо като наслагване. Включено използва всеки втори екран. Изключено оставя менюто на екрана на играта; от него все още може да изберете втори екран.</string>
<string name="stats_hud_frametime">Време за кадър</string>
<string name="stats_hud_last_seconds">последни %1$d сек</string>
<string name="stats_hud_avg_frame">среден кадър</string>
<string name="stats_hud_low" formatted="false">най-ниски 1 %</string>
<string name="stats_hud_slow_frames">кадри над 33 мс</string>
<string name="stats_hud_per_minute">%1$d / мин</string>
<string name="stats_hud_playing_for">Време в игра: %1$s</string>
<string name="stats_hud_core_group">%1$d ядра, до %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d ядра</string>
<string name="stats_hud_fps_of">FPS при %1$d Hz</string>
<string name="stats_hud_red_mark">Червеният маркер показва къде HUD изобразява температурата в червено.</string>
<string name="stats_hud_whole_device">цялото устройство</string>
<string name="bottom_hud_group">HUD на долния екран</string>
<string name="bottom_strip_tabs">Лента във всеки раздел</string>
<string name="bottom_strip_lows" formatted="false">най-ниски 1 % и 0,1 %</string>
<string name="bottom_strip_cpu_temp">Температура на CPU</string>
<string name="stats_hud_battery_temp">Батерия</string>
<string name="stats_hud_game_cores">Игра</string>
<string name="setup_privacy_policy">Политика за поверителност</string>
<string name="stores_gamejolt_not_owned">Не е в библиотеката ви в Game Jolt</string>
<string name="stores_gamejolt_no_build">Няма изтегляне за Windows за тази игра</string>
<string name="stores_gamejolt_no_exe">В изтеглянето не е намерена игра за Windows</string>
@@ -3676,4 +3722,81 @@
<string name="stores_gamejolt_filter_status">Статус</string>
<string name="stores_gamejolt_filter_partners">Партньори</string>
<string name="stores_gamejolt_followed">Следвани игри</string>
<string name="cmgr_powervr_dxvk_managed">DXVK се предоставя от системната среда PowerVR. Други пакети DXVK могат да се съхраняват, но с този драйвер не се прилагат.</string>
<string name="cmgr_powervr_dxvk_queued">На опашка: %1$s (изчаква се друг драйвер на средата за изпълнение)</string>
<string name="diagnostics_share_all">Всички пакети</string>
<string name="diagnostics_share_games">Игри и съвместимост</string>
<string name="diagnostics_share_graphics">Графика</string>
<string name="diagnostics_share_session">Сесия и устройство</string>
<string name="diagnostics_share_title">Споделяне на диагностични пакети</string>
<string name="drivers_built_on">компилация от %1$s</string>
<string name="drivers_mali_bridge_auto">Все още няма отворен драйвер за този Mali: мостът Mali е включен.</string>
<string name="drivers_mali_bridge_fallback">%1$s още не е публикуван: мостът Mali е включен, докато не бъде.</string>
<string name="drivers_mali_no_pair_auto">Все още няма отворен драйвер за този Mali: използва се %1$s с драйвера на самия телефон.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s не е наличен, а драйверът на този телефон отчита Vulkan %2$s (Venus изисква 1.2): използва се мостът Mali.</string>
<string name="drivers_mali_unpublished_auto">%1$s още не е публикуван: дотогава се използва %2$s.</string>
<string name="drivers_pair_updated">%1$s е обновен до най-новата компилация</string>
<string name="drivers_powervr_builtin">Системният драйвер PowerVR е избран за средата за изпълнение и дисплея.</string>
<string name="drivers_route_bridge">мостът Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Използва вградените драйвери за този GPU</string>
<string name="gpu_system_powervr">Системен PowerVR</string>
<string name="gpuinfo_bridge">Работи през моста Mali, със собствения Vulkan драйвер на телефона</string>
<string name="gpuinfo_bridge_off">Не се поддържа при изключен мост Mali: Turnip изисква GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">Системен драйвер PowerVR: съвместимостта зависи от устройството</string>
<string name="gpuinfo_xclipse">Експериментално: RADV или системният драйвер Xclipse</string>
<string name="libraries_access_allow">Разреши</string>
<string name="libraries_access_hint">Разрешете достъп до всички файлове, за да вижда DroidDeck библиотеките на Steam, които други приложения държат във вътрешната памет</string>
<string name="libraries_access_title">Търсене на библиотеки във вътрешната памет</string>
<string name="libraries_add_found">Добави</string>
<string name="libraries_found_hint">Намерено на това устройство · %1$s · %2$s</string>
<string name="libraries_used_inner">Добавена е %1$s, библиотеката на Steam в избраната папка</string>
<string name="loadstate_steam_update_applying">Прилагане на обновлението на клиента на Steam…</string>
<string name="pckb_group_arrows">Клавиши със стрелки</string>
<string name="pckb_group_function">Esc и функционален ред</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">Скрити клавиши</string>
<string name="pckb_hidden_keys_hint">Рядко използваните клавиши не се показват на клавиатурата.</string>
<string name="pckb_opacity">Непрозрачност</string>
<string name="pckb_opacity_hint">Намалете я, за да виждате играта зад клавиатурата.</string>
<string name="pckb_size">Размер на екрана</string>
<string name="pckb_size_hint">Дял от екрана, който заема клавиатурата, заедно със заглавната лента.</string>
<string name="session_end_present_fault">Сесията не успя да изобрази на екрана: %1$s. Този екран показва причината вместо черен екран; подробности има в дневниците на сесията.</string>
<string name="session_killed_for_memory">Последната сесия приключи, защото Android затвори DroidDeck, за да освободи памет, а не заради срив. Затворете други приложения или намалете настройките на текстурите в играта и опитайте отново.</string>
<string name="setup_enhanced_diagnostics">Разширена диагностика за следващата сесия</string>
<string name="setup_enhanced_diagnostics_hint">Записва подробни дневници за една сесия. Може да намали производителността и да заеме повече място. Изключва се сама при стартиране на сесията; прегледайте дневниците преди споделяне.</string>
<string name="setup_gpu_bridge_fell_back">Мостът Mali не издържа самотестването на %1$s през последната сесия (изображения или чертане), затова сесията използва двойката драйвери PanVK.</string>
<string name="setup_gpu_bridge_no_memory_sharing">Мостът Mali не намери начин да споделя памет с драйвера на %1$s през последната сесия, затова сесията използва двойката драйвери PanVK. Мостът не може да работи на този телефон; изключването му запазва двойката PanVK.</string>
<string name="setup_gpu_bridge_unusable">%1$s не може да използва моста Mali (изисква GPU Mali без маршрут през системен драйвер), затова този превключвател тук няма ефект.</string>
<string name="setup_gpu_bridged">GPU през моста Mali</string>
<string name="setup_gpu_venus_host_too_old">Драйверът на %1$s отчита Vulkan %2$s. Mali (Venus) изисква Vulkan 1.2 или по-нов (gamescope не стартира на по-стар), затова сесиите пропускат Venus и използват двойката драйвери PanVK.</string>
<string name="setup_gpu_venus_off">Mali (Venus): изкл.</string>
<string name="setup_gpu_venus_on">Mali (Venus): вкл.</string>
<string name="setup_graphics_logs">Подробни графични дневници</string>
<string name="setup_graphics_logs_hint">Дневниците на Proton, DXVK и драйвера на всяка игра в дневниците на сесията. Винаги включено, когато GPU не е Adreno.</string>
<string name="setup_log_current">Текуща сесия</string>
<string name="setup_log_delete">Изтрий</string>
<string name="setup_log_delete_failed">Дневниците на сесията не можаха да бъдат изтрити.</string>
<string name="setup_log_delete_message">Да се изтрие ли %1$s? Това действие не може да бъде отменено.</string>
<string name="setup_log_delete_title">Изтриване на дневниците на сесията?</string>
<string name="setup_log_empty">Все още няма запазени дневници на сесии.</string>
<string name="setup_log_export_failed">Дневниците на сесията не можаха да бъдат запазени.</string>
<string name="setup_log_export_hint">Споделените и запазените ZIP файлове включват избраната сесия и свързана диагностика на Steam, магазини, инструменти и Battle.net.</string>
<string name="setup_log_export_saved">Дневниците на сесията са запазени.</string>
<string name="setup_log_save">Запази на устройството</string>
<string name="setup_log_sessions">Налични дневници на сесии</string>
<string name="setup_log_share">Сподели</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Интерфейс на Steam: чертане с GPU</string>
<string name="setup_steam_gpu_drawing_hint">С двойката драйвери Xclipse Steam чертае страниците си софтуерно (чертането с GPU показваше счупени плочки). Включете за по-бърз интерфейс на Steam, ако изглежда правилно на този телефон.</string>
<string name="swap_game_screen">Показване на играта на</string>
<string name="swap_game_screen_bottom">Долния екран</string>
<string name="swap_game_screen_group">Екран на играта</string>
<string name="swap_game_screen_top">Горния екран</string>
<string name="swap_screens">Размяна на екраните</string>
<string name="swap_screens_unavailable">Долният екран не е свободен в момента</string>
<string name="upd_block_non_adreno_test">Това е тестовата компилация без Snapdragon: официалните компилации тук работят само на Adreno, затова не се инсталират върху нея. Новите тестови компилации са на github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Обновлението е на опашка.</string>
<string name="wincomp_repair_required">Това изтегляне се нуждае от обновяване.</string>
<string name="wincomp_retry">Повтори обновяването</string>
</resources>
+127 -4
View File
@@ -675,9 +675,9 @@
<string name="hud_group_metrics">Údaje</string>
<string name="hud_metric_graph">Graf časů snímků</string>
<string name="hud_metric_decimal">Snímková frekvence s desetinným místem</string>
<string name="hud_metric_low1">1% nejnižší</string>
<string name="hud_metric_low01">0.1% nejnižší</string>
<string name="hud_metric_low001">0.01% nejnižší</string>
<string name="hud_metric_low1" formatted="false">1% nejnižší</string>
<string name="hud_metric_low01" formatted="false">0.1% nejnižší</string>
<string name="hud_metric_low001" formatted="false">0.01% nejnižší</string>
<string name="hud_metric_engine">Grafický engine</string>
<string name="hud_metric_gpu_model">Název GPU</string>
<string name="hud_metric_gpu">Vytížení GPU</string>
@@ -3629,7 +3629,7 @@
<string name="integration_shortcuts_maybe_restricted">V Androidu zašedlé? Informace o aplikaci → ⋮ → Povolit omezená nastavení</string>
<string name="integration_shortcuts_restricted">Blokováno Androidem · Informace o aplikaci → ⋮ → Povolit omezená nastavení</string>
<string name="integration_shortcuts_service">Systémové klávesy DroidDeck</string>
<string name="integration_shortcuts_service_description">Ponechá Alt+Tab, klávesu Win a tlačítko Domů v aktivní relaci DroidDeck místo Androidu.</string>
<string name="integration_shortcuts_service_description">Udržuje Alt+Tab, klávesu Win a tlačítko Domů/Guide uvnitř aktivní relace DroidDeck a rozpoznává aplikace na spodní obrazovce. Během aktivní relace čte klávesy z fyzické klávesnice a názvy balíčků aplikací a hranice oken, pouze v tomto zařízení. Nečte text, nezachytává obrazovku ani tato data neodesílá.</string>
<string name="integration_brightness_needed">Zapněte Jas v Příprava → Integrace se Steamem</string>
<string name="setup_gpu_bridge_off">Most Mali: vypnuto</string>
<string name="setup_gpu_bridge_on">Most Mali: zapnuto</string>
@@ -3725,6 +3725,52 @@
<string name="gog_repair_action">Opravit soubory</string>
<string name="gog_refresh_action">Obnovit instalaci</string>
<string name="gog_select_patch">Vybrat</string>
<string name="setup_all_files">Přístup ke všem souborům</string>
<string name="setup_local_network">Místní síť</string>
<string name="libraries_title">Další knihovny Steamu</string>
<string name="libraries_hint">Složky, do kterých může Steam instalovat hry a spouštět je, každá s vlastním názvem. Odebrání zachová hry na disku.</string>
<string name="libraries_add">Přidat knihovnu</string>
<string name="libraries_rename_title">Název knihovny</string>
<string name="libraries_pick">Vyberte složku knihovny</string>
<string name="libraries_full">Nejvýše %1$d dalších knihoven</string>
<string name="libraries_overlap">Tato složka už je knihovnou nebo se v ní nachází</string>
<string name="libraries_missing">%1$s · nedostupná</string>
<string name="libraries_applies_next">Platí od příští relace Steamu</string>
<string name="bottom_desktop_start">Spustit plochu</string>
<string name="bottom_desktop_mode_touch">Přímý dotyk</string>
<string name="bottom_desktop_trackpad">Touchpad. Klepnutím přepnete na přímý dotyk</string>
<string name="bottom_desktop_direct_touch">Přímý dotyk. Klepnutím přepnete na touchpad</string>
<string name="bottom_desktop_starting">Spouštění plochy KDE…</string>
<string name="bottom_desktop_ended">Plocha byla zavřena.</string>
<string name="bottom_desktop_failed">Plochu se nepodařilo spustit. Protokoly jsou ve složce bottom-desktop této relace.</string>
<string name="bottom_desktop_off">Plocha je zavřená.</string>
<string name="libraries_rename">Přejmenovat</string>
<string name="second_mode_desktop">Plocha</string>
<string name="bottom_desktop_mode_trackpad">Trackpad</string>
<string name="bottom_desktop_title">Plocha · %1$s</string>
<string name="setup_dual_screen_launcher">Spouštěč na dvou obrazovkách</string>
<string name="setup_dual_screen_launcher_hint">Automaticky umístí části spouštěče (Steam, Knihovna, Obchody a další) na vestavěnou druhou obrazovku. Zapnuto použije libovolnou druhou obrazovku. Vypnuto ponechá spouštěč na jedné obrazovce.</string>
<string name="setup_dual_screen_sessions">Relace na dvou obrazovkách</string>
<string name="setup_dual_screen_sessions_hint">Automaticky umístí herní nabídku na vestavěnou druhou obrazovku místo překrytí. Zapnuto použije libovolnou druhou obrazovku. Vypnuto ponechá nabídku na herní obrazovce; stále z ní můžete vybrat druhou obrazovku.</string>
<string name="stats_hud_frametime">Doba snímku</string>
<string name="stats_hud_last_seconds">posledních %1$d s</string>
<string name="stats_hud_avg_frame">průměrný snímek</string>
<string name="stats_hud_low" formatted="false">nejnižší 1 %</string>
<string name="stats_hud_slow_frames">snímky nad 33 ms</string>
<string name="stats_hud_per_minute">%1$d / min</string>
<string name="stats_hud_playing_for">Doba hraní: %1$s</string>
<string name="stats_hud_core_group">%1$d jader, až %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d jader</string>
<string name="stats_hud_fps_of">FPS při %1$d Hz</string>
<string name="stats_hud_red_mark">Červená značka ukazuje, kde HUD zobrazí teplotu červeně.</string>
<string name="stats_hud_whole_device">celé zařízení</string>
<string name="bottom_hud_group">HUD spodní obrazovky</string>
<string name="bottom_strip_tabs">Lišta na každé kartě</string>
<string name="bottom_strip_lows" formatted="false">nejnižší 1 % a 0,1 %</string>
<string name="bottom_strip_cpu_temp">Teplota CPU</string>
<string name="stats_hud_battery_temp">Baterie</string>
<string name="stats_hud_game_cores">Hra</string>
<string name="setup_privacy_policy">Zásady ochrany osobních údajů</string>
<string name="stores_gamejolt_not_owned">Není ve vaší knihovně Game Jolt</string>
<string name="stores_gamejolt_no_build">Pro tuto hru není ke stažení verze pro Windows</string>
<string name="stores_gamejolt_no_exe">Ve stahování nebyla nalezena hra pro Windows</string>
@@ -3754,4 +3800,81 @@
<string name="stores_gamejolt_filter_status">Stav</string>
<string name="stores_gamejolt_filter_partners">Partneři</string>
<string name="stores_gamejolt_followed">Sledované hry</string>
<string name="cmgr_powervr_dxvk_managed">DXVK poskytuje systémové běhové prostředí PowerVR. Další balíčky DXVK lze uložit, ale s tímto ovladačem se nepoužijí.</string>
<string name="cmgr_powervr_dxvk_queued">Ve frontě: %1$s (čeká se na jiný ovladač běhového prostředí)</string>
<string name="diagnostics_share_all">Všechny balíčky</string>
<string name="diagnostics_share_games">Hry a kompatibilita</string>
<string name="diagnostics_share_graphics">Grafika</string>
<string name="diagnostics_share_session">Relace a zařízení</string>
<string name="diagnostics_share_title">Sdílet diagnostické balíčky</string>
<string name="drivers_built_on">sestaveno %1$s</string>
<string name="drivers_mali_bridge_auto">Pro toto Mali zatím neexistuje otevřený ovladač: most Mali je zapnutý.</string>
<string name="drivers_mali_bridge_fallback">%1$s ještě není zveřejněn: most Mali je zapnutý, dokud nebude.</string>
<string name="drivers_mali_no_pair_auto">Pro toto Mali zatím neexistuje otevřený ovladač: používá se %1$s na vlastním ovladači telefonu.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s není k dispozici a ovladač tohoto telefonu hlásí Vulkan %2$s (Venus vyžaduje 1.2): používá se most Mali.</string>
<string name="drivers_mali_unpublished_auto">%1$s ještě není zveřejněn: do té doby se používá %2$s.</string>
<string name="drivers_pair_updated">%1$s aktualizován na nejnovější sestavení</string>
<string name="drivers_powervr_builtin">Pro běhové prostředí a displej je vybrán systémový ovladač PowerVR.</string>
<string name="drivers_route_bridge">most Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Používá vestavěné ovladače pro toto GPU</string>
<string name="gpu_system_powervr">Systémový PowerVR</string>
<string name="gpuinfo_bridge">Běží přes most Mali, na vlastním ovladači Vulkan telefonu</string>
<string name="gpuinfo_bridge_off">Nepodporováno s vypnutým mostem Mali: Turnip vyžaduje GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">Systémový ovladač PowerVR: kompatibilita se liší podle zařízení</string>
<string name="gpuinfo_xclipse">Experimentální: RADV nebo systémový ovladač Xclipse</string>
<string name="libraries_access_allow">Povolit</string>
<string name="libraries_access_hint">Povolte přístup ke všem souborům, aby DroidDeck viděl knihovny Steam, které jiné aplikace ukládají do interního úložiště</string>
<string name="libraries_access_title">Najít knihovny v interním úložišti</string>
<string name="libraries_add_found">Přidat</string>
<string name="libraries_found_hint">Nalezeno v tomto zařízení · %1$s · %2$s</string>
<string name="libraries_used_inner">Přidána %1$s, knihovna Steam uvnitř vybrané složky</string>
<string name="loadstate_steam_update_applying">Použití aktualizace klienta Steam…</string>
<string name="pckb_group_arrows">Šipky</string>
<string name="pckb_group_function">Esc a funkční řada</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">Skryté klávesy</string>
<string name="pckb_hidden_keys_hint">Klávesy, které používáte zřídka, se na klávesnici nezobrazují.</string>
<string name="pckb_opacity">Průhlednost</string>
<string name="pckb_opacity_hint">Snižte ji, abyste viděli hru za klávesnicí.</string>
<string name="pckb_size">Velikost na obrazovce</string>
<string name="pckb_size_hint">Podíl obrazovky, který klávesnice zabírá, včetně záhlaví.</string>
<string name="session_end_present_fault">Relace nemohla kreslit na obrazovku: %1$s. Tato obrazovka místo černé zobrazuje důvod; podrobnosti jsou v protokolech relace.</string>
<string name="session_killed_for_memory">Poslední relace skončila, protože Android zavřel DroidDeck kvůli uvolnění paměti, nikoli kvůli pádu. Zavřete jiné aplikace nebo snižte nastavení textur ve hře a zkuste to znovu.</string>
<string name="setup_enhanced_diagnostics">Rozšířená diagnostika pro příští relaci</string>
<string name="setup_enhanced_diagnostics_hint">Zaznamenává podrobné protokoly po dobu jedné relace. Může snížit výkon a zabrat více místa. Při spuštění relace se sama vypne; před sdílením protokoly zkontrolujte.</string>
<string name="setup_gpu_bridge_fell_back">Most Mali v poslední relaci neprošel autotestem na %1$s (obrázky nebo vykreslování), proto relace použila dvojici ovladačů PanVK.</string>
<string name="setup_gpu_bridge_no_memory_sharing">Most Mali v poslední relaci nenašel způsob sdílení paměti s ovladačem na %1$s, proto relace použila dvojici ovladačů PanVK. Most na tomto telefonu nemůže fungovat; po jeho vypnutí zůstane dvojice PanVK.</string>
<string name="setup_gpu_bridge_unusable">%1$s nemůže používat most Mali (vyžaduje GPU Mali bez cesty přes systémový ovladač), proto tento přepínač zde nemá žádný účinek.</string>
<string name="setup_gpu_bridged">GPU přes most Mali</string>
<string name="setup_gpu_venus_host_too_old">Ovladač na %1$s hlásí Vulkan %2$s. Mali (Venus) vyžaduje Vulkan 1.2 nebo novější (gamescope na starším nespustí), proto relace Venus přeskočí a použijí dvojici ovladačů PanVK.</string>
<string name="setup_gpu_venus_off">Mali (Venus): vypnuto</string>
<string name="setup_gpu_venus_on">Mali (Venus): zapnuto</string>
<string name="setup_graphics_logs">Podrobné grafické protokoly</string>
<string name="setup_graphics_logs_hint">Protokoly Proton, DXVK a ovladače každé hry v protokolech relace. Vždy zapnuto, pokud GPU není Adreno.</string>
<string name="setup_log_current">Aktuální relace</string>
<string name="setup_log_delete">Smazat</string>
<string name="setup_log_delete_failed">Protokoly relace se nepodařilo smazat.</string>
<string name="setup_log_delete_message">Smazat %1$s? Tuto akci nelze vrátit zpět.</string>
<string name="setup_log_delete_title">Smazat protokoly relace?</string>
<string name="setup_log_empty">Zatím žádné uložené protokoly relací.</string>
<string name="setup_log_export_failed">Protokoly relace se nepodařilo uložit.</string>
<string name="setup_log_export_hint">Sdílené a uložené soubory ZIP obsahují vybranou relaci a související diagnostiku Steam, obchodů, nástrojů a Battle.net.</string>
<string name="setup_log_export_saved">Protokoly relace uloženy.</string>
<string name="setup_log_save">Uložit do zařízení</string>
<string name="setup_log_sessions">Dostupné protokoly relací</string>
<string name="setup_log_share">Sdílet</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Rozhraní Steam: kreslení na GPU</string>
<string name="setup_steam_gpu_drawing_hint">S dvojicí ovladačů Xclipse Steam kreslí stránky softwarově (kreslení na GPU zobrazovalo poškozené dlaždice). Zapněte pro rychlejší rozhraní Steam, pokud na tomto telefonu vypadá správně.</string>
<string name="swap_game_screen">Zobrazit hru na</string>
<string name="swap_game_screen_bottom">Spodní obrazovce</string>
<string name="swap_game_screen_group">Obrazovka hry</string>
<string name="swap_game_screen_top">Horní obrazovce</string>
<string name="swap_screens">Prohodit obrazovky</string>
<string name="swap_screens_unavailable">Spodní obrazovka teď není volná</string>
<string name="upd_block_non_adreno_test">Toto je testovací sestavení bez Snapdragonu: oficiální sestavení zde běží jen na Adreno, proto se přes něj neinstalují. Nová testovací sestavení jsou na github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Aktualizace ve frontě.</string>
<string name="wincomp_repair_required">Toto stažení vyžaduje aktualizaci.</string>
<string name="wincomp_retry">Zkusit aktualizaci znovu</string>
</resources>
+127 -4
View File
@@ -671,9 +671,9 @@
<string name="hud_group_metrics">Målinger</string>
<string name="hud_metric_graph">Billedtidsgraf</string>
<string name="hud_metric_decimal">Billedhastighed med decimal</string>
<string name="hud_metric_low1">1% laveste</string>
<string name="hud_metric_low01">0.1% laveste</string>
<string name="hud_metric_low001">0.01% laveste</string>
<string name="hud_metric_low1" formatted="false">1% laveste</string>
<string name="hud_metric_low01" formatted="false">0.1% laveste</string>
<string name="hud_metric_low001" formatted="false">0.01% laveste</string>
<string name="hud_metric_engine">Grafikmotor</string>
<string name="hud_metric_gpu_model">GPU-navn</string>
<string name="hud_metric_gpu">GPU-belastning</string>
@@ -3553,7 +3553,7 @@
<string name="integration_shortcuts_maybe_restricted">Nedtonet i Android? Appinfo → ⋮ → Tillad begrænsede indstillinger</string>
<string name="integration_shortcuts_restricted">Blokeret af Android · Appinfo → ⋮ → Tillad begrænsede indstillinger</string>
<string name="integration_shortcuts_service">DroidDeck-systemtaster</string>
<string name="integration_shortcuts_service_description">Holder Alt+Tab, Win-tasten og Hjem-knappen i en aktiv DroidDeck-session i stedet for Android.</string>
<string name="integration_shortcuts_service_description">Holder Alt+Tab, Win-tasten og Hjem/Guide-knappen inde i en aktiv DroidDeck-session og registrerer apps på den nederste skærm. Mens en session er aktiv, læser den taster fra et fysisk tastatur samt apppakkenavne og vinduesgrænser, kun på denne enhed. Den læser ikke tekst, optager ikke skærmen og sender ikke disse data.</string>
<string name="integration_brightness_needed">Slå Lysstyrke til under Opsætning → Steam-integration</string>
<string name="setup_gpu_bridge_off">Mali-bro: fra</string>
<string name="setup_gpu_bridge_on">Mali-bro: til</string>
@@ -3647,6 +3647,52 @@
<string name="gog_repair_action">Reparer filer</string>
<string name="gog_refresh_action">Opdater installationen</string>
<string name="gog_select_patch">Vælg</string>
<string name="setup_all_files">Adgang til alle filer</string>
<string name="setup_local_network">Lokalt netværk</string>
<string name="libraries_title">Flere Steam-biblioteker</string>
<string name="libraries_hint">Mapper, som Steam kan installere og starte spil fra, hver med sit eget navn. Fjernelse bevarer spillene på disken.</string>
<string name="libraries_add">Tilføj bibliotek</string>
<string name="libraries_rename_title">Bibliotekets navn</string>
<string name="libraries_pick">Vælg en mappe til biblioteket</string>
<string name="libraries_full">Højst %1$d ekstra biblioteker</string>
<string name="libraries_overlap">Denne mappe er allerede et bibliotek eller ligger i et</string>
<string name="libraries_missing">%1$s · ikke tilgængeligt</string>
<string name="libraries_applies_next">Gælder fra næste Steam-session</string>
<string name="bottom_desktop_start">Start skrivebordet</string>
<string name="bottom_desktop_mode_touch">Direkte berøring</string>
<string name="bottom_desktop_trackpad">Pegefelt. Tryk for direkte berøring</string>
<string name="bottom_desktop_direct_touch">Direkte berøring. Tryk for pegefelt</string>
<string name="bottom_desktop_starting">Starter KDE-skrivebordet…</string>
<string name="bottom_desktop_ended">Skrivebordet blev lukket.</string>
<string name="bottom_desktop_failed">Skrivebordet kunne ikke starte. Logfilerne findes i denne sessions bottom-desktop-mappe.</string>
<string name="bottom_desktop_off">Skrivebordet er lukket.</string>
<string name="libraries_rename">Omdøb</string>
<string name="second_mode_desktop">Skrivebord</string>
<string name="bottom_desktop_mode_trackpad">Trackpad</string>
<string name="bottom_desktop_title">Skrivebord · %1$s</string>
<string name="setup_dual_screen_launcher">Starter på to skærme</string>
<string name="setup_dual_screen_launcher_hint">Automatisk viser starterens sektioner (Steam, Bibliotek, Butikker og andre) på den indbyggede anden skærm. Til bruger enhver anden skærm. Fra holder starteren på én skærm.</string>
<string name="setup_dual_screen_sessions">Sessioner på to skærme</string>
<string name="setup_dual_screen_sessions_hint">Automatisk viser spilmenuen på den indbyggede anden skærm i stedet for som overlejring. Til bruger enhver anden skærm. Fra holder menuen på spilskærmen; du kan stadig vælge en anden skærm fra den.</string>
<string name="stats_hud_frametime">Billedtid</string>
<string name="stats_hud_last_seconds">seneste %1$d s</string>
<string name="stats_hud_avg_frame">gennemsnitligt billede</string>
<string name="stats_hud_low" formatted="false">laveste 1 %</string>
<string name="stats_hud_slow_frames">billeder over 33 ms</string>
<string name="stats_hud_per_minute">%1$d / min</string>
<string name="stats_hud_playing_for">Spilletid: %1$s</string>
<string name="stats_hud_core_group">%1$d kerner, op til %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d kerner</string>
<string name="stats_hud_fps_of">FPS ved %1$d Hz</string>
<string name="stats_hud_red_mark">Det røde mærke viser, hvor HUD viser temperaturen med rødt.</string>
<string name="stats_hud_whole_device">hele enheden</string>
<string name="bottom_hud_group">HUD på nederste skærm</string>
<string name="bottom_strip_tabs">Stribe på hver fane</string>
<string name="bottom_strip_lows" formatted="false">laveste 1 % og 0,1 %</string>
<string name="bottom_strip_cpu_temp">CPU-temperatur</string>
<string name="stats_hud_battery_temp">Batteri</string>
<string name="stats_hud_game_cores">Spil</string>
<string name="setup_privacy_policy">Privatlivspolitik</string>
<string name="stores_gamejolt_not_owned">Ikke i dit Game Jolt-bibliotek</string>
<string name="stores_gamejolt_no_build">Ingen Windows-download til dette spil</string>
<string name="stores_gamejolt_no_exe">Der blev ikke fundet et Windows-spil i downloaden</string>
@@ -3676,4 +3722,81 @@
<string name="stores_gamejolt_filter_status">Status</string>
<string name="stores_gamejolt_filter_partners">Partnere</string>
<string name="stores_gamejolt_followed">Fulgte spil</string>
<string name="cmgr_powervr_dxvk_managed">DXVK leveres af systemets PowerVR-kørselsmiljø. Andre DXVK-pakker kan gemmes, men anvendes ikke med denne driver.</string>
<string name="cmgr_powervr_dxvk_queued">I kø: %1$s (venter på en anden kørselsmiljødriver)</string>
<string name="diagnostics_share_all">Alle pakker</string>
<string name="diagnostics_share_games">Spil og kompatibilitet</string>
<string name="diagnostics_share_graphics">Grafik</string>
<string name="diagnostics_share_session">Session og enhed</string>
<string name="diagnostics_share_title">Del diagnosticeringspakker</string>
<string name="drivers_built_on">bygget %1$s</string>
<string name="drivers_mali_bridge_auto">Ingen åben driver dækker denne Mali endnu: Mali-broen er slået til.</string>
<string name="drivers_mali_bridge_fallback">%1$s er ikke udgivet endnu: Mali-broen er slået til, indtil den er.</string>
<string name="drivers_mali_no_pair_auto">Ingen åben driver dækker denne Mali endnu: %1$s bruges på telefonens egen driver.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s er ikke tilgængelig, og denne telefons driver rapporterer Vulkan %2$s (Venus kræver 1.2): Mali-broen bruges.</string>
<string name="drivers_mali_unpublished_auto">%1$s er ikke udgivet endnu: %2$s bruges, indtil den er.</string>
<string name="drivers_pair_updated">%1$s opdateret til den nyeste version</string>
<string name="drivers_powervr_builtin">Systemets PowerVR-driver er valgt til kørselsmiljøet og skærmen.</string>
<string name="drivers_route_bridge">Mali-broen</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Bruger de indbyggede drivere til denne GPU</string>
<string name="gpu_system_powervr">System-PowerVR</string>
<string name="gpuinfo_bridge">Kører via Mali-broen på telefonens egen Vulkan-driver</string>
<string name="gpuinfo_bridge_off">Understøttes ikke, når Mali-broen er slået fra: Turnip kræver en Adreno-GPU (Snapdragon)</string>
<string name="gpuinfo_powervr">Systemets PowerVR-driver: kompatibiliteten varierer fra enhed til enhed</string>
<string name="gpuinfo_xclipse">Eksperimentel: RADV eller systemets Xclipse-driver</string>
<string name="libraries_access_allow">Tillad</string>
<string name="libraries_access_hint">Tillad adgang til alle filer, så DroidDeck kan se Steam-biblioteker, som andre apps har på den interne lagerplads</string>
<string name="libraries_access_title">Find biblioteker på intern lagerplads</string>
<string name="libraries_add_found">Tilføj</string>
<string name="libraries_found_hint">Fundet på denne enhed · %1$s · %2$s</string>
<string name="libraries_used_inner">Tilføjede %1$s, Steam-biblioteket i den mappe, du valgte</string>
<string name="loadstate_steam_update_applying">Anvender opdateringen af Steam-klienten…</string>
<string name="pckb_group_arrows">Piletaster</string>
<string name="pckb_group_function">Esc og funktionsrækken</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">Skjulte taster</string>
<string name="pckb_hidden_keys_hint">Taster, du sjældent bruger, holdes væk fra tastaturet.</string>
<string name="pckb_opacity">Uigennemsigtighed</string>
<string name="pckb_opacity_hint">Sænk den for at se spillet bag tastaturet.</string>
<string name="pckb_size">Størrelse på skærmen</string>
<string name="pckb_size_hint">Den del af skærmen, tastaturet fylder, inklusive overskriften.</string>
<string name="session_end_present_fault">Sessionen kunne ikke tegne på skærmen: %1$s. Denne skærm viser hvorfor i stedet for en sort skærm; sessionslogfilerne har detaljerne.</string>
<string name="session_killed_for_memory">Den seneste session sluttede, fordi Android lukkede DroidDeck for at frigøre hukommelse, ikke på grund af et nedbrud. Luk andre apps, eller sænk spillets teksturindstillinger, og prøv igen.</string>
<string name="setup_enhanced_diagnostics">Udvidet diagnosticering til næste session</string>
<string name="setup_enhanced_diagnostics_hint">Indsamler detaljerede logfiler for én session. Kan sænke ydeevnen og bruge mere lagerplads. Slår sig selv fra, når sessionen starter; gennemgå logfilerne, før du deler.</string>
<string name="setup_gpu_bridge_fell_back">Mali-broen bestod ikke sin selvtest på %1$s i sidste session (billeder eller tegning), så sessionen brugte PanVK-driverparret i stedet.</string>
<string name="setup_gpu_bridge_no_memory_sharing">Mali-broen fandt ingen måde at dele hukommelse med driveren på %1$s i sidste session, så sessionen brugte PanVK-driverparret i stedet. Broen kan ikke fungere på denne telefon; hvis du slår den fra, beholdes PanVK-parret.</string>
<string name="setup_gpu_bridge_unusable">%1$s kan ikke bruge Mali-broen (den kræver en Mali-GPU uden en systemdriverrute), så denne kontakt har ingen effekt her.</string>
<string name="setup_gpu_bridged">GPU via Mali-broen</string>
<string name="setup_gpu_venus_host_too_old">Driveren på %1$s rapporterer Vulkan %2$s. Mali (Venus) kræver Vulkan 1.2 eller nyere (gamescope starter ikke på ældre), så sessioner springer Venus over og bruger PanVK-driverparret i stedet.</string>
<string name="setup_gpu_venus_off">Mali (Venus): fra</string>
<string name="setup_gpu_venus_on">Mali (Venus): til</string>
<string name="setup_graphics_logs">Detaljerede grafiklogfiler</string>
<string name="setup_graphics_logs_hint">Hvert spils Proton-, DXVK- og driverlogfiler i sessionens logfiler. Altid slået til, når GPU\'en ikke er en Adreno.</string>
<string name="setup_log_current">Nuværende session</string>
<string name="setup_log_delete">Slet</string>
<string name="setup_log_delete_failed">Kunne ikke slette sessionslogfiler.</string>
<string name="setup_log_delete_message">Slet %1$s? Det kan ikke fortrydes.</string>
<string name="setup_log_delete_title">Slet sessionslogfiler?</string>
<string name="setup_log_empty">Ingen gemte sessionslogfiler endnu.</string>
<string name="setup_log_export_failed">Kunne ikke gemme sessionslogfiler.</string>
<string name="setup_log_export_hint">Delte og gemte ZIP-filer indeholder den valgte session samt tilhørende diagnosticering af Steam, butik, værktøjer og Battle.net.</string>
<string name="setup_log_export_saved">Sessionslogfiler gemt.</string>
<string name="setup_log_save">Gem på enheden</string>
<string name="setup_log_sessions">Tilgængelige sessionslogfiler</string>
<string name="setup_log_share">Del</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Steam-grænseflade: GPU-tegning</string>
<string name="setup_steam_gpu_drawing_hint">Med Xclipse-driverparret tegner Steam sine sider i software (GPU-tegning viste ødelagte felter). Slå til for en hurtigere Steam-grænseflade, hvis den ser rigtig ud på denne telefon.</string>
<string name="swap_game_screen">Vis spillet på</string>
<string name="swap_game_screen_bottom">Nederste skærm</string>
<string name="swap_game_screen_group">Spilskærm</string>
<string name="swap_game_screen_top">Øverste skærm</string>
<string name="swap_screens">Byt skærme</string>
<string name="swap_screens_unavailable">Den nederste skærm er ikke ledig lige nu</string>
<string name="upd_block_non_adreno_test">Dette er testbuilden uden Snapdragon: de officielle builds her kører kun på Adreno, så de installeres ikke oven på den. Nye testbuilds findes på github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Opdatering i kø.</string>
<string name="wincomp_repair_required">Denne download skal opdateres.</string>
<string name="wincomp_retry">Prøv opdatering igen</string>
</resources>
+127 -4
View File
@@ -671,9 +671,9 @@
<string name="hud_group_metrics">Messwerte</string>
<string name="hud_metric_graph">Frametime-Diagramm</string>
<string name="hud_metric_decimal">Bildrate mit Nachkommastelle</string>
<string name="hud_metric_low1">1%-Tiefstwert</string>
<string name="hud_metric_low01">0.1%-Tiefstwert</string>
<string name="hud_metric_low001">0.01%-Tiefstwert</string>
<string name="hud_metric_low1" formatted="false">1%-Tiefstwert</string>
<string name="hud_metric_low01" formatted="false">0.1%-Tiefstwert</string>
<string name="hud_metric_low001" formatted="false">0.01%-Tiefstwert</string>
<string name="hud_metric_engine">Grafik-Engine</string>
<string name="hud_metric_gpu_model">GPU-Name</string>
<string name="hud_metric_gpu">GPU-Auslastung</string>
@@ -3553,7 +3553,7 @@
<string name="integration_shortcuts_maybe_restricted">In Android ausgegraut? App-Info → ⋮ → Eingeschränkte Einstellungen zulassen</string>
<string name="integration_shortcuts_restricted">Von Android blockiert · App-Info → ⋮ → Eingeschränkte Einstellungen zulassen</string>
<string name="integration_shortcuts_service">DroidDeck-Systemtasten</string>
<string name="integration_shortcuts_service_description">Hält Alt+Tab, die Win-Taste und die Home-Taste in einer aktiven DroidDeck-Sitzung statt bei Android.</string>
<string name="integration_shortcuts_service_description">Hält Alt+Tab, die Win-Taste und die Home/Guide-Taste in einer aktiven DroidDeck-Sitzung und erkennt Apps auf dem unteren Bildschirm. Während eine Sitzung aktiv ist, liest der Dienst Tasten einer Hardwaretastatur sowie App-Paketnamen und Fenstergrenzen, nur auf diesem Gerät. Er liest keinen Text, nimmt den Bildschirm nicht auf und sendet diese Daten nicht.</string>
<string name="integration_brightness_needed">Aktiviere Helligkeit unter Einrichtung → Steam-Integration</string>
<string name="setup_gpu_bridge_off">Mali-Brücke: aus</string>
<string name="setup_gpu_bridge_on">Mali-Brücke: an</string>
@@ -3647,6 +3647,52 @@
<string name="gog_repair_action">Dateien reparieren</string>
<string name="gog_refresh_action">Installation erneuern</string>
<string name="gog_select_patch">Auswählen</string>
<string name="setup_all_files">Zugriff auf alle Dateien</string>
<string name="setup_local_network">Lokales Netzwerk</string>
<string name="libraries_title">Weitere Steam-Bibliotheken</string>
<string name="libraries_hint">Ordner, in denen Steam Spiele installieren und starten kann, jeweils mit eigenem Namen. Beim Entfernen bleiben die Spiele auf dem Datenträger.</string>
<string name="libraries_add">Bibliothek hinzufügen</string>
<string name="libraries_rename_title">Bibliotheksname</string>
<string name="libraries_pick">Ordner für die Bibliothek wählen</string>
<string name="libraries_full">Höchstens %1$d zusätzliche Bibliotheken</string>
<string name="libraries_overlap">Dieser Ordner ist bereits eine Bibliothek oder liegt darin</string>
<string name="libraries_missing">%1$s · nicht verfügbar</string>
<string name="libraries_applies_next">Gilt ab der nächsten Steam-Sitzung</string>
<string name="bottom_desktop_start">Desktop starten</string>
<string name="bottom_desktop_mode_touch">Direkte Berührung</string>
<string name="bottom_desktop_trackpad">Trackpad. Für direkte Berührung tippen</string>
<string name="bottom_desktop_direct_touch">Direkte Berührung. Für Trackpad tippen</string>
<string name="bottom_desktop_starting">KDE-Desktop wird gestartet…</string>
<string name="bottom_desktop_ended">Der Desktop wurde geschlossen.</string>
<string name="bottom_desktop_failed">Der Desktop konnte nicht starten. Seine Protokolle liegen im Ordner bottom-desktop dieser Sitzung.</string>
<string name="bottom_desktop_off">Der Desktop ist geschlossen.</string>
<string name="libraries_rename">Umbenennen</string>
<string name="second_mode_desktop">Desktop</string>
<string name="bottom_desktop_mode_trackpad">Trackpad</string>
<string name="bottom_desktop_title">Desktop · %1$s</string>
<string name="setup_dual_screen_launcher">Launcher auf zwei Bildschirmen</string>
<string name="setup_dual_screen_launcher_hint">Automatisch zeigt die Launcher-Bereiche (Steam, Bibliothek, Stores und weitere) auf dem eingebauten zweiten Bildschirm. Ein nutzt jeden zweiten Bildschirm. Aus hält den Launcher auf einem Bildschirm.</string>
<string name="setup_dual_screen_sessions">Sitzungen auf zwei Bildschirmen</string>
<string name="setup_dual_screen_sessions_hint">Automatisch dockt das Spielmenü am eingebauten zweiten Bildschirm statt als Einblendung an. Ein nutzt jeden zweiten Bildschirm. Aus zeigt das Menü auf dem Spielbildschirm; dort kann weiterhin ein zweiter Bildschirm gewählt werden.</string>
<string name="stats_hud_frametime">Framezeit</string>
<string name="stats_hud_last_seconds">letzte %1$d s</string>
<string name="stats_hud_avg_frame">mittlere Framezeit</string>
<string name="stats_hud_low" formatted="false">1-%-Tiefstwert</string>
<string name="stats_hud_slow_frames">Frames über 33 ms</string>
<string name="stats_hud_per_minute">%1$d / Min.</string>
<string name="stats_hud_playing_for">Spielzeit: %1$s</string>
<string name="stats_hud_core_group">%1$d Kerne, bis %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d Kerne</string>
<string name="stats_hud_fps_of">FPS bei %1$d Hz</string>
<string name="stats_hud_red_mark">An der roten Markierung zeigt das HUD die Temperatur rot an.</string>
<string name="stats_hud_whole_device">gesamtes Gerät</string>
<string name="bottom_hud_group">HUD auf unterem Bildschirm</string>
<string name="bottom_strip_tabs">Leiste auf jedem Tab</string>
<string name="bottom_strip_lows" formatted="false">1-%- und 0,1-%-Tiefstwerte</string>
<string name="bottom_strip_cpu_temp">CPU-Temperatur</string>
<string name="stats_hud_battery_temp">Akku</string>
<string name="stats_hud_game_cores">Spiel</string>
<string name="setup_privacy_policy">Datenschutzerklärung</string>
<string name="stores_gamejolt_not_owned">Nicht in deiner Game Jolt-Bibliothek</string>
<string name="stores_gamejolt_no_build">Kein Windows-Download für dieses Spiel</string>
<string name="stores_gamejolt_no_exe">Im Download wurde kein Windows-Spiel gefunden</string>
@@ -3676,4 +3722,81 @@
<string name="stores_gamejolt_filter_status">Status</string>
<string name="stores_gamejolt_filter_partners">Partner</string>
<string name="stores_gamejolt_followed">Gefolgte Spiele</string>
<string name="cmgr_powervr_dxvk_managed">DXVK wird von der PowerVR-Laufzeit des Systems bereitgestellt. Andere DXVK-Pakete können gespeichert, mit diesem Treiber aber nicht angewendet werden.</string>
<string name="cmgr_powervr_dxvk_queued">In Warteschlange: %1$s (wartet auf einen anderen Laufzeittreiber)</string>
<string name="diagnostics_share_all">Alle Bundles</string>
<string name="diagnostics_share_games">Spiele und Kompatibilität</string>
<string name="diagnostics_share_graphics">Grafik</string>
<string name="diagnostics_share_session">Sitzung und Gerät</string>
<string name="diagnostics_share_title">Diagnose-Bundles teilen</string>
<string name="drivers_built_on">erstellt %1$s</string>
<string name="drivers_mali_bridge_auto">Noch kein offener Treiber deckt diese Mali ab: Die Mali-Brücke ist aktiv.</string>
<string name="drivers_mali_bridge_fallback">%1$s ist noch nicht veröffentlicht: Die Mali-Brücke ist aktiv, bis es so weit ist.</string>
<string name="drivers_mali_no_pair_auto">Noch kein offener Treiber deckt diese Mali ab: %1$s wird mit dem eigenen Treiber des Handys verwendet.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s ist nicht verfügbar, und der Treiber dieses Handys meldet Vulkan %2$s (Venus braucht 1.2): Die Mali-Brücke wird verwendet.</string>
<string name="drivers_mali_unpublished_auto">%1$s ist noch nicht veröffentlicht: Bis dahin wird %2$s verwendet.</string>
<string name="drivers_pair_updated">%1$s auf den neuesten Build aktualisiert</string>
<string name="drivers_powervr_builtin">Der PowerVR-Systemtreiber ist für Laufzeit und Anzeige ausgewählt.</string>
<string name="drivers_route_bridge">die Mali-Brücke</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Verwendet die integrierten Treiber für diese GPU</string>
<string name="gpu_system_powervr">System-PowerVR</string>
<string name="gpuinfo_bridge">Läuft über die Mali-Brücke, auf dem eigenen Vulkan-Treiber des Handys</string>
<string name="gpuinfo_bridge_off">Bei ausgeschalteter Mali-Brücke nicht unterstützt: Turnip braucht eine Adreno-GPU (Snapdragon)</string>
<string name="gpuinfo_powervr">PowerVR-Systemtreiber: Kompatibilität variiert je nach Gerät</string>
<string name="gpuinfo_xclipse">Experimentell: RADV oder der Xclipse-Systemtreiber</string>
<string name="libraries_access_allow">Erlauben</string>
<string name="libraries_access_hint">Erlaube den Zugriff auf alle Dateien, damit DroidDeck Steam-Bibliotheken sehen kann, die andere Apps im internen Speicher ablegen</string>
<string name="libraries_access_title">Bibliotheken im internen Speicher finden</string>
<string name="libraries_add_found">Hinzufügen</string>
<string name="libraries_found_hint">Auf diesem Gerät gefunden · %1$s · %2$s</string>
<string name="libraries_used_inner">%1$s hinzugefügt, die Steam-Bibliothek im gewählten Ordner</string>
<string name="loadstate_steam_update_applying">Steam-Client-Update wird angewendet…</string>
<string name="pckb_group_arrows">Pfeiltasten</string>
<string name="pckb_group_function">Esc und Funktionstasten</string>
<string name="pckb_group_nav">Einfg / Entf / Pos1 / Ende / Bild auf / Bild ab</string>
<string name="pckb_hidden_keys">Ausgeblendete Tasten</string>
<string name="pckb_hidden_keys_hint">Selten genutzte Tasten werden auf der Tastatur nicht angezeigt.</string>
<string name="pckb_opacity">Deckkraft</string>
<string name="pckb_opacity_hint">Verringere sie, um das Spiel hinter der Tastatur zu sehen.</string>
<string name="pckb_size">Größe auf dem Bildschirm</string>
<string name="pckb_size_hint">Anteil des Bildschirms, den die Tastatur einnimmt, Kopfzeile eingeschlossen.</string>
<string name="session_end_present_fault">Die Sitzung konnte nicht auf den Bildschirm zeichnen: %1$s. Dieser Bildschirm zeigt den Grund statt eines schwarzen; die Sitzungsprotokolle enthalten die Details.</string>
<string name="session_killed_for_memory">Die letzte Sitzung wurde beendet, weil Android DroidDeck geschlossen hat, um Arbeitsspeicher freizugeben, nicht wegen eines Absturzes. Schließe andere Apps oder senke die Texturoptionen des Spiels und versuche es dann erneut.</string>
<string name="setup_enhanced_diagnostics">Erweiterte Diagnose für nächste Sitzung</string>
<string name="setup_enhanced_diagnostics_hint">Erfasst detaillierte Protokolle für eine Sitzung. Kann die Leistung verringern und mehr Speicher belegen. Schaltet sich beim Start der Sitzung selbst aus; prüfe die Protokolle vor dem Teilen.</string>
<string name="setup_gpu_bridge_fell_back">Die Mali-Brücke hat den Selbsttest auf %1$s in der letzten Sitzung nicht bestanden (Bilder oder Zeichenaufrufe), daher verwendete die Sitzung stattdessen das PanVK-Treiberpaar.</string>
<string name="setup_gpu_bridge_no_memory_sharing">Die Mali-Brücke fand in der letzten Sitzung auf %1$s keine Möglichkeit, Speicher mit dem Treiber zu teilen, daher verwendete die Sitzung stattdessen das PanVK-Treiberpaar. Die Brücke funktioniert auf diesem Handy nicht; wenn du sie ausschaltest, bleibt das PanVK-Paar aktiv.</string>
<string name="setup_gpu_bridge_unusable">%1$s kann die Mali-Brücke nicht nutzen (sie braucht eine Mali-GPU ohne Systemtreiber-Route), daher hat dieser Schalter hier keine Wirkung.</string>
<string name="setup_gpu_bridged">GPU über Mali-Brücke</string>
<string name="setup_gpu_venus_host_too_old">Der Treiber auf %1$s meldet Vulkan %2$s. Mali (Venus) braucht Vulkan 1.2 oder neuer (gamescope startet mit älteren nicht), daher überspringen Sitzungen Venus und verwenden stattdessen das PanVK-Treiberpaar.</string>
<string name="setup_gpu_venus_off">Mali (Venus): aus</string>
<string name="setup_gpu_venus_on">Mali (Venus): an</string>
<string name="setup_graphics_logs">Detaillierte Grafikprotokolle</string>
<string name="setup_graphics_logs_hint">Proton-, DXVK- und Treiberprotokolle jedes Spiels in den Sitzungsprotokollen. Immer an, wenn die GPU keine Adreno ist.</string>
<string name="setup_log_current">Aktuelle Sitzung</string>
<string name="setup_log_delete">Löschen</string>
<string name="setup_log_delete_failed">Sitzungsprotokolle konnten nicht gelöscht werden.</string>
<string name="setup_log_delete_message">%1$s löschen? Das kann nicht rückgängig gemacht werden.</string>
<string name="setup_log_delete_title">Sitzungsprotokolle löschen?</string>
<string name="setup_log_empty">Noch keine gespeicherten Sitzungsprotokolle.</string>
<string name="setup_log_export_failed">Sitzungsprotokolle konnten nicht gespeichert werden.</string>
<string name="setup_log_export_hint">Geteilte und gespeicherte ZIPs enthalten die gewählte Sitzung sowie zugehörige Steam-, Store-, Tool- und Battle.net-Diagnosen.</string>
<string name="setup_log_export_saved">Sitzungsprotokolle gespeichert.</string>
<string name="setup_log_save">Auf Gerät speichern</string>
<string name="setup_log_sessions">Verfügbare Sitzungsprotokolle</string>
<string name="setup_log_share">Teilen</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Steam-Oberfläche: GPU-Zeichnung</string>
<string name="setup_steam_gpu_drawing_hint">Mit dem Xclipse-Treiberpaar zeichnet Steam seine Seiten per Software (die GPU-Zeichnung zeigte kaputte Kacheln). Schalte sie für eine schnellere Steam-Oberfläche ein, wenn sie auf diesem Handy richtig aussieht.</string>
<string name="swap_game_screen">Spiel anzeigen auf</string>
<string name="swap_game_screen_bottom">Unterer Bildschirm</string>
<string name="swap_game_screen_group">Spielbildschirm</string>
<string name="swap_game_screen_top">Oberer Bildschirm</string>
<string name="swap_screens">Bildschirme tauschen</string>
<string name="swap_screens_unavailable">Der untere Bildschirm ist gerade nicht frei</string>
<string name="upd_block_non_adreno_test">Dies ist der Test-Build ohne Snapdragon: Die offiziellen Builds laufen hier nur auf Adreno und werden daher nicht darüber installiert. Neue Test-Builds gibt es auf github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Update in Warteschlange.</string>
<string name="wincomp_repair_required">Dieser Download braucht ein Update.</string>
<string name="wincomp_retry">Update wiederholen</string>
</resources>
+127 -4
View File
@@ -671,9 +671,9 @@
<string name="hud_group_metrics">Μετρήσεις</string>
<string name="hud_metric_graph">Γράφημα χρόνου καρέ</string>
<string name="hud_metric_decimal">Ρυθμός καρέ με δεκαδικό</string>
<string name="hud_metric_low1">1% χαμηλότερα</string>
<string name="hud_metric_low01">0.1% χαμηλότερα</string>
<string name="hud_metric_low001">0.01% χαμηλότερα</string>
<string name="hud_metric_low1" formatted="false">1% χαμηλότερα</string>
<string name="hud_metric_low01" formatted="false">0.1% χαμηλότερα</string>
<string name="hud_metric_low001" formatted="false">0.01% χαμηλότερα</string>
<string name="hud_metric_engine">Μηχανή γραφικών</string>
<string name="hud_metric_gpu_model">Όνομα GPU</string>
<string name="hud_metric_gpu">Φόρτος GPU</string>
@@ -3553,7 +3553,7 @@
<string name="integration_shortcuts_maybe_restricted">Γκρίζο στο Android; Πληροφορίες εφαρμογής → ⋮ → Να επιτρέπονται οι περιορισμένες ρυθμίσεις</string>
<string name="integration_shortcuts_restricted">Αποκλείεται από το Android · Πληροφορίες εφαρμογής → ⋮ → Να επιτρέπονται οι περιορισμένες ρυθμίσεις</string>
<string name="integration_shortcuts_service">Πλήκτρα συστήματος DroidDeck</string>
<string name="integration_shortcuts_service_description">Διατηρεί τα Alt+Tab, το πλήκτρο Win και το κουμπί Αρχικής στην ενεργή συνεδρία DroidDeck αντί για το Android.</string>
<string name="integration_shortcuts_service_description">Κρατά τα Alt+Tab, το πλήκτρο Win και το κουμπί Αρχική/Guide μέσα σε ενεργή συνεδρία DroidDeck και εντοπίζει εφαρμογές στην κάτω οθόνη. Όσο η συνεδρία είναι ενεργή, διαβάζει πλήκτρα από φυσικό πληκτρολόγιο, καθώς και ονόματα πακέτων εφαρμογών και όρια παραθύρων, μόνο σε αυτή τη συσκευή. Δεν διαβάζει κείμενο, δεν καταγράφει την οθόνη και δεν στέλνει αυτά τα δεδομένα.</string>
<string name="integration_brightness_needed">Ενεργοποιήστε τη Φωτεινότητα στη Ρύθμιση → Ενσωμάτωση Steam</string>
<string name="setup_gpu_bridge_off">Γέφυρα Mali: ανενεργή</string>
<string name="setup_gpu_bridge_on">Γέφυρα Mali: ενεργή</string>
@@ -3647,6 +3647,52 @@
<string name="gog_repair_action">Επιδιόρθωση αρχείων</string>
<string name="gog_refresh_action">Ανανέωση εγκατάστασης</string>
<string name="gog_select_patch">Επιλογή</string>
<string name="setup_all_files">Πρόσβαση σε όλα τα αρχεία</string>
<string name="setup_local_network">Τοπικό δίκτυο</string>
<string name="libraries_title">Περισσότερες βιβλιοθήκες Steam</string>
<string name="libraries_hint">Φάκελοι όπου το Steam μπορεί να εγκαθιστά και να εκκινεί παιχνίδια, ο καθένας με δικό του όνομα. Η αφαίρεση διατηρεί τα παιχνίδια στον δίσκο.</string>
<string name="libraries_add">Προσθήκη βιβλιοθήκης</string>
<string name="libraries_rename_title">Όνομα βιβλιοθήκης</string>
<string name="libraries_pick">Επιλέξτε φάκελο βιβλιοθήκης</string>
<string name="libraries_full">Έως %1$d πρόσθετες βιβλιοθήκες</string>
<string name="libraries_overlap">Αυτός ο φάκελος είναι ήδη βιβλιοθήκη ή βρίσκεται μέσα σε μία</string>
<string name="libraries_missing">%1$s · μη διαθέσιμη</string>
<string name="libraries_applies_next">Ισχύει από την επόμενη συνεδρία Steam</string>
<string name="bottom_desktop_start">Εκκίνηση επιφάνειας εργασίας</string>
<string name="bottom_desktop_mode_touch">Άμεση αφή</string>
<string name="bottom_desktop_trackpad">Επιφάνεια αφής. Πατήστε για άμεση αφή</string>
<string name="bottom_desktop_direct_touch">Άμεση αφή. Πατήστε για επιφάνεια αφής</string>
<string name="bottom_desktop_starting">Εκκίνηση επιφάνειας εργασίας KDE…</string>
<string name="bottom_desktop_ended">Η επιφάνεια εργασίας έκλεισε.</string>
<string name="bottom_desktop_failed">Δεν ήταν δυνατή η εκκίνηση της επιφάνειας εργασίας. Τα αρχεία καταγραφής βρίσκονται στον φάκελο bottom-desktop αυτής της συνεδρίας.</string>
<string name="bottom_desktop_off">Η επιφάνεια εργασίας είναι κλειστή.</string>
<string name="libraries_rename">Μετονομασία</string>
<string name="second_mode_desktop">Επιφάνεια εργασίας</string>
<string name="bottom_desktop_mode_trackpad">Πινακίδα αφής</string>
<string name="bottom_desktop_title">Επιφάνεια εργασίας · %1$s</string>
<string name="setup_dual_screen_launcher">Εκκινητής σε δύο οθόνες</string>
<string name="setup_dual_screen_launcher_hint">Το Αυτόματο τοποθετεί τις ενότητες του εκκινητή (Steam, Βιβλιοθήκη, Καταστήματα και άλλες) στην ενσωματωμένη δεύτερη οθόνη. Το Ενεργό χρησιμοποιεί οποιαδήποτε δεύτερη οθόνη. Το Ανενεργό κρατά τον εκκινητή σε μία οθόνη.</string>
<string name="setup_dual_screen_sessions">Συνεδρίες σε δύο οθόνες</string>
<string name="setup_dual_screen_sessions_hint">Το Αυτόματο τοποθετεί το μενού παιχνιδιού στην ενσωματωμένη δεύτερη οθόνη αντί για επικάλυψη. Το Ενεργό χρησιμοποιεί οποιαδήποτε δεύτερη οθόνη. Το Ανενεργό κρατά το μενού στην οθόνη παιχνιδιού· μπορείτε ακόμη να επιλέξετε δεύτερη οθόνη από εκεί.</string>
<string name="stats_hud_frametime">Χρόνος καρέ</string>
<string name="stats_hud_last_seconds">τελευταία %1$d δ</string>
<string name="stats_hud_avg_frame">μέσο καρέ</string>
<string name="stats_hud_low" formatted="false">χαμηλότερο 1 %</string>
<string name="stats_hud_slow_frames">καρέ πάνω από 33 ms</string>
<string name="stats_hud_per_minute">%1$d / λεπτό</string>
<string name="stats_hud_playing_for">Χρόνος παιχνιδιού: %1$s</string>
<string name="stats_hud_core_group">%1$d πυρήνες, έως %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d πυρήνες</string>
<string name="stats_hud_fps_of">FPS στα %1$d Hz</string>
<string name="stats_hud_red_mark">Η κόκκινη ένδειξη δείχνει πού το HUD εμφανίζει τη θερμοκρασία με κόκκινο.</string>
<string name="stats_hud_whole_device">ολόκληρη συσκευή</string>
<string name="bottom_hud_group">HUD κάτω οθόνης</string>
<string name="bottom_strip_tabs">Λωρίδα σε κάθε καρτέλα</string>
<string name="bottom_strip_lows" formatted="false">χαμηλότερα 1 % και 0,1 %</string>
<string name="bottom_strip_cpu_temp">Θερμοκρασία CPU</string>
<string name="stats_hud_battery_temp">Μπαταρία</string>
<string name="stats_hud_game_cores">Παιχνίδι</string>
<string name="setup_privacy_policy">Πολιτική απορρήτου</string>
<string name="stores_gamejolt_not_owned">Δεν είναι στη βιβλιοθήκη σας στο Game Jolt</string>
<string name="stores_gamejolt_no_build">Δεν υπάρχει λήψη για Windows για αυτό το παιχνίδι</string>
<string name="stores_gamejolt_no_exe">Δεν βρέθηκε παιχνίδι Windows στη λήψη</string>
@@ -3676,4 +3722,81 @@
<string name="stores_gamejolt_filter_status">Κατάσταση</string>
<string name="stores_gamejolt_filter_partners">Συνεργάτες</string>
<string name="stores_gamejolt_followed">Παιχνίδια που ακολουθείτε</string>
<string name="cmgr_powervr_dxvk_managed">Το DXVK παρέχεται από το περιβάλλον εκτέλεσης PowerVR του συστήματος. Άλλα πακέτα DXVK μπορούν να αποθηκευτούν, αλλά δεν εφαρμόζονται με αυτόν τον οδηγό.</string>
<string name="cmgr_powervr_dxvk_queued">Στην ουρά: %1$s (αναμονή για διαφορετικό οδηγό περιβάλλοντος εκτέλεσης)</string>
<string name="diagnostics_share_all">Όλα τα πακέτα</string>
<string name="diagnostics_share_games">Παιχνίδια και συμβατότητα</string>
<string name="diagnostics_share_graphics">Γραφικά</string>
<string name="diagnostics_share_session">Συνεδρία και συσκευή</string>
<string name="diagnostics_share_title">Κοινοποίηση πακέτων διαγνωστικών</string>
<string name="drivers_built_on">έκδοση %1$s</string>
<string name="drivers_mali_bridge_auto">Δεν υπάρχει ακόμη ανοιχτός οδηγός για αυτό το Mali: η γέφυρα Mali είναι ενεργή.</string>
<string name="drivers_mali_bridge_fallback">Το %1$s δεν έχει δημοσιευτεί ακόμη: η γέφυρα Mali είναι ενεργή μέχρι τότε.</string>
<string name="drivers_mali_no_pair_auto">Δεν υπάρχει ακόμη ανοιχτός οδηγός για αυτό το Mali: χρησιμοποιείται το %1$s, με τον ίδιο τον οδηγό του τηλεφώνου.</string>
<string name="drivers_mali_old_vulkan_bridge">Το %1$s δεν είναι διαθέσιμο και ο οδηγός αυτού του τηλεφώνου αναφέρει Vulkan %2$s (το Venus απαιτεί 1.2): χρησιμοποιείται η γέφυρα Mali.</string>
<string name="drivers_mali_unpublished_auto">Το %1$s δεν έχει δημοσιευτεί ακόμη: μέχρι τότε χρησιμοποιείται το %2$s.</string>
<string name="drivers_pair_updated">Το %1$s ενημερώθηκε στην πιο πρόσφατη έκδοση</string>
<string name="drivers_powervr_builtin">Ο οδηγός PowerVR του συστήματος έχει επιλεγεί για το περιβάλλον εκτέλεσης και την οθόνη.</string>
<string name="drivers_route_bridge">η γέφυρα Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Χρησιμοποιεί τους ενσωματωμένους οδηγούς για αυτό το GPU</string>
<string name="gpu_system_powervr">PowerVR συστήματος</string>
<string name="gpuinfo_bridge">Εκτελείται μέσω της γέφυρας Mali, με τον ίδιο τον οδηγό Vulkan του τηλεφώνου</string>
<string name="gpuinfo_bridge_off">Δεν υποστηρίζεται με απενεργοποιημένη τη γέφυρα Mali: το Turnip απαιτεί GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">Οδηγός PowerVR συστήματος: η συμβατότητα διαφέρει ανά συσκευή</string>
<string name="gpuinfo_xclipse">Πειραματικό: RADV ή ο οδηγός Xclipse του συστήματος</string>
<string name="libraries_access_allow">Να επιτρέπεται</string>
<string name="libraries_access_hint">Επιτρέψτε πρόσβαση σε όλα τα αρχεία, ώστε το DroidDeck να βλέπει βιβλιοθήκες Steam που άλλες εφαρμογές κρατούν στον εσωτερικό αποθηκευτικό χώρο</string>
<string name="libraries_access_title">Εύρεση βιβλιοθηκών στον εσωτερικό αποθηκευτικό χώρο</string>
<string name="libraries_add_found">Προσθήκη</string>
<string name="libraries_found_hint">Βρέθηκε σε αυτήν τη συσκευή · %1$s · %2$s</string>
<string name="libraries_used_inner">Προστέθηκε το %1$s, η βιβλιοθήκη Steam μέσα στον φάκελο που επιλέξατε</string>
<string name="loadstate_steam_update_applying">Εφαρμογή της ενημέρωσης του προγράμματος-πελάτη Steam…</string>
<string name="pckb_group_arrows">Πλήκτρα βελών</string>
<string name="pckb_group_function">Esc και σειρά λειτουργιών</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">Κρυφά πλήκτρα</string>
<string name="pckb_hidden_keys_hint">Τα πλήκτρα που σπάνια χρησιμοποιείτε δεν εμφανίζονται στο πληκτρολόγιο.</string>
<string name="pckb_opacity">Αδιαφάνεια</string>
<string name="pckb_opacity_hint">Μειώστε την για να βλέπετε το παιχνίδι πίσω από το πληκτρολόγιο.</string>
<string name="pckb_size">Μέγεθος στην οθόνη</string>
<string name="pckb_size_hint">Το ποσοστό της οθόνης που καταλαμβάνει το πληκτρολόγιο, μαζί με την κεφαλίδα.</string>
<string name="session_end_present_fault">Η συνεδρία δεν μπόρεσε να σχεδιάσει στην οθόνη: %1$s. Αυτή η οθόνη δείχνει τον λόγο αντί για μαύρη οθόνη· οι λεπτομέρειες υπάρχουν στα αρχεία καταγραφής της συνεδρίας.</string>
<string name="session_killed_for_memory">Η τελευταία συνεδρία τερματίστηκε επειδή το Android έκλεισε το DroidDeck για να ελευθερώσει μνήμη, όχι λόγω κατάρρευσης. Κλείστε άλλες εφαρμογές ή χαμηλώστε τις ρυθμίσεις υφής του παιχνιδιού και δοκιμάστε ξανά.</string>
<string name="setup_enhanced_diagnostics">Βελτιωμένα διαγνωστικά για την επόμενη συνεδρία</string>
<string name="setup_enhanced_diagnostics_hint">Καταγράφει αναλυτικά αρχεία καταγραφής για μία συνεδρία. Μπορεί να μειώσει την απόδοση και να χρησιμοποιήσει περισσότερο χώρο. Απενεργοποιείται μόνο του όταν ξεκινά η συνεδρία· ελέγξτε τα αρχεία καταγραφής πριν τα κοινοποιήσετε.</string>
<string name="setup_gpu_bridge_fell_back">Η γέφυρα Mali απέτυχε στον αυτοέλεγχο στο %1$s την τελευταία συνεδρία (εικόνες ή σχεδίαση), οπότε η συνεδρία χρησιμοποίησε το ζεύγος οδηγών PanVK.</string>
<string name="setup_gpu_bridge_no_memory_sharing">Η γέφυρα Mali δεν βρήκε τρόπο κοινής χρήσης μνήμης με τον οδηγό στο %1$s την τελευταία συνεδρία, οπότε η συνεδρία χρησιμοποίησε το ζεύγος οδηγών PanVK. Η γέφυρα δεν μπορεί να λειτουργήσει σε αυτό το τηλέφωνο· η απενεργοποίησή της διατηρεί το ζεύγος PanVK.</string>
<string name="setup_gpu_bridge_unusable">Το %1$s δεν μπορεί να χρησιμοποιήσει τη γέφυρα Mali (απαιτεί GPU Mali χωρίς διαδρομή μέσω οδηγού συστήματος), οπότε αυτός ο διακόπτης δεν έχει εδώ κανένα αποτέλεσμα.</string>
<string name="setup_gpu_bridged">GPU μέσω γέφυρας Mali</string>
<string name="setup_gpu_venus_host_too_old">Ο οδηγός στο %1$s αναφέρει Vulkan %2$s. Το Mali (Venus) απαιτεί Vulkan 1.2 ή νεότερο (το gamescope δεν ξεκινά σε παλαιότερο), οπότε οι συνεδρίες παραλείπουν το Venus και χρησιμοποιούν το ζεύγος οδηγών PanVK.</string>
<string name="setup_gpu_venus_off">Mali (Venus): ανενεργό</string>
<string name="setup_gpu_venus_on">Mali (Venus): ενεργό</string>
<string name="setup_graphics_logs">Αναλυτικά αρχεία καταγραφής γραφικών</string>
<string name="setup_graphics_logs_hint">Τα αρχεία καταγραφής Proton, DXVK και οδηγού κάθε παιχνιδιού στα αρχεία καταγραφής της συνεδρίας. Πάντα ενεργό όταν το GPU δεν είναι Adreno.</string>
<string name="setup_log_current">Τρέχουσα συνεδρία</string>
<string name="setup_log_delete">Διαγραφή</string>
<string name="setup_log_delete_failed">Δεν ήταν δυνατή η διαγραφή των αρχείων καταγραφής συνεδρίας.</string>
<string name="setup_log_delete_message">Διαγραφή του %1$s; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.</string>
<string name="setup_log_delete_title">Διαγραφή αρχείων καταγραφής συνεδρίας;</string>
<string name="setup_log_empty">Δεν υπάρχουν ακόμη αποθηκευμένα αρχεία καταγραφής συνεδρίας.</string>
<string name="setup_log_export_failed">Δεν ήταν δυνατή η αποθήκευση των αρχείων καταγραφής συνεδρίας.</string>
<string name="setup_log_export_hint">Τα κοινοποιημένα και αποθηκευμένα ZIP περιλαμβάνουν την επιλεγμένη συνεδρία μαζί με σχετικά διαγνωστικά Steam, καταστημάτων, εργαλείων και Battle.net.</string>
<string name="setup_log_export_saved">Τα αρχεία καταγραφής συνεδρίας αποθηκεύτηκαν.</string>
<string name="setup_log_save">Αποθήκευση στη συσκευή</string>
<string name="setup_log_sessions">Διαθέσιμα αρχεία καταγραφής συνεδριών</string>
<string name="setup_log_share">Κοινοποίηση</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Διεπαφή Steam: σχεδίαση με GPU</string>
<string name="setup_steam_gpu_drawing_hint">Με το ζεύγος οδηγών Xclipse, το Steam σχεδιάζει τις σελίδες του μέσω λογισμικού (η σχεδίαση με GPU έδειχνε κατεστραμμένα πλακίδια). Ενεργοποιήστε για ταχύτερη διεπαφή Steam αν φαίνεται σωστή σε αυτό το τηλέφωνο.</string>
<string name="swap_game_screen">Εμφάνιση του παιχνιδιού στην</string>
<string name="swap_game_screen_bottom">Κάτω οθόνη</string>
<string name="swap_game_screen_group">Οθόνη παιχνιδιού</string>
<string name="swap_game_screen_top">Πάνω οθόνη</string>
<string name="swap_screens">Εναλλαγή οθονών</string>
<string name="swap_screens_unavailable">Η κάτω οθόνη δεν είναι ελεύθερη αυτήν τη στιγμή</string>
<string name="upd_block_non_adreno_test">Αυτή είναι η δοκιμαστική έκδοση χωρίς Snapdragon: οι επίσημες εκδόσεις εδώ λειτουργούν μόνο σε Adreno, οπότε δεν εγκαθίστανται πάνω της. Οι νέες δοκιμαστικές εκδόσεις βρίσκονται στο github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Η ενημέρωση μπήκε στην ουρά.</string>
<string name="wincomp_repair_required">Αυτή η λήψη χρειάζεται ενημέρωση.</string>
<string name="wincomp_retry">Επανάληψη ενημέρωσης</string>
</resources>
+127 -4
View File
@@ -673,9 +673,9 @@
<string name="hud_group_metrics">Lecturas</string>
<string name="hud_metric_graph">Gráfico de tiempos de fotograma</string>
<string name="hud_metric_decimal">Tasa de fotogramas con decimal</string>
<string name="hud_metric_low1">1% más bajo</string>
<string name="hud_metric_low01">0.1% más bajo</string>
<string name="hud_metric_low001">0.01% más bajo</string>
<string name="hud_metric_low1" formatted="false">1% más bajo</string>
<string name="hud_metric_low01" formatted="false">0.1% más bajo</string>
<string name="hud_metric_low001" formatted="false">0.01% más bajo</string>
<string name="hud_metric_engine">Motor gráfico</string>
<string name="hud_metric_gpu_model">Nombre de la GPU</string>
<string name="hud_metric_gpu">Carga de la GPU</string>
@@ -3591,7 +3591,7 @@
<string name="integration_shortcuts_maybe_restricted">¿Aparece en gris en Android? Información de la aplicación → ⋮ → Permitir ajustes restringidos</string>
<string name="integration_shortcuts_restricted">Bloqueado por Android · Información de la aplicación → ⋮ → Permitir ajustes restringidos</string>
<string name="integration_shortcuts_service">Teclas del sistema de DroidDeck</string>
<string name="integration_shortcuts_service_description">Mantiene Alt+Tab, la tecla Win y el botón Inicio en una sesión activa de DroidDeck en lugar de Android.</string>
<string name="integration_shortcuts_service_description">Mantiene Alt+Tab, la tecla Win y el botón Inicio/Guía dentro de una sesión de DroidDeck activa y detecta apps en la pantalla inferior. Mientras la sesión está activa lee las teclas de un teclado físico, y lee nombres de paquetes de apps y límites de ventanas solo en este dispositivo. No lee texto, no captura la pantalla ni envía estos datos.</string>
<string name="integration_brightness_needed">Activa Brillo en Configuración → Integración con Steam</string>
<string name="setup_gpu_bridge_off">Puente Mali: desactivado</string>
<string name="setup_gpu_bridge_on">Puente Mali: activado</string>
@@ -3686,6 +3686,52 @@
<string name="gog_repair_action">Reparar archivos</string>
<string name="gog_refresh_action">Renovar la instalación</string>
<string name="gog_select_patch">Seleccionar</string>
<string name="setup_all_files">Acceso a todos los archivos</string>
<string name="setup_local_network">Red local</string>
<string name="libraries_title">Más bibliotecas de Steam</string>
<string name="libraries_hint">Carpetas donde Steam puede instalar y ejecutar juegos, cada una con su propio nombre. Quitarlas conserva los juegos en el disco.</string>
<string name="libraries_add">Añadir biblioteca</string>
<string name="libraries_rename_title">Nombre de la biblioteca</string>
<string name="libraries_pick">Elige una carpeta para la biblioteca</string>
<string name="libraries_full">Como máximo %1$d bibliotecas adicionales</string>
<string name="libraries_overlap">Esa carpeta ya es una biblioteca o está dentro de una</string>
<string name="libraries_missing">%1$s · no disponible</string>
<string name="libraries_applies_next">Se aplica a partir de la próxima sesión de Steam</string>
<string name="bottom_desktop_start">Iniciar el escritorio</string>
<string name="bottom_desktop_mode_touch">Toque directo</string>
<string name="bottom_desktop_trackpad">Panel táctil. Toca para usar el toque directo</string>
<string name="bottom_desktop_direct_touch">Toque directo. Toca para usar el panel táctil</string>
<string name="bottom_desktop_starting">Iniciando el escritorio KDE…</string>
<string name="bottom_desktop_ended">El escritorio se cerró.</string>
<string name="bottom_desktop_failed">El escritorio no pudo iniciarse. Sus registros están en la carpeta bottom-desktop de esta sesión.</string>
<string name="bottom_desktop_off">El escritorio está cerrado.</string>
<string name="libraries_rename">Renombrar</string>
<string name="second_mode_desktop">Escritorio</string>
<string name="bottom_desktop_mode_trackpad">Trackpad</string>
<string name="bottom_desktop_title">Escritorio · %1$s</string>
<string name="setup_dual_screen_launcher">Lanzador en dos pantallas</string>
<string name="setup_dual_screen_launcher_hint">Automático coloca las secciones del lanzador (Steam, Biblioteca, Tiendas y otras) en la segunda pantalla integrada. Activado usa cualquier segunda pantalla. Desactivado mantiene el lanzador en una sola pantalla.</string>
<string name="setup_dual_screen_sessions">Sesiones en dos pantallas</string>
<string name="setup_dual_screen_sessions_hint">Automático coloca el menú del juego en la segunda pantalla integrada en lugar de superponerlo. Activado usa cualquier segunda pantalla. Desactivado mantiene el menú en la pantalla del juego; aún puedes elegir una segunda pantalla desde él.</string>
<string name="stats_hud_frametime">Tiempo de fotograma</string>
<string name="stats_hud_last_seconds">últimos %1$d s</string>
<string name="stats_hud_avg_frame">fotograma medio</string>
<string name="stats_hud_low" formatted="false">1 % más bajo</string>
<string name="stats_hud_slow_frames">fotogramas de más de 33 ms</string>
<string name="stats_hud_per_minute">%1$d / min</string>
<string name="stats_hud_playing_for">Tiempo de juego: %1$s</string>
<string name="stats_hud_core_group">%1$d núcleos, hasta %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d núcleos</string>
<string name="stats_hud_fps_of">FPS de %1$d Hz</string>
<string name="stats_hud_red_mark">La marca roja indica dónde el HUD muestra la temperatura en rojo.</string>
<string name="stats_hud_whole_device">todo el dispositivo</string>
<string name="bottom_hud_group">HUD de la pantalla inferior</string>
<string name="bottom_strip_tabs">Franja en cada pestaña</string>
<string name="bottom_strip_lows" formatted="false">1 % y 0,1 % más bajos</string>
<string name="bottom_strip_cpu_temp">Temperatura de CPU</string>
<string name="stats_hud_battery_temp">Batería</string>
<string name="stats_hud_game_cores">Juego</string>
<string name="setup_privacy_policy">Política de privacidad</string>
<string name="stores_gamejolt_not_owned">No está en tu biblioteca de Game Jolt</string>
<string name="stores_gamejolt_no_build">No hay descarga para Windows de este juego</string>
<string name="stores_gamejolt_no_exe">No se ha encontrado un juego de Windows en la descarga</string>
@@ -3715,4 +3761,81 @@
<string name="stores_gamejolt_filter_status">Estado</string>
<string name="stores_gamejolt_filter_partners">Socios</string>
<string name="stores_gamejolt_followed">Juegos seguidos</string>
<string name="cmgr_powervr_dxvk_managed">DXVK lo proporciona el runtime PowerVR del sistema. Se pueden guardar otros paquetes de DXVK, pero no se aplican con este controlador.</string>
<string name="cmgr_powervr_dxvk_queued">En cola: %1$s (esperando a otro controlador de runtime)</string>
<string name="diagnostics_share_all">Todos los paquetes</string>
<string name="diagnostics_share_games">Juegos y compatibilidad</string>
<string name="diagnostics_share_graphics">Gráficos</string>
<string name="diagnostics_share_session">Sesión y dispositivo</string>
<string name="diagnostics_share_title">Compartir paquetes de diagnóstico</string>
<string name="drivers_built_on">compilado %1$s</string>
<string name="drivers_mali_bridge_auto">Aún no hay un controlador abierto para esta Mali: el puente Mali está activado.</string>
<string name="drivers_mali_bridge_fallback">%1$s aún no está publicado: el puente Mali está activado hasta que lo esté.</string>
<string name="drivers_mali_no_pair_auto">Aún no hay un controlador abierto para esta Mali: se usa %1$s, con el controlador propio del teléfono.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s no está disponible y el controlador de este teléfono indica Vulkan %2$s (Venus necesita 1.2): se usa el puente Mali.</string>
<string name="drivers_mali_unpublished_auto">%1$s aún no está publicado: se usa %2$s hasta que lo esté.</string>
<string name="drivers_pair_updated">%1$s actualizado a la compilación más reciente</string>
<string name="drivers_powervr_builtin">El controlador PowerVR del sistema está seleccionado para el runtime y la pantalla.</string>
<string name="drivers_route_bridge">el puente Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Usa los controladores integrados para esta GPU</string>
<string name="gpu_system_powervr">PowerVR del sistema</string>
<string name="gpuinfo_bridge">Funciona mediante el puente Mali, con el controlador Vulkan propio del teléfono</string>
<string name="gpuinfo_bridge_off">No compatible con el puente Mali desactivado: Turnip necesita una GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">Controlador PowerVR del sistema: la compatibilidad varía según el dispositivo</string>
<string name="gpuinfo_xclipse">Experimental: RADV o el controlador Xclipse del sistema</string>
<string name="libraries_access_allow">Permitir</string>
<string name="libraries_access_hint">Permite el acceso a todos los archivos para que DroidDeck vea las bibliotecas de Steam que otras apps guardan en el almacenamiento interno</string>
<string name="libraries_access_title">Buscar bibliotecas en el almacenamiento interno</string>
<string name="libraries_add_found">Añadir</string>
<string name="libraries_found_hint">Encontrada en este dispositivo · %1$s · %2$s</string>
<string name="libraries_used_inner">Se añadió %1$s, la biblioteca de Steam dentro de la carpeta elegida</string>
<string name="loadstate_steam_update_applying">Aplicando la actualización del cliente de Steam…</string>
<string name="pckb_group_arrows">Teclas de flecha</string>
<string name="pckb_group_function">Esc y teclas de función</string>
<string name="pckb_group_nav">Ins / Supr / Inicio / Fin / Re Pág / Av Pág</string>
<string name="pckb_hidden_keys">Teclas ocultas</string>
<string name="pckb_hidden_keys_hint">Las teclas que casi no usas quedan fuera del teclado.</string>
<string name="pckb_opacity">Opacidad</string>
<string name="pckb_opacity_hint">Redúcela para ver el juego detrás del teclado.</string>
<string name="pckb_size">Tamaño en pantalla</string>
<string name="pckb_size_hint">Parte de la pantalla que ocupa el teclado, con el encabezado incluido.</string>
<string name="session_end_present_fault">La sesión no pudo dibujar en la pantalla: %1$s. Esta pantalla muestra el motivo en lugar de una negra; los registros de la sesión tienen los detalles.</string>
<string name="session_killed_for_memory">La última sesión terminó porque Android cerró DroidDeck para liberar memoria, no por un fallo. Cierra otras apps o baja los ajustes de texturas del juego y vuelve a intentarlo.</string>
<string name="setup_enhanced_diagnostics">Diagnóstico avanzado para la próxima sesión</string>
<string name="setup_enhanced_diagnostics_hint">Captura registros detallados de una sesión. Puede reducir el rendimiento y usar más almacenamiento. Se desactiva solo al iniciar la sesión; revisa los registros antes de compartirlos.</string>
<string name="setup_gpu_bridge_fell_back">El puente Mali no superó su autoprueba en %1$s la última sesión (imágenes o dibujado), así que la sesión usó el par de controladores PanVK.</string>
<string name="setup_gpu_bridge_no_memory_sharing">El puente Mali no encontró ninguna forma de compartir memoria con el controlador en %1$s la última sesión, así que la sesión usó el par de controladores PanVK. El puente no puede funcionar en este teléfono; si lo desactivas, se mantiene el par PanVK.</string>
<string name="setup_gpu_bridge_unusable">%1$s no puede usar el puente Mali (necesita una GPU Mali sin ruta por controlador del sistema), así que este interruptor no tiene efecto aquí.</string>
<string name="setup_gpu_bridged">GPU mediante puente Mali</string>
<string name="setup_gpu_venus_host_too_old">El controlador de %1$s indica Vulkan %2$s. Mali (Venus) necesita Vulkan 1.2 o posterior (gamescope no inicia con versiones anteriores), así que las sesiones omiten Venus y usan el par de controladores PanVK.</string>
<string name="setup_gpu_venus_off">Mali (Venus): desactivado</string>
<string name="setup_gpu_venus_on">Mali (Venus): activado</string>
<string name="setup_graphics_logs">Registros gráficos detallados</string>
<string name="setup_graphics_logs_hint">Los registros de Proton, DXVK y del controlador de cada juego en los registros de la sesión. Siempre activado si la GPU no es una Adreno.</string>
<string name="setup_log_current">Sesión actual</string>
<string name="setup_log_delete">Eliminar</string>
<string name="setup_log_delete_failed">No se pudieron eliminar los registros de la sesión.</string>
<string name="setup_log_delete_message">¿Eliminar %1$s? Esta acción no se puede deshacer.</string>
<string name="setup_log_delete_title">¿Eliminar los registros de la sesión?</string>
<string name="setup_log_empty">Aún no hay registros de sesión guardados.</string>
<string name="setup_log_export_failed">No se pudieron guardar los registros de la sesión.</string>
<string name="setup_log_export_hint">Los ZIP compartidos y guardados incluyen la sesión seleccionada y los diagnósticos relacionados de Steam, la tienda, las herramientas y Battle.net.</string>
<string name="setup_log_export_saved">Registros de la sesión guardados.</string>
<string name="setup_log_save">Guardar en el dispositivo</string>
<string name="setup_log_sessions">Registros de sesión disponibles</string>
<string name="setup_log_share">Compartir</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Interfaz de Steam: dibujado por GPU</string>
<string name="setup_steam_gpu_drawing_hint">Con el par de controladores Xclipse, Steam dibuja sus páginas por software (el dibujado por GPU mostraba mosaicos rotos). Actívalo para una interfaz de Steam más rápida si se ve bien en este teléfono.</string>
<string name="swap_game_screen">Mostrar el juego en</string>
<string name="swap_game_screen_bottom">Pantalla inferior</string>
<string name="swap_game_screen_group">Pantalla del juego</string>
<string name="swap_game_screen_top">Pantalla superior</string>
<string name="swap_screens">Intercambiar pantallas</string>
<string name="swap_screens_unavailable">La pantalla inferior no está libre ahora mismo</string>
<string name="upd_block_non_adreno_test">Esta es la compilación de prueba sin Snapdragon: las compilaciones oficiales solo funcionan aquí en Adreno, así que no se instalan sobre ella. Las nuevas compilaciones de prueba están en github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Actualización en cola.</string>
<string name="wincomp_repair_required">Esta descarga necesita una actualización.</string>
<string name="wincomp_retry">Reintentar actualización</string>
</resources>
+127 -4
View File
@@ -671,9 +671,9 @@
<string name="hud_group_metrics">Lukemat</string>
<string name="hud_metric_graph">Kuva-aikakäyrä</string>
<string name="hud_metric_decimal">Kuvataajuus desimaalilla</string>
<string name="hud_metric_low1">1% alin</string>
<string name="hud_metric_low01">0.1% alin</string>
<string name="hud_metric_low001">0.01% alin</string>
<string name="hud_metric_low1" formatted="false">1% alin</string>
<string name="hud_metric_low01" formatted="false">0.1% alin</string>
<string name="hud_metric_low001" formatted="false">0.01% alin</string>
<string name="hud_metric_engine">Grafiikkamoottori</string>
<string name="hud_metric_gpu_model">GPU:n nimi</string>
<string name="hud_metric_gpu">GPU:n kuorma</string>
@@ -3553,7 +3553,7 @@
<string name="integration_shortcuts_maybe_restricted">Harmaana Androidissa? Sovelluksen tiedot → ⋮ → Salli rajoitetut asetukset</string>
<string name="integration_shortcuts_restricted">Androidin estämä · Sovelluksen tiedot → ⋮ → Salli rajoitetut asetukset</string>
<string name="integration_shortcuts_service">DroidDeckin järjestelmänäppäimet</string>
<string name="integration_shortcuts_service_description">Pitää Alt+Tabin, Win-näppäimen ja Koti-painikkeen aktiivisessa DroidDeck-istunnossa Androidin sijaan.</string>
<string name="integration_shortcuts_service_description">Pitää Alt+Tabin, Win-näppäimen ja Koti/Guide-painikkeen aktiivisessa DroidDeck-istunnossa ja tunnistaa alanäytön sovellukset. Istunnon ollessa aktiivinen se lukee fyysisen näppäimistön näppäimiä sekä sovellusten pakettinimiä ja ikkunoiden rajoja vain tällä laitteella. Se ei lue tekstiä, kaappaa näyttöä eikä lähetä näitä tietoja.</string>
<string name="integration_brightness_needed">Ota Kirkkaus käyttöön kohdassa Käyttöönotto → Steam-integraatio</string>
<string name="setup_gpu_bridge_off">Mali-silta: pois</string>
<string name="setup_gpu_bridge_on">Mali-silta: päällä</string>
@@ -3647,6 +3647,52 @@
<string name="gog_repair_action">Korjaa tiedostot</string>
<string name="gog_refresh_action">Päivitä asennus</string>
<string name="gog_select_patch">Valitse</string>
<string name="setup_all_files">Kaikkien tiedostojen käyttöoikeus</string>
<string name="setup_local_network">Paikallisverkko</string>
<string name="libraries_title">Lisää Steam-kirjastoja</string>
<string name="libraries_hint">Kansiot, joihin Steam voi asentaa pelejä ja joista se voi käynnistää niitä, kukin omalla nimellään. Poistaminen luettelosta säilyttää pelit levyllä.</string>
<string name="libraries_add">Lisää kirjasto</string>
<string name="libraries_rename_title">Kirjaston nimi</string>
<string name="libraries_pick">Valitse kirjaston kansio</string>
<string name="libraries_full">Enintään %1$d lisäkirjastoa</string>
<string name="libraries_overlap">Tämä kansio on jo kirjasto tai sijaitsee kirjastossa</string>
<string name="libraries_missing">%1$s · ei käytettävissä</string>
<string name="libraries_applies_next">Voimassa seuraavasta Steam-istunnosta alkaen</string>
<string name="bottom_desktop_start">Käynnistä työpöytä</string>
<string name="bottom_desktop_mode_touch">Suora kosketus</string>
<string name="bottom_desktop_trackpad">Kosketuslevy. Vaihda suoraan kosketukseen napauttamalla</string>
<string name="bottom_desktop_direct_touch">Suora kosketus. Vaihda kosketuslevyyn napauttamalla</string>
<string name="bottom_desktop_starting">KDE-työpöytää käynnistetään…</string>
<string name="bottom_desktop_ended">Työpöytä sulkeutui.</string>
<string name="bottom_desktop_failed">Työpöytää ei voitu käynnistää. Lokit ovat tämän istunnon bottom-desktop-kansiossa.</string>
<string name="bottom_desktop_off">Työpöytä on suljettu.</string>
<string name="libraries_rename">Nimeä uudelleen</string>
<string name="second_mode_desktop">Työpöytä</string>
<string name="bottom_desktop_mode_trackpad">Kosketuslevy</string>
<string name="bottom_desktop_title">Työpöytä · %1$s</string>
<string name="setup_dual_screen_launcher">Käynnistin kahdella näytöllä</string>
<string name="setup_dual_screen_launcher_hint">Automaattinen näyttää käynnistimen osiot (Steam, Kirjasto, Kaupat ja muut) sisäisellä toisella näytöllä. Päällä käyttää mitä tahansa toista näyttöä. Pois pitää käynnistimen yhdellä näytöllä.</string>
<string name="setup_dual_screen_sessions">Istunnot kahdella näytöllä</string>
<string name="setup_dual_screen_sessions_hint">Automaattinen näyttää pelivalikon sisäisellä toisella näytöllä peittokuvan sijaan. Päällä käyttää mitä tahansa toista näyttöä. Pois pitää valikon pelinäytöllä; siitä voi silti valita toisen näytön.</string>
<string name="stats_hud_frametime">Ruutuaika</string>
<string name="stats_hud_last_seconds">viimeiset %1$d s</string>
<string name="stats_hud_avg_frame">keskimääräinen ruutu</string>
<string name="stats_hud_low" formatted="false">alin 1 %</string>
<string name="stats_hud_slow_frames">yli 33 ms:n ruudut</string>
<string name="stats_hud_per_minute">%1$d / min</string>
<string name="stats_hud_playing_for">Peliaika: %1$s</string>
<string name="stats_hud_core_group">%1$d ydintä, enintään %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d ydintä</string>
<string name="stats_hud_fps_of">FPS %1$d Hz:n taajuudella</string>
<string name="stats_hud_red_mark">Punainen merkki näyttää kohdan, jossa HUD näyttää lämpötilan punaisena.</string>
<string name="stats_hud_whole_device">koko laite</string>
<string name="bottom_hud_group">Alanäytön HUD</string>
<string name="bottom_strip_tabs">Palkki joka välilehdellä</string>
<string name="bottom_strip_lows" formatted="false">alimmat 1 % ja 0,1 %</string>
<string name="bottom_strip_cpu_temp">CPU:n lämpötila</string>
<string name="stats_hud_battery_temp">Akku</string>
<string name="stats_hud_game_cores">Peli</string>
<string name="setup_privacy_policy">Tietosuojakäytäntö</string>
<string name="stores_gamejolt_not_owned">Ei Game Jolt-kirjastossasi</string>
<string name="stores_gamejolt_no_build">Ei Windows-latausta tälle pelille</string>
<string name="stores_gamejolt_no_exe">Latauksesta ei löytynyt Windows-peliä</string>
@@ -3676,4 +3722,81 @@
<string name="stores_gamejolt_filter_status">Tila</string>
<string name="stores_gamejolt_filter_partners">Kumppanit</string>
<string name="stores_gamejolt_followed">Seuratut pelit</string>
<string name="cmgr_powervr_dxvk_managed">DXVK tulee järjestelmän PowerVR-ajoympäristöstä. Muita DXVK-paketteja voi tallentaa, mutta niitä ei oteta käyttöön tämän ajurin kanssa.</string>
<string name="cmgr_powervr_dxvk_queued">Jonossa: %1$s (odottaa toista ajoympäristön ajuria)</string>
<string name="diagnostics_share_all">Kaikki paketit</string>
<string name="diagnostics_share_games">Pelit ja yhteensopivuus</string>
<string name="diagnostics_share_graphics">Grafiikka</string>
<string name="diagnostics_share_session">Istunto ja laite</string>
<string name="diagnostics_share_title">Jaa diagnostiikkapaketteja</string>
<string name="drivers_built_on">koottu %1$s</string>
<string name="drivers_mali_bridge_auto">Mikään avoin ajuri ei vielä kata tätä Malia: Mali-silta on päällä.</string>
<string name="drivers_mali_bridge_fallback">%1$s ei ole vielä julkaistu: Mali-silta on päällä, kunnes se julkaistaan.</string>
<string name="drivers_mali_no_pair_auto">Mikään avoin ajuri ei vielä kata tätä Malia: käytössä on %1$s puhelimen omalla ajurilla.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s ei ole saatavilla, ja tämän puhelimen ajuri ilmoittaa Vulkan-version %2$s (Venus vaatii 1.2): käytössä on Mali-silta.</string>
<string name="drivers_mali_unpublished_auto">%1$s ei ole vielä julkaistu: käytössä on %2$s, kunnes se julkaistaan.</string>
<string name="drivers_pair_updated">%1$s päivitetty uusimpaan versioon</string>
<string name="drivers_powervr_builtin">Järjestelmän PowerVR-ajuri on valittu ajoympäristölle ja näytölle.</string>
<string name="drivers_route_bridge">Mali-silta</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Käyttää tämän GPU:n sisäänrakennettuja ajureita</string>
<string name="gpu_system_powervr">Järjestelmän PowerVR</string>
<string name="gpuinfo_bridge">Toimii Mali-sillan kautta puhelimen omalla Vulkan-ajurilla</string>
<string name="gpuinfo_bridge_off">Ei tuettu, kun Mali-silta on pois päältä: Turnip vaatii Adreno-GPU:n (Snapdragon)</string>
<string name="gpuinfo_powervr">Järjestelmän PowerVR-ajuri: yhteensopivuus vaihtelee laitteittain</string>
<string name="gpuinfo_xclipse">Kokeellinen: RADV tai järjestelmän Xclipse-ajuri</string>
<string name="libraries_access_allow">Salli</string>
<string name="libraries_access_hint">Salli pääsy kaikkiin tiedostoihin, jotta DroidDeck näkee Steam-kirjastot, joita muut sovellukset pitävät sisäisessä tallennustilassa</string>
<string name="libraries_access_title">Etsi kirjastoja sisäisestä tallennustilasta</string>
<string name="libraries_add_found">Lisää</string>
<string name="libraries_found_hint">Löytyi tältä laitteelta · %1$s · %2$s</string>
<string name="libraries_used_inner">Lisättiin %1$s, valitsemasi kansion sisällä oleva Steam-kirjasto</string>
<string name="loadstate_steam_update_applying">Otetaan käyttöön Steam-asiakasohjelman päivitystä…</string>
<string name="pckb_group_arrows">Nuolinäppäimet</string>
<string name="pckb_group_function">Esc ja funktionäppäinrivi</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">Piilotetut näppäimet</string>
<string name="pckb_hidden_keys_hint">Harvoin käyttämäsi näppäimet jäävät pois näppäimistöstä.</string>
<string name="pckb_opacity">Läpinäkyvyys</string>
<string name="pckb_opacity_hint">Pienennä nähdäksesi pelin näppäimistön takana.</string>
<string name="pckb_size">Koko näytöllä</string>
<string name="pckb_size_hint">Näppäimistön osuus näytöstä otsikko mukaan lukien.</string>
<string name="session_end_present_fault">Istunto ei voinut piirtää näytölle: %1$s. Tämä näyttö kertoo syyn mustan ruudun sijaan; tiedot ovat istunnon lokeissa.</string>
<string name="session_killed_for_memory">Edellinen istunto päättyi, koska Android sulki DroidDeckin vapauttaakseen muistia, ei kaatumisen vuoksi. Sulje muut sovellukset tai laske pelin tekstuuriasetuksia ja yritä uudelleen.</string>
<string name="setup_enhanced_diagnostics">Tehostettu diagnostiikka seuraavaan istuntoon</string>
<string name="setup_enhanced_diagnostics_hint">Tallentaa yksityiskohtaiset lokit yhdestä istunnosta. Voi heikentää suorituskykyä ja käyttää enemmän tallennustilaa. Sammuu itsestään istunnon alkaessa; tarkista lokit ennen jakamista.</string>
<string name="setup_gpu_bridge_fell_back">Mali-silta ei läpäissyt omaa testiään laitteella %1$s viime istunnossa (kuvat tai piirto), joten istunto käytti sen sijaan PanVK-ajuriparia.</string>
<string name="setup_gpu_bridge_no_memory_sharing">Mali-silta ei löytänyt tapaa jakaa muistia ajurin kanssa laitteella %1$s viime istunnossa, joten istunto käytti sen sijaan PanVK-ajuriparia. Silta ei voi toimia tässä puhelimessa; sen sammuttaminen pitää PanVK-parin käytössä.</string>
<string name="setup_gpu_bridge_unusable">%1$s ei voi käyttää Mali-siltaa (se vaatii Mali-GPU:n ilman järjestelmäajurin reittiä), joten tällä kytkimellä ei ole tässä vaikutusta.</string>
<string name="setup_gpu_bridged">GPU Mali-sillan kautta</string>
<string name="setup_gpu_venus_host_too_old">Laitteen %1$s ajuri ilmoittaa Vulkan-version %2$s. Mali (Venus) vaatii Vulkan 1.2:n tai uudemman (gamescope ei käynnisty vanhemmalla), joten istunnot ohittavat Venuksen ja käyttävät sen sijaan PanVK-ajuriparia.</string>
<string name="setup_gpu_venus_off">Mali (Venus): pois</string>
<string name="setup_gpu_venus_on">Mali (Venus): päällä</string>
<string name="setup_graphics_logs">Yksityiskohtaiset grafiikkalokit</string>
<string name="setup_graphics_logs_hint">Jokaisen pelin Proton-, DXVK- ja ajurilokit istunnon lokeissa. Aina päällä, kun GPU ei ole Adreno.</string>
<string name="setup_log_current">Nykyinen istunto</string>
<string name="setup_log_delete">Poista</string>
<string name="setup_log_delete_failed">Istuntolokien poistaminen epäonnistui.</string>
<string name="setup_log_delete_message">Poistetaanko %1$s? Tätä ei voi perua.</string>
<string name="setup_log_delete_title">Poistetaanko istuntolokit?</string>
<string name="setup_log_empty">Tallennettuja istuntolokeja ei vielä ole.</string>
<string name="setup_log_export_failed">Istuntolokien tallentaminen epäonnistui.</string>
<string name="setup_log_export_hint">Jaetut ja tallennetut ZIP-tiedostot sisältävät valitun istunnon sekä siihen liittyvän Steam-, kauppa-, työkalu- ja Battle.net-diagnostiikan.</string>
<string name="setup_log_export_saved">Istuntolokit tallennettu.</string>
<string name="setup_log_save">Tallenna laitteelle</string>
<string name="setup_log_sessions">Saatavilla olevat istuntolokit</string>
<string name="setup_log_share">Jaa</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Steam-käyttöliittymä: GPU-piirto</string>
<string name="setup_steam_gpu_drawing_hint">Xclipse-ajuriparin kanssa Steam piirtää sivunsa ohjelmallisesti (GPU-piirto näytti rikkinäisiä ruutuja). Ota käyttöön nopeamman Steam-käyttöliittymän saamiseksi, jos se näyttää oikealta tässä puhelimessa.</string>
<string name="swap_game_screen">Näytä peli täällä</string>
<string name="swap_game_screen_bottom">Alanäyttö</string>
<string name="swap_game_screen_group">Pelinäyttö</string>
<string name="swap_game_screen_top">Ylänäyttö</string>
<string name="swap_screens">Vaihda näytöt</string>
<string name="swap_screens_unavailable">Alanäyttö ei ole nyt vapaana</string>
<string name="upd_block_non_adreno_test">Tämä on ei-Snapdragon-testiversio: viralliset versiot toimivat täällä vain Adrenolla, joten niitä ei asenneta sen päälle. Uudet testiversiot ovat osoitteessa github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Päivitys jonossa.</string>
<string name="wincomp_repair_required">Tämä lataus on päivitettävä.</string>
<string name="wincomp_retry">Yritä päivitystä uudelleen</string>
</resources>
+127 -4
View File
@@ -673,9 +673,9 @@
<string name="hud_group_metrics">Mesures</string>
<string name="hud_metric_graph">Graphique des temps d\'image</string>
<string name="hud_metric_decimal">Fréquence d\'images avec une décimale</string>
<string name="hud_metric_low1">1% les plus bas</string>
<string name="hud_metric_low01">0.1% les plus bas</string>
<string name="hud_metric_low001">0.01% les plus bas</string>
<string name="hud_metric_low1" formatted="false">1% les plus bas</string>
<string name="hud_metric_low01" formatted="false">0.1% les plus bas</string>
<string name="hud_metric_low001" formatted="false">0.01% les plus bas</string>
<string name="hud_metric_engine">Moteur graphique</string>
<string name="hud_metric_gpu_model">Nom du GPU</string>
<string name="hud_metric_gpu">Charge du GPU</string>
@@ -3591,7 +3591,7 @@
<string name="integration_shortcuts_maybe_restricted">Grisé dans Android ? Infos sur l\'appli → ⋮ → Autoriser les paramètres restreints</string>
<string name="integration_shortcuts_restricted">Bloqué par Android · Infos sur l\'appli → ⋮ → Autoriser les paramètres restreints</string>
<string name="integration_shortcuts_service">Touches système DroidDeck</string>
<string name="integration_shortcuts_service_description">Garde Alt+Tab, la touche Win et le bouton Accueil dans une session DroidDeck active plutôt que dans Android.</string>
<string name="integration_shortcuts_service_description">Garde Alt+Tab, la touche Win et le bouton Accueil/Guide dans une session DroidDeck active et détecte les applis sur l\'écran inférieur. Pendant une session active, il lit les touches d\'un clavier physique ainsi que les noms de paquets des applis et les limites des fenêtres, uniquement sur cet appareil. Il ne lit aucun texte, ne capture pas l\'écran et n\'envoie pas ces données.</string>
<string name="integration_brightness_needed">Activez Luminosité dans Paramètres → Intégration Steam</string>
<string name="setup_gpu_bridge_off">Pont Mali : désactivé</string>
<string name="setup_gpu_bridge_on">Pont Mali : activé</string>
@@ -3686,6 +3686,52 @@
<string name="gog_repair_action">Réparer les fichiers</string>
<string name="gog_refresh_action">Actualiser l\'installation</string>
<string name="gog_select_patch">Sélectionner</string>
<string name="setup_all_files">Accès à tous les fichiers</string>
<string name="setup_local_network">Réseau local</string>
<string name="libraries_title">Autres bibliothèques Steam</string>
<string name="libraries_hint">Dossiers où Steam peut installer et lancer des jeux, chacun avec son propre nom. Retirer un dossier conserve ses jeux sur le disque.</string>
<string name="libraries_add">Ajouter une bibliothèque</string>
<string name="libraries_rename_title">Nom de la bibliothèque</string>
<string name="libraries_pick">Choisir un dossier pour la bibliothèque</string>
<string name="libraries_full">%1$d bibliothèques supplémentaires au maximum</string>
<string name="libraries_overlap">Ce dossier est déjà une bibliothèque ou se trouve dans une bibliothèque</string>
<string name="libraries_missing">%1$s · indisponible</string>
<string name="libraries_applies_next">Prend effet à la prochaine session Steam</string>
<string name="bottom_desktop_start">Démarrer le bureau</string>
<string name="bottom_desktop_mode_touch">Toucher direct</string>
<string name="bottom_desktop_trackpad">Pavé tactile. Touchez pour passer au toucher direct</string>
<string name="bottom_desktop_direct_touch">Toucher direct. Touchez pour passer au pavé tactile</string>
<string name="bottom_desktop_starting">Démarrage du bureau KDE…</string>
<string name="bottom_desktop_ended">Le bureau s’est fermé.</string>
<string name="bottom_desktop_failed">Le bureau n’a pas pu démarrer. Ses journaux se trouvent dans le dossier bottom-desktop de cette session.</string>
<string name="bottom_desktop_off">Le bureau est fermé.</string>
<string name="libraries_rename">Renommer</string>
<string name="second_mode_desktop">Bureau</string>
<string name="bottom_desktop_mode_trackpad">Pavé tactile</string>
<string name="bottom_desktop_title">Bureau · %1$s</string>
<string name="setup_dual_screen_launcher">Lanceur sur deux écrans</string>
<string name="setup_dual_screen_launcher_hint">Automatique place les sections du lanceur (Steam, Bibliothèque, Boutiques et autres) sur le second écran intégré. Activé utilise tout second écran. Désactivé garde le lanceur sur un seul écran.</string>
<string name="setup_dual_screen_sessions">Sessions sur deux écrans</string>
<string name="setup_dual_screen_sessions_hint">Automatique place le menu de jeu sur le second écran intégré au lieu de le superposer. Activé utilise tout second écran. Désactivé garde le menu sur l’écran du jeu ; vous pouvez toujours y choisir un second écran.</string>
<string name="stats_hud_frametime">Durée d’image</string>
<string name="stats_hud_last_seconds">%1$d dernières s</string>
<string name="stats_hud_avg_frame">image moyenne</string>
<string name="stats_hud_low" formatted="false">1 % le plus bas</string>
<string name="stats_hud_slow_frames">images de plus de 33 ms</string>
<string name="stats_hud_per_minute">%1$d / min</string>
<string name="stats_hud_playing_for">Temps de jeu : %1$s</string>
<string name="stats_hud_core_group">%1$d cœurs, jusqu’à %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d cœurs</string>
<string name="stats_hud_fps_of">FPS sur %1$d Hz</string>
<string name="stats_hud_red_mark">Le repère rouge indique où le HUD affiche la température en rouge.</string>
<string name="stats_hud_whole_device">appareil entier</string>
<string name="bottom_hud_group">HUD de l’écran inférieur</string>
<string name="bottom_strip_tabs">Bande sur chaque onglet</string>
<string name="bottom_strip_lows" formatted="false">1 % et 0,1 % les plus bas</string>
<string name="bottom_strip_cpu_temp">Température du processeur</string>
<string name="stats_hud_battery_temp">Batterie</string>
<string name="stats_hud_game_cores">Jeu</string>
<string name="setup_privacy_policy">Politique de confidentialité</string>
<string name="stores_gamejolt_not_owned">Absent de votre bibliothèque Game Jolt</string>
<string name="stores_gamejolt_no_build">Pas de téléchargement Windows pour ce jeu</string>
<string name="stores_gamejolt_no_exe">Aucun jeu Windows trouvé dans le téléchargement</string>
@@ -3715,4 +3761,81 @@
<string name="stores_gamejolt_filter_status">Statut</string>
<string name="stores_gamejolt_filter_partners">Partenaires</string>
<string name="stores_gamejolt_followed">Jeux suivis</string>
<string name="cmgr_powervr_dxvk_managed">DXVK est fourni par l\'environnement d\'exécution PowerVR du système. Les autres paquets DXVK peuvent être stockés mais ne sont pas appliqués avec ce pilote.</string>
<string name="cmgr_powervr_dxvk_queued">En attente : %1$s (en attente d\'un autre pilote d\'exécution)</string>
<string name="diagnostics_share_all">Tous les bundles</string>
<string name="diagnostics_share_games">Jeux et compatibilité</string>
<string name="diagnostics_share_graphics">Graphismes</string>
<string name="diagnostics_share_session">Session et appareil</string>
<string name="diagnostics_share_title">Partager les bundles de diagnostic</string>
<string name="drivers_built_on">compilé le %1$s</string>
<string name="drivers_mali_bridge_auto">Aucun pilote ouvert ne couvre encore ce Mali : la passerelle Mali est activée.</string>
<string name="drivers_mali_bridge_fallback">%1$s n\'est pas encore publié : la passerelle Mali est activée en attendant.</string>
<string name="drivers_mali_no_pair_auto">Aucun pilote ouvert ne couvre encore ce Mali : %1$s est utilisé, avec le pilote natif du téléphone.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s n\'est pas disponible et le pilote de ce téléphone signale Vulkan %2$s (Venus nécessite 1.2) : la passerelle Mali est utilisée.</string>
<string name="drivers_mali_unpublished_auto">%1$s n\'est pas encore publié : %2$s est utilisé en attendant.</string>
<string name="drivers_pair_updated">%1$s mis à jour vers la dernière version</string>
<string name="drivers_powervr_builtin">Le pilote PowerVR du système est sélectionné pour l\'exécution et l\'affichage.</string>
<string name="drivers_route_bridge">la passerelle Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Utilise les pilotes intégrés pour ce GPU</string>
<string name="gpu_system_powervr">PowerVR du système</string>
<string name="gpuinfo_bridge">Passe par la passerelle Mali, avec le pilote Vulkan natif du téléphone</string>
<string name="gpuinfo_bridge_off">Non pris en charge avec la passerelle Mali désactivée : Turnip nécessite un GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">Pilote PowerVR du système : la compatibilité varie selon l\'appareil</string>
<string name="gpuinfo_xclipse">Expérimental : RADV ou le pilote Xclipse du système</string>
<string name="libraries_access_allow">Autoriser</string>
<string name="libraries_access_hint">Autorisez l\'accès à tous les fichiers pour que DroidDeck voie les bibliothèques Steam que d\'autres applications conservent dans le stockage interne</string>
<string name="libraries_access_title">Trouver les bibliothèques du stockage interne</string>
<string name="libraries_add_found">Ajouter</string>
<string name="libraries_found_hint">Trouvé sur cet appareil · %1$s · %2$s</string>
<string name="libraries_used_inner">%1$s ajouté, la bibliothèque Steam située dans le dossier choisi</string>
<string name="loadstate_steam_update_applying">Application de la mise à jour du client Steam…</string>
<string name="pckb_group_arrows">Touches fléchées</string>
<string name="pckb_group_function">Échap et touches de fonction</string>
<string name="pckb_group_nav">Inser / Suppr / Début / Fin / Pg préc / Pg suiv</string>
<string name="pckb_hidden_keys">Touches masquées</string>
<string name="pckb_hidden_keys_hint">Les touches rarement utilisées restent hors du clavier.</string>
<string name="pckb_opacity">Opacité</string>
<string name="pckb_opacity_hint">Réduisez-la pour voir le jeu derrière le clavier.</string>
<string name="pckb_size">Taille à l\'écran</string>
<string name="pckb_size_hint">Part de l\'écran occupée par le clavier, en-tête compris.</string>
<string name="session_end_present_fault">La session n\'a pas pu afficher à l\'écran : %1$s. Cet écran en indique la raison au lieu d\'un écran noir ; les journaux de session contiennent les détails.</string>
<string name="session_killed_for_memory">La dernière session s\'est terminée parce qu\'Android a fermé DroidDeck pour libérer de la mémoire, et non à cause d\'un plantage. Fermez d\'autres applications ou réduisez les paramètres de textures du jeu, puis réessayez.</string>
<string name="setup_enhanced_diagnostics">Diagnostic avancé pour la prochaine session</string>
<string name="setup_enhanced_diagnostics_hint">Capture des journaux détaillés pour une session. Peut réduire les performances et utiliser plus de stockage. Se désactive au démarrage de la session ; vérifiez les journaux avant de les partager.</string>
<string name="setup_gpu_bridge_fell_back">La passerelle Mali a échoué à son autotest sur %1$s lors de la dernière session (images ou rendus), la session a donc utilisé la paire de pilotes PanVK à la place.</string>
<string name="setup_gpu_bridge_no_memory_sharing">La passerelle Mali n\'a trouvé aucun moyen de partager la mémoire avec le pilote sur %1$s lors de la dernière session, la session a donc utilisé la paire de pilotes PanVK à la place. La passerelle ne peut pas fonctionner sur ce téléphone ; la désactiver conserve la paire PanVK.</string>
<string name="setup_gpu_bridge_unusable">%1$s ne peut pas utiliser la passerelle Mali (elle nécessite un GPU Mali sans voie par pilote système), cet interrupteur n\'a donc aucun effet ici.</string>
<string name="setup_gpu_bridged">GPU via la passerelle Mali</string>
<string name="setup_gpu_venus_host_too_old">Le pilote de %1$s signale Vulkan %2$s. Mali (Venus) nécessite Vulkan 1.2 ou plus récent (gamescope ne démarre pas avec une version antérieure), les sessions ignorent donc Venus et utilisent la paire de pilotes PanVK à la place.</string>
<string name="setup_gpu_venus_off">Mali (Venus) : désactivé</string>
<string name="setup_gpu_venus_on">Mali (Venus) : activé</string>
<string name="setup_graphics_logs">Journaux graphiques détaillés</string>
<string name="setup_graphics_logs_hint">Les journaux Proton, DXVK et pilote de chaque jeu dans les journaux de session. Toujours activé quand le GPU n\'est pas un Adreno.</string>
<string name="setup_log_current">Session en cours</string>
<string name="setup_log_delete">Supprimer</string>
<string name="setup_log_delete_failed">Impossible de supprimer les journaux de session.</string>
<string name="setup_log_delete_message">Supprimer %1$s ? Cette action est irréversible.</string>
<string name="setup_log_delete_title">Supprimer les journaux de session ?</string>
<string name="setup_log_empty">Aucun journal de session enregistré pour le moment.</string>
<string name="setup_log_export_failed">Impossible d\'enregistrer les journaux de session.</string>
<string name="setup_log_export_hint">Les ZIP partagés et enregistrés contiennent la session sélectionnée ainsi que les diagnostics associés de Steam, de la boutique, des outils et de Battle.net.</string>
<string name="setup_log_export_saved">Journaux de session enregistrés.</string>
<string name="setup_log_save">Enregistrer sur l\'appareil</string>
<string name="setup_log_sessions">Journaux de session disponibles</string>
<string name="setup_log_share">Partager</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Interface Steam : rendu GPU</string>
<string name="setup_steam_gpu_drawing_hint">Avec la paire de pilotes Xclipse, Steam affiche ses pages en logiciel (le rendu GPU produisait des tuiles corrompues). Activez-le pour une interface Steam plus rapide si le rendu est correct sur ce téléphone.</string>
<string name="swap_game_screen">Afficher le jeu sur</string>
<string name="swap_game_screen_bottom">Écran du bas</string>
<string name="swap_game_screen_group">Écran du jeu</string>
<string name="swap_game_screen_top">Écran du haut</string>
<string name="swap_screens">Inverser les écrans</string>
<string name="swap_screens_unavailable">L\'écran du bas n\'est pas libre pour le moment</string>
<string name="upd_block_non_adreno_test">Ceci est la version de test non Snapdragon : les versions officielles ne fonctionnent ici que sur Adreno et ne sont donc pas installées par-dessus. Les nouvelles versions de test sont sur github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Mise à jour en attente.</string>
<string name="wincomp_repair_required">Ce téléchargement nécessite une mise à jour.</string>
<string name="wincomp_retry">Réessayer la mise à jour</string>
</resources>
+127 -4
View File
@@ -671,9 +671,9 @@
<string name="hud_group_metrics">रीडिंग</string>
<string name="hud_metric_graph">फ़्रेम टाइम ग्राफ़</string>
<string name="hud_metric_decimal">दशमलव सहित फ़्रेम दर</string>
<string name="hud_metric_low1">1% लो</string>
<string name="hud_metric_low01">0.1% लो</string>
<string name="hud_metric_low001">0.01% लो</string>
<string name="hud_metric_low1" formatted="false">1% लो</string>
<string name="hud_metric_low01" formatted="false">0.1% लो</string>
<string name="hud_metric_low001" formatted="false">0.01% लो</string>
<string name="hud_metric_engine">ग्राफ़िक्स इंजन</string>
<string name="hud_metric_gpu_model">GPU का नाम</string>
<string name="hud_metric_gpu">GPU लोड</string>
@@ -3553,7 +3553,7 @@
<string name="integration_shortcuts_maybe_restricted">Android में धूसर है? ऐप्लिकेशन की जानकारी → ⋮ → प्रतिबंधित सेटिंग की अनुमति दें</string>
<string name="integration_shortcuts_restricted">Android ने ब्लॉक किया · ऐप्लिकेशन की जानकारी → ⋮ → प्रतिबंधित सेटिंग की अनुमति दें</string>
<string name="integration_shortcuts_service">DroidDeck सिस्टम कुंजियाँ</string>
<string name="integration_shortcuts_service_description">Alt+Tab, Win कुंजी और होम बटन को Android के बजाय सक्रिय DroidDeck सत्र में रखता है।</string>
<string name="integration_shortcuts_service_description">Alt+Tab, Win कुंजी और Home/Guide बटन को सक्रिय DroidDeck सत्र के अंदर रखता है और निचली स्क्रीन पर ऐप पहचानता है। सत्र सक्रिय रहने पर यह हार्डवेयर कीबोर्ड की कुंजियाँ, ऐप पैकेज नाम और विंडो सीमाएँ केवल इसी डिवाइस पर पढ़ता है। यह टेक्स्ट नहीं पढ़ता, स्क्रीन कैप्चर नहीं करता और यह डेटा नहीं भेजता।</string>
<string name="integration_brightness_needed">Setup → Steam इंटीग्रेशन में ब्राइटनेस चालू करें</string>
<string name="setup_gpu_bridge_off">Mali ब्रिज: बंद</string>
<string name="setup_gpu_bridge_on">Mali ब्रिज: चालू</string>
@@ -3647,6 +3647,52 @@
<string name="gog_repair_action">फ़ाइलें रिपेयर करें</string>
<string name="gog_refresh_action">इंस्टॉलेशन रिफ़्रेश करें</string>
<string name="gog_select_patch">चुनें</string>
<string name="setup_all_files">सभी फ़ाइलों का ऐक्सेस</string>
<string name="setup_local_network">स्थानीय नेटवर्क</string>
<string name="libraries_title">और Steam लाइब्रेरी</string>
<string name="libraries_hint">ऐसे फ़ोल्डर जहाँ Steam गेम इंस्टॉल और शुरू कर सकता है, हर एक का अपना नाम है। सूची से हटाने पर गेम डिस्क पर बने रहते हैं।</string>
<string name="libraries_add">लाइब्रेरी जोड़ें</string>
<string name="libraries_rename_title">लाइब्रेरी का नाम</string>
<string name="libraries_pick">लाइब्रेरी के लिए फ़ोल्डर चुनें</string>
<string name="libraries_full">अधिकतम %1$d अतिरिक्त लाइब्रेरी</string>
<string name="libraries_overlap">यह फ़ोल्डर पहले से लाइब्रेरी है या किसी लाइब्रेरी के अंदर है</string>
<string name="libraries_missing">%1$s · उपलब्ध नहीं</string>
<string name="libraries_applies_next">अगले Steam सत्र से लागू होगा</string>
<string name="bottom_desktop_start">डेस्कटॉप शुरू करें</string>
<string name="bottom_desktop_mode_touch">सीधा स्पर्श</string>
<string name="bottom_desktop_trackpad">ट्रैकपैड। सीधे स्पर्श के लिए टैप करें</string>
<string name="bottom_desktop_direct_touch">सीधा स्पर्श। ट्रैकपैड के लिए टैप करें</string>
<string name="bottom_desktop_starting">KDE डेस्कटॉप शुरू हो रहा है…</string>
<string name="bottom_desktop_ended">डेस्कटॉप बंद हो गया।</string>
<string name="bottom_desktop_failed">डेस्कटॉप शुरू नहीं हो सका। इसके लॉग इस सत्र के bottom-desktop फ़ोल्डर में हैं।</string>
<string name="bottom_desktop_off">डेस्कटॉप बंद है।</string>
<string name="libraries_rename">नाम बदलें</string>
<string name="second_mode_desktop">डेस्कटॉप</string>
<string name="bottom_desktop_mode_trackpad">ट्रैकपैड</string>
<string name="bottom_desktop_title">डेस्कटॉप · %1$s</string>
<string name="setup_dual_screen_launcher">दो स्क्रीन पर लॉन्चर</string>
<string name="setup_dual_screen_launcher_hint">स्वचालित, लॉन्चर के भागों (Steam, लाइब्रेरी, स्टोर और अन्य) को अंतर्निर्मित दूसरी स्क्रीन पर रखता है। चालू किसी भी दूसरी स्क्रीन का उपयोग करता है। बंद लॉन्चर को एक स्क्रीन पर रखता है।</string>
<string name="setup_dual_screen_sessions">दो स्क्रीन पर सत्र</string>
<string name="setup_dual_screen_sessions_hint">स्वचालित, गेम मेनू को ऊपर दिखाने के बजाय अंतर्निर्मित दूसरी स्क्रीन पर रखता है। चालू किसी भी दूसरी स्क्रीन का उपयोग करता है। बंद मेनू को गेम स्क्रीन पर रखता है; फिर भी मेनू से दूसरी स्क्रीन चुन सकते हैं।</string>
<string name="stats_hud_frametime">फ़्रेम समय</string>
<string name="stats_hud_last_seconds">पिछले %1$d सेकंड</string>
<string name="stats_hud_avg_frame">औसत फ़्रेम</string>
<string name="stats_hud_low" formatted="false">सबसे कम 1 %</string>
<string name="stats_hud_slow_frames">33 ms से लंबे फ़्रेम</string>
<string name="stats_hud_per_minute">%1$d / मिनट</string>
<string name="stats_hud_playing_for">खेलने का समय: %1$s</string>
<string name="stats_hud_core_group">%1$d कोर, अधिकतम %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d कोर</string>
<string name="stats_hud_fps_of">%1$d Hz पर FPS</string>
<string name="stats_hud_red_mark">लाल निशान वह स्थान बताता है जहाँ HUD तापमान लाल रंग में दिखाता है।</string>
<string name="stats_hud_whole_device">पूरा डिवाइस</string>
<string name="bottom_hud_group">निचली स्क्रीन का HUD</string>
<string name="bottom_strip_tabs">हर टैब पर पट्टी</string>
<string name="bottom_strip_lows" formatted="false">सबसे कम 1 % और 0.1 %</string>
<string name="bottom_strip_cpu_temp">CPU तापमान</string>
<string name="stats_hud_battery_temp">बैटरी</string>
<string name="stats_hud_game_cores">गेम</string>
<string name="setup_privacy_policy">निजता नीति</string>
<string name="stores_gamejolt_not_owned">आपकी Game Jolt लाइब्रेरी में नहीं</string>
<string name="stores_gamejolt_no_build">इस गेम के लिए Windows डाउनलोड नहीं है</string>
<string name="stores_gamejolt_no_exe">डाउनलोड में कोई Windows गेम नहीं मिला</string>
@@ -3676,4 +3722,81 @@
<string name="stores_gamejolt_filter_status">स्थिति</string>
<string name="stores_gamejolt_filter_partners">पार्टनर</string>
<string name="stores_gamejolt_followed">फ़ॉलो किए गए गेम</string>
<string name="cmgr_powervr_dxvk_managed">DXVK सिस्टम PowerVR रनटाइम से मिलता है। अन्य DXVK पैकेज सहेजे जा सकते हैं, लेकिन इस ड्राइवर के साथ लागू नहीं होते।</string>
<string name="cmgr_powervr_dxvk_queued">कतार में: %1$s (किसी दूसरे रनटाइम ड्राइवर का इंतज़ार)</string>
<string name="diagnostics_share_all">सभी बंडल</string>
<string name="diagnostics_share_games">गेम और संगतता</string>
<string name="diagnostics_share_graphics">ग्राफ़िक्स</string>
<string name="diagnostics_share_session">सेशन और डिवाइस</string>
<string name="diagnostics_share_title">डायग्नोस्टिक बंडल शेयर करें</string>
<string name="drivers_built_on">%1$s को बना</string>
<string name="drivers_mali_bridge_auto">इस Mali के लिए अभी कोई ओपन ड्राइवर नहीं है: Mali ब्रिज चालू है।</string>
<string name="drivers_mali_bridge_fallback">%1$s अभी प्रकाशित नहीं हुआ है: होने तक Mali ब्रिज चालू है।</string>
<string name="drivers_mali_no_pair_auto">इस Mali के लिए अभी कोई ओपन ड्राइवर नहीं है: फ़ोन के अपने ड्राइवर पर %1$s इस्तेमाल हो रहा है।</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s उपलब्ध नहीं है, और इस फ़ोन का ड्राइवर Vulkan %2$s बताता है (Venus को 1.2 चाहिए): Mali ब्रिज इस्तेमाल हो रहा है।</string>
<string name="drivers_mali_unpublished_auto">%1$s अभी प्रकाशित नहीं हुआ है: होने तक %2$s इस्तेमाल हो रहा है।</string>
<string name="drivers_pair_updated">%1$s नए बिल्ड पर अपडेट हो गया</string>
<string name="drivers_powervr_builtin">रनटाइम और डिस्प्ले के लिए सिस्टम PowerVR ड्राइवर चुना गया है।</string>
<string name="drivers_route_bridge">Mali ब्रिज</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">इस GPU के लिए बिल्ट-इन ड्राइवर इस्तेमाल करता है</string>
<string name="gpu_system_powervr">सिस्टम PowerVR</string>
<string name="gpuinfo_bridge">Mali ब्रिज के ज़रिए, फ़ोन के अपने Vulkan ड्राइवर पर चलता है</string>
<string name="gpuinfo_bridge_off">Mali ब्रिज बंद होने पर समर्थित नहीं: Turnip को Adreno (Snapdragon) GPU चाहिए</string>
<string name="gpuinfo_powervr">सिस्टम PowerVR ड्राइवर: संगतता डिवाइस के अनुसार बदलती है</string>
<string name="gpuinfo_xclipse">प्रायोगिक: RADV या सिस्टम Xclipse ड्राइवर</string>
<string name="libraries_access_allow">अनुमति दें</string>
<string name="libraries_access_hint">सभी फ़ाइलों की पहुँच दें, ताकि DroidDeck वे Steam लाइब्रेरी देख सके जो दूसरे ऐप इंटरनल स्टोरेज में रखते हैं</string>
<string name="libraries_access_title">इंटरनल स्टोरेज में लाइब्रेरी खोजें</string>
<string name="libraries_add_found">जोड़ें</string>
<string name="libraries_found_hint">इस डिवाइस पर मिली · %1$s · %2$s</string>
<string name="libraries_used_inner">%1$s जोड़ी गई, जो चुने गए फ़ोल्डर के अंदर की Steam लाइब्रेरी है</string>
<string name="loadstate_steam_update_applying">Steam क्लाइंट अपडेट लागू हो रहा है…</string>
<string name="pckb_group_arrows">ऐरो कीज़</string>
<string name="pckb_group_function">Esc और फ़ंक्शन पंक्ति</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">छिपी हुई कीज़</string>
<string name="pckb_hidden_keys_hint">कम इस्तेमाल होने वाली कीज़ कीबोर्ड पर नहीं दिखतीं।</string>
<string name="pckb_opacity">ओपैसिटी</string>
<string name="pckb_opacity_hint">कम करें ताकि कीबोर्ड के पीछे का गेम दिखे।</string>
<string name="pckb_size">स्क्रीन पर आकार</string>
<string name="pckb_size_hint">कीबोर्ड स्क्रीन का कितना हिस्सा लेता है, हेडर समेत।</string>
<string name="session_end_present_fault">सेशन स्क्रीन पर ड्रॉ नहीं कर सका: %1$s। काली स्क्रीन की जगह यह स्क्रीन वजह दिखाती है; ब्योरा सेशन लॉग में है।</string>
<string name="session_killed_for_memory">पिछला सेशन इसलिए खत्म हुआ क्योंकि Android ने मेमोरी खाली करने के लिए DroidDeck को बंद कर दिया, यह क्रैश नहीं था। दूसरे ऐप बंद करें या गेम की टेक्सचर सेटिंग घटाएँ, फिर दोबारा कोशिश करें।</string>
<string name="setup_enhanced_diagnostics">अगले सेशन के लिए एन्हांस्ड डायग्नोस्टिक्स</string>
<string name="setup_enhanced_diagnostics_hint">एक सेशन के लिए विस्तृत लॉग कैप्चर करता है। परफ़ॉर्मेंस घट सकती है और ज़्यादा स्टोरेज लग सकता है। सेशन शुरू होते ही अपने-आप बंद हो जाता है; शेयर करने से पहले लॉग जाँच लें।</string>
<string name="setup_gpu_bridge_fell_back">पिछले सेशन में %1$s पर Mali ब्रिज अपनी सेल्फ़-टेस्ट में फ़ेल हो गया (इमेज या ड्रॉ), इसलिए सेशन ने इसके बजाय PanVK ड्राइवर जोड़ी इस्तेमाल की।</string>
<string name="setup_gpu_bridge_no_memory_sharing">पिछले सेशन में %1$s पर Mali ब्रिज को ड्राइवर के साथ मेमोरी शेयर करने का कोई तरीका नहीं मिला, इसलिए सेशन ने इसके बजाय PanVK ड्राइवर जोड़ी इस्तेमाल की। ब्रिज इस फ़ोन पर काम नहीं कर सकता; इसे बंद करने से PanVK जोड़ी बनी रहती है।</string>
<string name="setup_gpu_bridge_unusable">%1$s Mali ब्रिज इस्तेमाल नहीं कर सकता (इसके लिए ऐसा Mali GPU चाहिए जिसका सिस्टम-ड्राइवर रास्ता न हो), इसलिए यह स्विच यहाँ बेअसर है।</string>
<string name="setup_gpu_bridged">Mali ब्रिज से GPU</string>
<string name="setup_gpu_venus_host_too_old">%1$s पर ड्राइवर Vulkan %2$s बताता है। Mali (Venus) को Vulkan 1.2 या नया चाहिए (पुराने पर gamescope शुरू नहीं होता), इसलिए सेशन Venus छोड़कर PanVK ड्राइवर जोड़ी इस्तेमाल करते हैं।</string>
<string name="setup_gpu_venus_off">Mali (Venus): बंद</string>
<string name="setup_gpu_venus_on">Mali (Venus): चालू</string>
<string name="setup_graphics_logs">विस्तृत ग्राफ़िक्स लॉग</string>
<string name="setup_graphics_logs_hint">हर गेम के Proton, DXVK और ड्राइवर लॉग सेशन लॉग में रहते हैं। GPU Adreno न होने पर हमेशा चालू।</string>
<string name="setup_log_current">मौजूदा सेशन</string>
<string name="setup_log_delete">हटाएँ</string>
<string name="setup_log_delete_failed">सेशन लॉग हटाए नहीं जा सके।</string>
<string name="setup_log_delete_message">%1$s हटाएँ? इसे पहले जैसा नहीं किया जा सकता।</string>
<string name="setup_log_delete_title">सेशन लॉग हटाएँ?</string>
<string name="setup_log_empty">अभी कोई सेशन लॉग सहेजा नहीं गया है।</string>
<string name="setup_log_export_failed">सेशन लॉग सहेजे नहीं जा सके।</string>
<string name="setup_log_export_hint">शेयर और सेव किए गए ZIP में चुना गया सेशन और उससे जुड़े Steam, स्टोर, टूल और Battle.net डायग्नोस्टिक्स शामिल होते हैं।</string>
<string name="setup_log_export_saved">सेशन लॉग सहेजे गए।</string>
<string name="setup_log_save">डिवाइस में सेव करें</string>
<string name="setup_log_sessions">उपलब्ध सेशन लॉग</string>
<string name="setup_log_share">शेयर करें</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Steam इंटरफ़ेस: GPU ड्रॉइंग</string>
<string name="setup_steam_gpu_drawing_hint">Xclipse ड्राइवर जोड़ी के साथ Steam अपने पेज सॉफ़्टवेयर से ड्रॉ करता है (GPU ड्रॉइंग में टाइलें टूटी दिखती थीं)। अगर इस फ़ोन पर सही दिखे, तो तेज़ Steam इंटरफ़ेस के लिए चालू करें।</string>
<string name="swap_game_screen">गेम यहाँ दिखाएँ</string>
<string name="swap_game_screen_bottom">निचली स्क्रीन</string>
<string name="swap_game_screen_group">गेम स्क्रीन</string>
<string name="swap_game_screen_top">ऊपरी स्क्रीन</string>
<string name="swap_screens">स्क्रीन बदलें</string>
<string name="swap_screens_unavailable">निचली स्क्रीन अभी खाली नहीं है</string>
<string name="upd_block_non_adreno_test">यह गैर-Snapdragon टेस्ट बिल्ड है: आधिकारिक बिल्ड यहाँ सिर्फ़ Adreno पर चलते हैं, इसलिए वे इसके ऊपर इंस्टॉल नहीं होते। नए टेस्ट बिल्ड github.com/The412Banner/droiddeck-nonadreno-drivers पर हैं।</string>
<string name="wincomp_repair_queued">अपडेट कतार में है।</string>
<string name="wincomp_repair_required">इस डाउनलोड को अपडेट की ज़रूरत है।</string>
<string name="wincomp_retry">अपडेट दोबारा आज़माएँ</string>
</resources>
+127 -4
View File
@@ -671,9 +671,9 @@
<string name="hud_group_metrics">Mért értékek</string>
<string name="hud_metric_graph">Képkockaidő-grafikon</string>
<string name="hud_metric_decimal">Képkockasebesség tizedesjeggyel</string>
<string name="hud_metric_low1">1% legalacsonyabb</string>
<string name="hud_metric_low01">0.1% legalacsonyabb</string>
<string name="hud_metric_low001">0.01% legalacsonyabb</string>
<string name="hud_metric_low1" formatted="false">1% legalacsonyabb</string>
<string name="hud_metric_low01" formatted="false">0.1% legalacsonyabb</string>
<string name="hud_metric_low001" formatted="false">0.01% legalacsonyabb</string>
<string name="hud_metric_engine">Grafikus motor</string>
<string name="hud_metric_gpu_model">GPU neve</string>
<string name="hud_metric_gpu">GPU-terhelés</string>
@@ -3553,7 +3553,7 @@
<string name="integration_shortcuts_maybe_restricted">Szürke az Androidban? Alkalmazásinformáció → ⋮ → Korlátozott beállítások engedélyezése</string>
<string name="integration_shortcuts_restricted">Az Android letiltotta · Alkalmazásinformáció → ⋮ → Korlátozott beállítások engedélyezése</string>
<string name="integration_shortcuts_service">DroidDeck rendszerbillentyűk</string>
<string name="integration_shortcuts_service_description">Az Alt+Tab, a Win billentyű és a Kezdőlap gomb az aktív DroidDeck-munkamenetben marad az Android helyett.</string>
<string name="integration_shortcuts_service_description">Az Alt+Tab, a Win billentyű és a Kezdőlap/Guide gomb az aktív DroidDeck-munkameneten belül marad, és felismeri az alsó képernyő alkalmazásait. Aktív munkamenet alatt beolvassa a hardveres billentyűzet billentyűit, valamint az alkalmazások csomagnevét és az ablakok határait, csak ezen az eszközön. Nem olvas szöveget, nem rögzíti a képernyőt, és nem küldi el ezeket az adatokat.</string>
<string name="integration_brightness_needed">Kapcsolja be a Fényerőt itt: Beállítás → Steam-integráció</string>
<string name="setup_gpu_bridge_off">Mali híd: ki</string>
<string name="setup_gpu_bridge_on">Mali híd: be</string>
@@ -3647,6 +3647,52 @@
<string name="gog_repair_action">Fájlok javítása</string>
<string name="gog_refresh_action">Telepítés frissítése</string>
<string name="gog_select_patch">Kiválasztás</string>
<string name="setup_all_files">Hozzáférés minden fájlhoz</string>
<string name="setup_local_network">Helyi hálózat</string>
<string name="libraries_title">További Steam-könyvtárak</string>
<string name="libraries_hint">Mappák, amelyekbe a Steam játékokat telepíthet és ahonnan elindíthatja őket, mind saját névvel. Az eltávolítás megtartja a játékokat a lemezen.</string>
<string name="libraries_add">Könyvtár hozzáadása</string>
<string name="libraries_rename_title">Könyvtár neve</string>
<string name="libraries_pick">Válasszon mappát a könyvtárhoz</string>
<string name="libraries_full">Legfeljebb %1$d további könyvtár</string>
<string name="libraries_overlap">Ez a mappa már könyvtár, vagy egy könyvtáron belül van</string>
<string name="libraries_missing">%1$s · nem érhető el</string>
<string name="libraries_applies_next">A következő Steam-munkamenettől érvényes</string>
<string name="bottom_desktop_start">Asztal indítása</string>
<string name="bottom_desktop_mode_touch">Közvetlen érintés</string>
<string name="bottom_desktop_trackpad">Érintőpad. Koppintson a közvetlen érintéshez</string>
<string name="bottom_desktop_direct_touch">Közvetlen érintés. Koppintson az érintőpadhoz</string>
<string name="bottom_desktop_starting">A KDE-asztal indítása…</string>
<string name="bottom_desktop_ended">Az asztal bezárult.</string>
<string name="bottom_desktop_failed">Az asztal nem indult el. A naplók a munkamenet bottom-desktop mappájában vannak.</string>
<string name="bottom_desktop_off">Az asztal be van zárva.</string>
<string name="libraries_rename">Átnevezés</string>
<string name="second_mode_desktop">Asztal</string>
<string name="bottom_desktop_mode_trackpad">Érintőpad</string>
<string name="bottom_desktop_title">Asztal · %1$s</string>
<string name="setup_dual_screen_launcher">Indító két kijelzőn</string>
<string name="setup_dual_screen_launcher_hint">Az Automatikus az indító részeit (Steam, Könyvtár, Áruházak és mások) a beépített második kijelzőre helyezi. A Be bármely második kijelzőt használja. A Ki egy kijelzőn tartja az indítót.</string>
<string name="setup_dual_screen_sessions">Munkamenetek két kijelzőn</string>
<string name="setup_dual_screen_sessions_hint">Az Automatikus a játékmenüt a beépített második kijelzőre helyezi átfedés helyett. A Be bármely második kijelzőt használja. A Ki a játék kijelzőjén tartja a menüt; abból továbbra is választható második kijelző.</string>
<string name="stats_hud_frametime">Képkockaidő</string>
<string name="stats_hud_last_seconds">utolsó %1$d mp</string>
<string name="stats_hud_avg_frame">átlagos képkocka</string>
<string name="stats_hud_low" formatted="false">legalacsonyabb 1 %</string>
<string name="stats_hud_slow_frames">33 ms feletti képkockák</string>
<string name="stats_hud_per_minute">%1$d / perc</string>
<string name="stats_hud_playing_for">Játékidő: %1$s</string>
<string name="stats_hud_core_group">%1$d mag, legfeljebb %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d mag</string>
<string name="stats_hud_fps_of">FPS %1$d Hz mellett</string>
<string name="stats_hud_red_mark">A piros jelzés mutatja, hol jeleníti meg a HUD pirossal a hőmérsékletet.</string>
<string name="stats_hud_whole_device">teljes eszköz</string>
<string name="bottom_hud_group">Alsó kijelző HUD</string>
<string name="bottom_strip_tabs">Sáv minden lapon</string>
<string name="bottom_strip_lows" formatted="false">legalacsonyabb 1 % és 0,1 %</string>
<string name="bottom_strip_cpu_temp">CPU-hőmérséklet</string>
<string name="stats_hud_battery_temp">Akkumulátor</string>
<string name="stats_hud_game_cores">Játék</string>
<string name="setup_privacy_policy">Adatvédelmi szabályzat</string>
<string name="stores_gamejolt_not_owned">Nincs a Game Jolt-könyvtáradban</string>
<string name="stores_gamejolt_no_build">Ehhez a játékhoz nincs Windows-letöltés</string>
<string name="stores_gamejolt_no_exe">A letöltésben nem található Windows-játék</string>
@@ -3676,4 +3722,81 @@
<string name="stores_gamejolt_filter_status">Állapot</string>
<string name="stores_gamejolt_filter_partners">Partnerek</string>
<string name="stores_gamejolt_followed">Követett játékok</string>
<string name="cmgr_powervr_dxvk_managed">A DXVK-t a rendszer PowerVR futtatókörnyezete biztosítja. Más DXVK-csomagok tárolhatók, de ezzel az illesztőprogrammal nem lesznek alkalmazva.</string>
<string name="cmgr_powervr_dxvk_queued">Sorban áll: %1$s (másik futtatókörnyezeti illesztőprogramra vár)</string>
<string name="diagnostics_share_all">Minden csomag</string>
<string name="diagnostics_share_games">Játékok és kompatibilitás</string>
<string name="diagnostics_share_graphics">Grafika</string>
<string name="diagnostics_share_session">Munkamenet és eszköz</string>
<string name="diagnostics_share_title">Diagnosztikai csomagok megosztása</string>
<string name="drivers_built_on">build: %1$s</string>
<string name="drivers_mali_bridge_auto">Ehhez a Mali-hoz még nincs nyílt illesztőprogram: a Mali-híd be van kapcsolva.</string>
<string name="drivers_mali_bridge_fallback">A(z) %1$s még nincs közzétéve: a Mali-híd addig be van kapcsolva.</string>
<string name="drivers_mali_no_pair_auto">Ehhez a Mali-hoz még nincs nyílt illesztőprogram: a(z) %1$s van használatban, a telefon saját illesztőprogramján.</string>
<string name="drivers_mali_old_vulkan_bridge">A(z) %1$s nem érhető el, és a telefon illesztőprogramja Vulkan %2$s verziót jelent (a Venushoz 1.2 kell): a Mali-híd van használatban.</string>
<string name="drivers_mali_unpublished_auto">A(z) %1$s még nincs közzétéve: addig a(z) %2$s van használatban.</string>
<string name="drivers_pair_updated">A(z) %1$s frissítve a legújabb buildre</string>
<string name="drivers_powervr_builtin">A futtatókörnyezethez és a kijelzőhöz a rendszer PowerVR illesztőprogramja van kiválasztva.</string>
<string name="drivers_route_bridge">a Mali-híd</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">A beépített illesztőprogramokat használja ehhez a GPU-hoz</string>
<string name="gpu_system_powervr">Rendszer PowerVR</string>
<string name="gpuinfo_bridge">A Mali-hídon keresztül fut, a telefon saját Vulkan illesztőprogramján</string>
<string name="gpuinfo_bridge_off">Kikapcsolt Mali-híddal nem támogatott: a Turniphoz Adreno (Snapdragon) GPU kell</string>
<string name="gpuinfo_powervr">Rendszer PowerVR illesztőprogram: a kompatibilitás eszközönként eltér</string>
<string name="gpuinfo_xclipse">Kísérleti: RADV vagy a rendszer Xclipse illesztőprogramja</string>
<string name="libraries_access_allow">Engedélyezés</string>
<string name="libraries_access_hint">Engedélyezze a hozzáférést minden fájlhoz, hogy a DroidDeck lássa a más alkalmazások által a belső tárhelyen tartott Steam-könyvtárakat</string>
<string name="libraries_access_title">Könyvtárak keresése a belső tárhelyen</string>
<string name="libraries_add_found">Hozzáadás</string>
<string name="libraries_found_hint">Megtalálva ezen az eszközön · %1$s · %2$s</string>
<string name="libraries_used_inner">Hozzáadva: %1$s, a kiválasztott mappában lévő Steam-könyvtár</string>
<string name="loadstate_steam_update_applying">A Steam-kliens frissítésének alkalmazása…</string>
<string name="pckb_group_arrows">Nyílbillentyűk</string>
<string name="pckb_group_function">Esc és a funkciósor</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">Rejtett billentyűk</string>
<string name="pckb_hidden_keys_hint">A ritkán használt billentyűk nem jelennek meg a billentyűzeten.</string>
<string name="pckb_opacity">Átlátszatlanság</string>
<string name="pckb_opacity_hint">Csökkentse, hogy lássa a játékot a billentyűzet mögött.</string>
<string name="pckb_size">Méret a képernyőn</string>
<string name="pckb_size_hint">A képernyőnek az a hányada, amelyet a billentyűzet a fejléccel együtt elfoglal.</string>
<string name="session_end_present_fault">A munkamenet nem tudott rajzolni a képernyőre: %1$s. Ez a képernyő fekete helyett az okot mutatja; a részletek a munkamenet naplóiban vannak.</string>
<string name="session_killed_for_memory">Az utolsó munkamenet azért ért véget, mert az Android bezárta a DroidDecket memória felszabadítása miatt, nem összeomlás miatt. Zárjon be más alkalmazásokat, vagy csökkentse a játék textúrabeállításait, majd próbálja újra.</string>
<string name="setup_enhanced_diagnostics">Bővített diagnosztika a következő munkamenethez</string>
<string name="setup_enhanced_diagnostics_hint">Részletes naplókat rögzít egy munkamenetre. Csökkentheti a teljesítményt és több tárhelyet használhat. A munkamenet indulásakor magától kikapcsol; megosztás előtt nézze át a naplókat.</string>
<string name="setup_gpu_bridge_fell_back">A Mali-híd megbukott az önteszten a(z) %1$s eszközön az utolsó munkamenetben (képek vagy rajzolás), ezért a munkamenet a PanVK illesztőprogrampárt használta.</string>
<string name="setup_gpu_bridge_no_memory_sharing">A Mali-híd nem talált módot a memória megosztására az illesztőprogrammal a(z) %1$s eszközön az utolsó munkamenetben, ezért a munkamenet a PanVK illesztőprogrampárt használta. A híd ezen a telefonon nem működhet; kikapcsolásával a PanVK-pár marad.</string>
<string name="setup_gpu_bridge_unusable">A(z) %1$s nem tudja használni a Mali-hidat (olyan Mali GPU kell hozzá, amelyhez nincs rendszerillesztőprogramos útvonal), ezért ez a kapcsoló itt hatástalan.</string>
<string name="setup_gpu_bridged">GPU a Mali-hídon át</string>
<string name="setup_gpu_venus_host_too_old">A(z) %1$s eszköz illesztőprogramja Vulkan %2$s verziót jelent. A Mali (Venus) Vulkan 1.2 vagy újabb verziót igényel (régebbin a gamescope nem indul), ezért a munkamenetek kihagyják a Venust, és a PanVK illesztőprogrampárt használják.</string>
<string name="setup_gpu_venus_off">Mali (Venus): ki</string>
<string name="setup_gpu_venus_on">Mali (Venus): be</string>
<string name="setup_graphics_logs">Részletes grafikai naplók</string>
<string name="setup_graphics_logs_hint">Minden játék Proton-, DXVK- és illesztőprogram-naplói a munkamenet naplóiban. Mindig be van kapcsolva, ha a GPU nem Adreno.</string>
<string name="setup_log_current">Aktuális munkamenet</string>
<string name="setup_log_delete">Törlés</string>
<string name="setup_log_delete_failed">A munkamenet naplóit nem sikerült törölni.</string>
<string name="setup_log_delete_message">Törli: %1$s? Ez a művelet nem vonható vissza.</string>
<string name="setup_log_delete_title">Törli a munkamenet naplóit?</string>
<string name="setup_log_empty">Még nincsenek mentett munkamenetnaplók.</string>
<string name="setup_log_export_failed">A munkamenet naplóit nem sikerült menteni.</string>
<string name="setup_log_export_hint">A megosztott és mentett ZIP-fájlok a kiválasztott munkamenetet és a kapcsolódó Steam-, áruház-, eszköz- és Battle.net-diagnosztikát tartalmazzák.</string>
<string name="setup_log_export_saved">A munkamenet naplói mentve.</string>
<string name="setup_log_save">Mentés az eszközre</string>
<string name="setup_log_sessions">Elérhető munkamenetnaplók</string>
<string name="setup_log_share">Megosztás</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Steam felület: GPU-rajzolás</string>
<string name="setup_steam_gpu_drawing_hint">Az Xclipse illesztőprogrampárral a Steam szoftveresen rajzolja az oldalait (a GPU-rajzolás hibás csempéket mutatott). Kapcsolja be a gyorsabb Steam felületért, ha ezen a telefonon jól néz ki.</string>
<string name="swap_game_screen">A játék megjelenítése itt:</string>
<string name="swap_game_screen_bottom">Alsó képernyő</string>
<string name="swap_game_screen_group">Játék képernyője</string>
<string name="swap_game_screen_top">Felső képernyő</string>
<string name="swap_screens">Képernyők cseréje</string>
<string name="swap_screens_unavailable">Az alsó képernyő jelenleg nem szabad</string>
<string name="upd_block_non_adreno_test">Ez a nem Snapdragon teszt build: a hivatalos buildek itt csak Adrenón futnak, ezért nem települnek rá. Az új teszt buildek a github.com/The412Banner/droiddeck-nonadreno-drivers oldalon vannak.</string>
<string name="wincomp_repair_queued">Frissítés sorban.</string>
<string name="wincomp_repair_required">Ezt a letöltést frissíteni kell.</string>
<string name="wincomp_retry">Frissítés újrapróbálása</string>
</resources>
+127 -4
View File
@@ -669,9 +669,9 @@
<string name="hud_group_metrics">Pembacaan</string>
<string name="hud_metric_graph">Grafik waktu bingkai</string>
<string name="hud_metric_decimal">Laju bingkai dengan desimal</string>
<string name="hud_metric_low1">1% terendah</string>
<string name="hud_metric_low01">0.1% terendah</string>
<string name="hud_metric_low001">0.01% terendah</string>
<string name="hud_metric_low1" formatted="false">1% terendah</string>
<string name="hud_metric_low01" formatted="false">0.1% terendah</string>
<string name="hud_metric_low001" formatted="false">0.01% terendah</string>
<string name="hud_metric_engine">Mesin grafis</string>
<string name="hud_metric_gpu_model">Nama GPU</string>
<string name="hud_metric_gpu">Beban GPU</string>
@@ -3524,7 +3524,7 @@
<string name="integration_shortcuts_maybe_restricted">Berwarna abu-abu di Android? Info aplikasi → ⋮ → Izinkan setelan terbatas</string>
<string name="integration_shortcuts_restricted">Diblokir oleh Android · Info aplikasi → ⋮ → Izinkan setelan terbatas</string>
<string name="integration_shortcuts_service">Tombol sistem DroidDeck</string>
<string name="integration_shortcuts_service_description">Menjaga Alt+Tab, tombol Win, dan tombol Beranda tetap di sesi DroidDeck yang aktif, bukan di Android.</string>
<string name="integration_shortcuts_service_description">Menjaga Alt+Tab, tombol Win, dan tombol Beranda/Guide tetap di dalam sesi DroidDeck yang aktif serta mendeteksi aplikasi di layar bawah. Selama sesi aktif, layanan ini membaca tombol dari keyboard fisik serta nama paket aplikasi dan batas jendela, hanya di perangkat ini. Layanan ini tidak membaca teks, tidak merekam layar, dan tidak mengirim data ini.</string>
<string name="integration_brightness_needed">Aktifkan Kecerahan di Penyiapan → Integrasi Steam</string>
<string name="setup_gpu_bridge_off">Jembatan Mali: mati</string>
<string name="setup_gpu_bridge_on">Jembatan Mali: nyala</string>
@@ -3617,6 +3617,52 @@
<string name="gog_repair_action">Perbaiki file</string>
<string name="gog_refresh_action">Segarkan instalasi</string>
<string name="gog_select_patch">Pilih</string>
<string name="setup_all_files">Akses semua file</string>
<string name="setup_local_network">Jaringan lokal</string>
<string name="libraries_title">Pustaka Steam lainnya</string>
<string name="libraries_hint">Folder tempat Steam dapat memasang dan menjalankan gim, masing-masing dengan nama sendiri. Menghapus dari daftar tetap menyimpan gim di disk.</string>
<string name="libraries_add">Tambah pustaka</string>
<string name="libraries_rename_title">Nama pustaka</string>
<string name="libraries_pick">Pilih folder untuk pustaka</string>
<string name="libraries_full">Maksimal %1$d pustaka tambahan</string>
<string name="libraries_overlap">Folder itu sudah menjadi pustaka atau berada di dalamnya</string>
<string name="libraries_missing">%1$s · tidak tersedia</string>
<string name="libraries_applies_next">Berlaku mulai sesi Steam berikutnya</string>
<string name="bottom_desktop_start">Mulai desktop</string>
<string name="bottom_desktop_mode_touch">Sentuhan langsung</string>
<string name="bottom_desktop_trackpad">Trackpad. Ketuk untuk sentuhan langsung</string>
<string name="bottom_desktop_direct_touch">Sentuhan langsung. Ketuk untuk trackpad</string>
<string name="bottom_desktop_starting">Memulai desktop KDE…</string>
<string name="bottom_desktop_ended">Desktop telah ditutup.</string>
<string name="bottom_desktop_failed">Desktop tidak dapat dimulai. Lognya berada di folder bottom-desktop sesi ini.</string>
<string name="bottom_desktop_off">Desktop ditutup.</string>
<string name="libraries_rename">Ganti nama</string>
<string name="second_mode_desktop">Desktop</string>
<string name="bottom_desktop_mode_trackpad">Trackpad</string>
<string name="bottom_desktop_title">Desktop · %1$s</string>
<string name="setup_dual_screen_launcher">Peluncur dua layar</string>
<string name="setup_dual_screen_launcher_hint">Otomatis menempatkan bagian peluncur (Steam, Pustaka, Toko, dan lainnya) di layar kedua bawaan. Aktif memakai layar kedua apa pun. Nonaktif menjaga peluncur di satu layar.</string>
<string name="setup_dual_screen_sessions">Sesi dua layar</string>
<string name="setup_dual_screen_sessions_hint">Otomatis menempatkan menu gim di layar kedua bawaan, bukan sebagai lapisan. Aktif memakai layar kedua apa pun. Nonaktif menjaga menu di layar gim; Anda tetap dapat memilih layar kedua dari menu.</string>
<string name="stats_hud_frametime">Waktu bingkai</string>
<string name="stats_hud_last_seconds">%1$d dtk terakhir</string>
<string name="stats_hud_avg_frame">bingkai rata-rata</string>
<string name="stats_hud_low" formatted="false">1 % terendah</string>
<string name="stats_hud_slow_frames">bingkai di atas 33 ms</string>
<string name="stats_hud_per_minute">%1$d / mnt</string>
<string name="stats_hud_playing_for">Waktu bermain: %1$s</string>
<string name="stats_hud_core_group">%1$d inti, hingga %2$s GHz</string>
<string name="stats_hud_core_group_unknown">%1$d inti</string>
<string name="stats_hud_fps_of">FPS pada %1$d Hz</string>
<string name="stats_hud_red_mark">Tanda merah menunjukkan tempat HUD menampilkan suhu dengan warna merah.</string>
<string name="stats_hud_whole_device">seluruh perangkat</string>
<string name="bottom_hud_group">HUD layar bawah</string>
<string name="bottom_strip_tabs">Bilah di setiap tab</string>
<string name="bottom_strip_lows" formatted="false">1 % dan 0,1 % terendah</string>
<string name="bottom_strip_cpu_temp">Suhu CPU</string>
<string name="stats_hud_battery_temp">Baterai</string>
<string name="stats_hud_game_cores">Permainan</string>
<string name="setup_privacy_policy">Kebijakan privasi</string>
<string name="stores_gamejolt_not_owned">Tidak ada di pustaka Game Jolt Anda</string>
<string name="stores_gamejolt_no_build">Tidak ada unduhan Windows untuk game ini</string>
<string name="stores_gamejolt_no_exe">Tidak ada game Windows di unduhan</string>
@@ -3646,4 +3692,81 @@
<string name="stores_gamejolt_filter_status">Status</string>
<string name="stores_gamejolt_filter_partners">Mitra</string>
<string name="stores_gamejolt_followed">Game yang diikuti</string>
<string name="cmgr_powervr_dxvk_managed">DXVK disediakan oleh runtime PowerVR sistem. Paket DXVK lain dapat disimpan tetapi tidak diterapkan dengan driver ini.</string>
<string name="cmgr_powervr_dxvk_queued">Diantrekan: %1$s (menunggu driver runtime yang berbeda)</string>
<string name="diagnostics_share_all">Semua paket</string>
<string name="diagnostics_share_games">Gim dan kompatibilitas</string>
<string name="diagnostics_share_graphics">Grafis</string>
<string name="diagnostics_share_session">Sesi dan perangkat</string>
<string name="diagnostics_share_title">Bagikan paket diagnostik</string>
<string name="drivers_built_on">dibuat %1$s</string>
<string name="drivers_mali_bridge_auto">Belum ada driver terbuka yang mendukung Mali ini: jembatan Mali aktif.</string>
<string name="drivers_mali_bridge_fallback">%1$s belum dirilis: jembatan Mali aktif sampai dirilis.</string>
<string name="drivers_mali_no_pair_auto">Belum ada driver terbuka yang mendukung Mali ini: %1$s digunakan, pada driver bawaan ponsel.</string>
<string name="drivers_mali_old_vulkan_bridge">%1$s tidak tersedia, dan driver ponsel ini melaporkan Vulkan %2$s (Venus memerlukan 1.2): jembatan Mali digunakan.</string>
<string name="drivers_mali_unpublished_auto">%1$s belum dirilis: %2$s digunakan sampai dirilis.</string>
<string name="drivers_pair_updated">%1$s diperbarui ke build terbaru</string>
<string name="drivers_powervr_builtin">Driver PowerVR sistem dipilih untuk runtime dan tampilan.</string>
<string name="drivers_route_bridge">jembatan Mali</string>
<string name="drivers_route_venus">Mali (Venus)</string>
<string name="gpu_auto_system_hint">Menggunakan driver bawaan untuk GPU ini</string>
<string name="gpu_system_powervr">PowerVR sistem</string>
<string name="gpuinfo_bridge">Berjalan melalui jembatan Mali, pada driver Vulkan bawaan ponsel</string>
<string name="gpuinfo_bridge_off">Tidak didukung saat jembatan Mali nonaktif: Turnip memerlukan GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_powervr">Driver PowerVR sistem: kompatibilitas bervariasi menurut perangkat</string>
<string name="gpuinfo_xclipse">Eksperimental: RADV atau driver Xclipse sistem</string>
<string name="libraries_access_allow">Izinkan</string>
<string name="libraries_access_hint">Izinkan akses ke semua file agar DroidDeck dapat melihat pustaka Steam yang disimpan aplikasi lain di penyimpanan internal</string>
<string name="libraries_access_title">Temukan pustaka di penyimpanan internal</string>
<string name="libraries_add_found">Tambah</string>
<string name="libraries_found_hint">Ditemukan di perangkat ini · %1$s · %2$s</string>
<string name="libraries_used_inner">%1$s ditambahkan, yaitu pustaka Steam di dalam folder yang Anda pilih</string>
<string name="loadstate_steam_update_applying">Menerapkan pembaruan klien Steam…</string>
<string name="pckb_group_arrows">Tombol panah</string>
<string name="pckb_group_function">Esc dan baris fungsi</string>
<string name="pckb_group_nav">Ins / Del / Home / End / PgUp / PgDn</string>
<string name="pckb_hidden_keys">Tombol tersembunyi</string>
<string name="pckb_hidden_keys_hint">Tombol yang jarang Anda gunakan tidak ditampilkan di keyboard.</string>
<string name="pckb_opacity">Opasitas</string>
<string name="pckb_opacity_hint">Turunkan untuk melihat gim di balik keyboard.</string>
<string name="pckb_size">Ukuran di layar</string>
<string name="pckb_size_hint">Porsi layar yang ditempati keyboard, termasuk header.</string>
<string name="session_end_present_fault">Sesi tidak dapat menggambar ke layar: %1$s. Layar ini menampilkan alasannya, bukan layar hitam; log sesi memuat detailnya.</string>
<string name="session_killed_for_memory">Sesi terakhir berakhir karena Android menutup DroidDeck untuk mengosongkan memori, bukan karena crash. Tutup aplikasi lain atau turunkan pengaturan tekstur gim, lalu coba lagi.</string>
<string name="setup_enhanced_diagnostics">Diagnostik lanjutan untuk sesi berikutnya</string>
<string name="setup_enhanced_diagnostics_hint">Merekam log terperinci untuk satu sesi. Dapat mengurangi performa dan menggunakan lebih banyak penyimpanan. Mati sendiri saat sesi dimulai; tinjau log sebelum membagikan.</string>
<string name="setup_gpu_bridge_fell_back">Jembatan Mali gagal dalam uji mandirinya di %1$s pada sesi terakhir (gambar atau penggambaran), sehingga sesi menggunakan pasangan driver PanVK.</string>
<string name="setup_gpu_bridge_no_memory_sharing">Jembatan Mali tidak menemukan cara berbagi memori dengan driver di %1$s pada sesi terakhir, sehingga sesi menggunakan pasangan driver PanVK. Jembatan tidak dapat bekerja di ponsel ini; menonaktifkannya mempertahankan pasangan PanVK.</string>
<string name="setup_gpu_bridge_unusable">%1$s tidak dapat menggunakan jembatan Mali (memerlukan GPU Mali tanpa jalur driver sistem), sehingga sakelar ini tidak berpengaruh di sini.</string>
<string name="setup_gpu_bridged">GPU melalui jembatan Mali</string>
<string name="setup_gpu_venus_host_too_old">Driver di %1$s melaporkan Vulkan %2$s. Mali (Venus) memerlukan Vulkan 1.2 atau lebih baru (gamescope tidak berjalan pada versi lebih lama), sehingga sesi melewati Venus dan menggunakan pasangan driver PanVK.</string>
<string name="setup_gpu_venus_off">Mali (Venus): nonaktif</string>
<string name="setup_gpu_venus_on">Mali (Venus): aktif</string>
<string name="setup_graphics_logs">Log grafis terperinci</string>
<string name="setup_graphics_logs_hint">Log Proton, DXVK, dan driver setiap gim di dalam log sesi. Selalu aktif jika GPU bukan Adreno.</string>
<string name="setup_log_current">Sesi saat ini</string>
<string name="setup_log_delete">Hapus</string>
<string name="setup_log_delete_failed">Tidak dapat menghapus log sesi.</string>
<string name="setup_log_delete_message">Hapus %1$s? Tindakan ini tidak dapat dibatalkan.</string>
<string name="setup_log_delete_title">Hapus log sesi?</string>
<string name="setup_log_empty">Belum ada log sesi tersimpan.</string>
<string name="setup_log_export_failed">Tidak dapat menyimpan log sesi.</string>
<string name="setup_log_export_hint">ZIP yang dibagikan dan disimpan berisi sesi yang dipilih beserta diagnostik Steam, toko, alat, dan Battle.net terkait.</string>
<string name="setup_log_export_saved">Log sesi disimpan.</string>
<string name="setup_log_save">Simpan ke perangkat</string>
<string name="setup_log_sessions">Log sesi yang tersedia</string>
<string name="setup_log_share">Bagikan</string>
<string name="setup_log_size_date">%1$s · %2$s</string>
<string name="setup_steam_gpu_drawing">Antarmuka Steam: penggambaran GPU</string>
<string name="setup_steam_gpu_drawing_hint">Dengan pasangan driver Xclipse, Steam menggambar halamannya lewat perangkat lunak (penggambaran GPU menampilkan kotak rusak). Aktifkan untuk antarmuka Steam yang lebih cepat jika tampilannya benar di ponsel ini.</string>
<string name="swap_game_screen">Tampilkan gim di</string>
<string name="swap_game_screen_bottom">Layar bawah</string>
<string name="swap_game_screen_group">Layar gim</string>
<string name="swap_game_screen_top">Layar atas</string>
<string name="swap_screens">Tukar layar</string>
<string name="swap_screens_unavailable">Layar bawah sedang tidak bebas</string>
<string name="upd_block_non_adreno_test">Ini adalah build uji non-Snapdragon: build resmi di sini hanya berjalan di Adreno, sehingga tidak diinstal menimpanya. Build uji baru ada di github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
<string name="wincomp_repair_queued">Pembaruan diantrekan.</string>
<string name="wincomp_repair_required">Unduhan ini perlu diperbarui.</string>
<string name="wincomp_retry">Coba perbarui lagi</string>
</resources>
Loaded 100 of 406 files, more files were not shown because too many files have changed in this diff. Show more