-
-
Notifications
You must be signed in to change notification settings - Fork 55
ADFA-5067: Support deep links to open projects and files #1651
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
davidschachterADFA
wants to merge
78
commits into
stage
Choose a base branch
from
task/ADFA-5067-deep-links
base: stage
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
78 commits
Select commit
Hold shift + click to select a range
2df5a56
ADFA-5067 | Add deep-link request models, path-traversal guard, and b…
davidschachterADFA 6b96c84
ADFA-5067 | Add DeepLinkActivity as the sole App Link entry point
davidschachterADFA 8c42c35
ADFA-5067 | Handle deep links with no project open in MainActivity
davidschachterADFA 0df3845
ADFA-5067 | Handle deep links to an already-open project in EditorHan…
davidschachterADFA 1109bf1
ADFA-5067 | Add RFC 5785 .well-known/assetlinks.json for App Links ve…
davidschachterADFA a0790b2
ADFA-5067 | Document the deep-link entry point in ARCHITECTURE.md
davidschachterADFA 1078604
ADFA-5067 | Fix deep-link routing race in DeepLinkActivity
davidschachterADFA aea677b
ADFA-5067 | Guard MainActivity's deep-link handling against recreation
davidschachterADFA 3e8fd65
ADFA-5067 | Prevent stacked confirm-close dialogs from dropping a dee…
davidschachterADFA 045aa00
ADFA-5067 | Fix reserved-keyword collision in DeepLinkRequest.parse()
davidschachterADFA ab4be5e
ADFA-5067 | Close symlink escape in resolveWithinDirectory
davidschachterADFA 3ad035b
ADFA-5067 | Sync ARCHITECTURE.md with the DeepLinkActivity routing fix
davidschachterADFA 0f5b682
ADFA-5067 | Acquire RecentProjectDao through Koin, not a raw DB call
davidschachterADFA ee35586
ADFA-5067 | Show a Toast when a deep link fails to parse
davidschachterADFA 4196a34
ADFA-5067 | Handle SecurityException scanning projects for a deep link
davidschachterADFA cc74e65
ADFA-5067 | Don't let a Recents-write failure crash the app
davidschachterADFA b68b50a
ADFA-5067 | Name deliberately-unused catch bindings "_"
davidschachterADFA 45d94cd
ADFA-5067 | Add more reserved-keyword-collision regression cases
davidschachterADFA a451470
ADFA-5067 | Fix three deep-link close/open correctness gaps
davidschachterADFA df705c9
ADFA-5067 | Match line/column from the end of the path, not the start
davidschachterADFA de0e9e8
ADFA-5067 | Add embedded-keyword regression tests; use Truth in this …
davidschachterADFA 86c1f70
ADFA-5067 | Block a new confirm-close while a save-and-close is in fl…
davidschachterADFA 9741df7
ADFA-5067 | Remove dead saveProjectToRecents(); Koin-provide PendingD…
davidschachterADFA e9a1afb
ADFA-5067 | Look up a deep-linked project by name directly, not by sc…
davidschachterADFA f8cb2c9
ADFA-5067 | Deduplicate deep-link line/column parsing
davidschachterADFA 11d1988
ADFA-5067 | Fix path traversal introduced by findValidProjectByName
davidschachterADFA a44feeb
ADFA-5067 | Narrow the Recents-insert catch to SQLException
davidschachterADFA 6a92920
ADFA-5067 | Document MainViewModel's screen-state and event contracts
davidschachterADFA 7e92715
ADFA-5067 | Dismiss the confirm-close dialog in onDestroy()
davidschachterADFA fa73614
ADFA-5067 | Always invoke saveAllAsync's runAfter, even if saveAll th…
davidschachterADFA 2a9c28a
ADFA-5067 | Reject overlapping confirm-close requests instead of hija…
davidschachterADFA 3b7afd7
ADFA-5067 | Fix same-project fast path; dedupe deep-link project lookup
davidschachterADFA dbf4f55
ADFA-5067 | Drain the pending file request even when sync fails
davidschachterADFA 8eb75ca
ADFA-5067 | ActionContextProvider never hands back a finishing activity
davidschachterADFA 06751ad
ADFA-5067 | Add CLEAR_TOP so repeated deep links don't stack MainActi…
davidschachterADFA df7d7b4
ADFA-5067 | Reject "." and embedded separators in a deep-link project…
davidschachterADFA 8343ea9
ADFA-5067 | Widen the Recents-insert catch back to Throwable
davidschachterADFA de62fac
ADFA-5067 | Document the full deep-link routing/file-open flow
davidschachterADFA de7ad7f
Merge remote-tracking branch 'origin/stage' into task/ADFA-5067-deep-…
davidschachterADFA 7a89bd6
ADFA-5067 | Use the inherited SLF4J logger, not android.util.Log
davidschachterADFA cfb708e
Merge branch 'stage' into task/ADFA-5067-deep-links
davidschachterADFA 44d0dbc
Merge branch 'stage' into task/ADFA-5067-deep-links
davidschachterADFA 49c0cd3
ADFA-5067: Validate deep-link scheme/host; fix silent line/column par…
davidschachterADFA 8b8150f
ADFA-5067: Fix deep-link project-open/close race conditions from code…
davidschachterADFA 232d249
ADFA-5067: Document the blank-projectDirPath branch in onNewIntent
davidschachterADFA 17d29d1
Merge branch 'stage' into task/ADFA-5067-deep-links
davidschachterADFA e2e4f03
ADFA-5067: Reindent GitBottomSheetFragment.kt and IEditorHandler.kt t…
davidschachterADFA 47a6eef
ADFA-5067: Fix third-round /code-review xhigh findings
davidschachterADFA dd21d62
ADFA-5067: Fix /code-review max findings
davidschachterADFA 277435d
ADFA-5067: Fix second /code-review max findings pass
davidschachterADFA 84bc0de
ADFA-5067: Fix CodeRabbit findings from the dd21d62b7 review round
davidschachterADFA 696fc4e
ADFA-5067: Fix real findings from another /code-review max pass
davidschachterADFA b8e1c43
ADFA-5067: Fix CodeRabbit nitpicks from the 84bc0de35 review round
davidschachterADFA 656a236
ADFA-5067: Fix test-isolation gap in the single-segment dot-dot test
davidschachterADFA 3590380
ADFA-5067: Fix real findings from a third /code-review max pass
davidschachterADFA 85877e2
ADFA-5067: Fix real findings from a fourth /code-review max pass
davidschachterADFA dcfeb90
Merge branch 'stage' into task/ADFA-5067-deep-links
davidschachterADFA 40abc0a
ADFA-5067: Fix real findings from a fifth /code-review max pass
davidschachterADFA b162f58
Merge remote-tracking branch 'origin/task/ADFA-5067-deep-links' into …
davidschachterADFA 5528e14
ADFA-5067: Fix real findings from a sixth /code-review max pass
davidschachterADFA cf4277c
Merge branch 'stage' into task/ADFA-5067-deep-links
davidschachterADFA 8618ca6
ADFA-5067: Address remaining open CodeRabbit test nitpicks
davidschachterADFA 44e4daa
Merge remote-tracking branch 'origin/task/ADFA-5067-deep-links' into …
davidschachterADFA 6d9c8d9
Merge branch 'stage' into task/ADFA-5067-deep-links
davidschachterADFA 6c9a7da
Merge remote-tracking branch 'origin/stage' into task/ADFA-5067-deep-…
davidschachterADFA 0761657
ADFA-5067: Address hal-eisen-adfa's PR review findings
davidschachterADFA 72a1042
ADFA-5067: Accept the apex domain for App Links, not just www
davidschachterADFA 059400e
ADFA-5067: Keep the deep-link handoff alive through teardown, and rem…
davidschachterADFA 8a2044b
ADFA-5067: Log the save-during-teardown branch instead of leaving it …
davidschachterADFA 90662d9
ADFA-5067: Reject a dot-dot path segment, not any filename containing…
davidschachterADFA 3bcce80
ADFA-5067: Run the save on the application scope, not the activity's
davidschachterADFA e939324
ADFA-5067: Stop exporting MainActivity, the deep-link handoff target
davidschachterADFA 6ccad37
ADFA-5067: Do not let a deep link walk past setup
davidschachterADFA 00739ce
ADFA-5067: Gate the deep link on the toolchain that is on disk, not t…
davidschachterADFA c151bac
Merge stage into task/ADFA-5067-deep-links
davidschachterADFA 6f13637
Merge branch 'stage' into task/ADFA-5067-deep-links
davidschachterADFA 337fde0
ADFA-5067: Don't race Environment.init() in the deep-link setup gate
claude 9afb976
ADFA-5067: Arm a mid-sync same-project deep link for postProjectInit
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
app/src/main/java/com/itsaky/androidide/activities/DeepLinkActivity.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /* | ||
| * This file is part of AndroidIDE. | ||
| * | ||
| * AndroidIDE is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU General Public License as published by | ||
| * the Free Software Foundation, either version 3 of the License, or | ||
| * (at your option) any later version. | ||
| * | ||
| * AndroidIDE is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU General Public License | ||
| * along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>. | ||
| */ | ||
|
|
||
| package com.itsaky.androidide.activities | ||
|
|
||
| import android.app.Activity | ||
| import android.content.Intent | ||
| import android.os.Bundle | ||
| import android.widget.Toast | ||
| import com.itsaky.androidide.activities.editor.EditorActivityKt | ||
| import com.itsaky.androidide.api.ActionContextProvider | ||
| import com.itsaky.androidide.models.DeepLinkRequest | ||
| import com.itsaky.androidide.resources.R.string | ||
|
|
||
| /** | ||
| * The sole `<intent-filter>` holder for `https://appdevforall.org/device/open/project/...` (and the | ||
| * identical `www` subdomain) App Links. Never shows any UI -- it only parses the incoming | ||
| * [android.net.Uri], decides whether a project is already loaded, and hands off to whichever real | ||
| * activity owns that scenario: | ||
| * [MainActivity] if nothing is open yet, or the already-running [EditorActivityKt] (via its | ||
| * `singleTask` `onNewIntent`) if one is. | ||
| * | ||
| * Kept as a plain [Activity] (like [SplashActivity]), not [com.itsaky.androidide.app.BaseIDEActivity], | ||
| * since it never calls `setContentView` and has no theming needs of its own. | ||
| */ | ||
| class DeepLinkActivity : Activity() { | ||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| // A link can arrive before the IDE is usable -- a fresh install, or a Clear Data. Both targets | ||
| // below sit *past* SplashActivity and OnboardingActivity, which are the only things enforcing | ||
| // the terms, the permissions, the JDK and SDK install, the low-storage check and the x86 | ||
| // exit, so following the link now would land the user in an editor that cannot build, on a | ||
| // device the app is supposed to refuse to run on at all (ADFA-5067 review). | ||
| // | ||
| // The link is dropped rather than deferred: carrying a request through an onboarding that can | ||
| // take several minutes, and may not finish at all, is a lot of machinery for a rare case. The | ||
| // user is told, and sent to the normal entry point, which decides what they actually need -- | ||
| // storage, ABI and onboarding are SplashActivity's to enforce, not this activity's to repeat. | ||
| if (!isIdeSetupComplete()) { | ||
| Toast.makeText(this, getString(string.msg_deeplink_setup_incomplete), Toast.LENGTH_LONG).show() | ||
| startActivity(Intent(this, SplashActivity::class.java)) | ||
| finish() | ||
| return | ||
| } | ||
|
|
||
| val request = DeepLinkRequest.parse(intent?.data) | ||
| if (request == null) { | ||
| // A Toast, not flashError -- this activity finishes immediately below, tearing down its | ||
| // window before a view-based Flashbar could ever render. | ||
| Toast.makeText(this, getString(string.msg_deeplink_invalid_link), Toast.LENGTH_LONG).show() | ||
| finish() | ||
| return | ||
| } | ||
|
|
||
| // ActionContextProvider tracks the live EditorHandlerActivity instance (set in its onCreate | ||
| // and re-asserted in onResume, cleared in onDestroy) -- this reflects "is an editor instance | ||
| // already alive to hand this off to via onNewIntent", unlike IProjectManager's workspace, | ||
| // which stays null for the whole duration of a Gradle sync even while EditorActivityKt is | ||
| // already open. | ||
| val target = | ||
|
davidschachterADFA marked this conversation as resolved.
|
||
| if (ActionContextProvider.getActivity() != null) { | ||
| EditorActivityKt::class.java | ||
| } else { | ||
| MainActivity::class.java | ||
| } | ||
|
|
||
| startActivity( | ||
| Intent(this, target).apply { | ||
| putExtra(DeepLinkRequest.EXTRA_KEY, request) | ||
| // FLAG_ACTIVITY_CLEAR_TOP deliberately omitted: MainActivity has no special launch | ||
| // mode, so if an existing MainActivity instance sits lower in this task's back stack | ||
| // under a live EditorActivityKt - which ActionContextProvider.getActivity() can miss | ||
| // even when that editor is alive (see its KDoc) - CLEAR_TOP would destroy that editor | ||
| // to clear the path down to MainActivity, discarding unsaved work with no prompt. | ||
| // Without it, this may at worst stack a redundant MainActivity instance, a harmless | ||
| // nuisance; EditorActivityKt is singleTask, so it always reuses its live instance via | ||
| // onNewIntent regardless of these flags. | ||
| addFlags( | ||
| Intent.FLAG_ACTIVITY_NEW_TASK or | ||
| Intent.FLAG_ACTIVITY_SINGLE_TOP, | ||
| ) | ||
| }, | ||
| ) | ||
| finish() | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.