Skip to content

Commit

Permalink
feat: deep links (Android only)
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-anders committed Jan 14, 2025
1 parent fcc09d4 commit ae73f9c
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 0 deletions.
23 changes: 23 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="lichess.org" android:scheme="https"
android:pathPrefix="/training" />
<data
android:host="lichess.org" android:scheme="https"
android:pathPrefix="/study" />
<!-- Either game or challenge -->
<data
android:host="lichess.org" android:scheme="https"
android:pathPattern="/........" />
<data
android:host="lichess.org" android:scheme="https"
android:pathPattern="/......../black" />
<data
android:host="lichess.org" android:scheme="https"
android:pathPattern="/......../white" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
Expand Down
25 changes: 25 additions & 0 deletions android/app/src/main/kotlin/org/lichess/mobileV2/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package org.lichess.mobileV2

import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import androidx.core.view.ViewCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
Expand All @@ -11,6 +13,7 @@ import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private val GESTURES_CHANNEL = "mobile.lichess.org/gestures_exclusion"
private val SYSTEM_CHANNEL = "mobile.lichess.org/system"
private val APP_LINK_CHANNEL = "mobile.lichess.org/app_links"

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Expand Down Expand Up @@ -48,6 +51,28 @@ class MainActivity: FlutterActivity() {
}
}
}

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, APP_LINK_CHANNEL).setMethodCallHandler {
call, result ->
when (call.method) {
"getAppLink" -> {
result.success(intent.data?.toString())
}
else -> {
result.notImplemented()
}
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, APP_LINK_CHANNEL)
.invokeMethod("newAppLink", intent.data?.toString())
}

private fun decodeExclusionRects(inputRects: List<Map<String, Int>>): List<Rect> =
Expand Down
1 change: 1 addition & 0 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ targets:
- lib/src/model/**/*.dart
- lib/src/network/*.dart
- lib/src/db/*.dart
- lib/src/utils/app_links.dart
- lib/src/**/*_providers.dart
16 changes: 16 additions & 0 deletions lib/src/navigation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/l10n/l10n.dart';
import 'package:lichess_mobile/src/network/connectivity.dart';
import 'package:lichess_mobile/src/utils/app_links.dart';
import 'package:lichess_mobile/src/utils/l10n_context.dart';
import 'package:lichess_mobile/src/utils/navigation.dart';
import 'package:lichess_mobile/src/view/home/home_tab_screen.dart';
import 'package:lichess_mobile/src/view/puzzle/puzzle_tab_screen.dart';
import 'package:lichess_mobile/src/view/settings/settings_tab_screen.dart';
Expand Down Expand Up @@ -127,6 +129,20 @@ class BottomNavScaffold extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final currentTab = ref.watch(currentBottomTabProvider);

ref.listen(appLinksProvider, (_, appLinkUri) {
switch (appLinkUri) {
case AsyncData(:final Uri value):
{
final builder = resolveAppLinkUri(value);
if (builder != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
pushPlatformRoute(context, rootNavigator: true, builder: builder);
});
}
}
}
});

switch (Theme.of(context).platform) {
case TargetPlatform.android:
return Scaffold(
Expand Down
76 changes: 76 additions & 0 deletions lib/src/utils/app_links.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'dart:async';

import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart';
import 'package:lichess_mobile/src/view/game/archived_game_screen.dart';
import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart';
import 'package:lichess_mobile/src/view/study/study_screen.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'app_links.g.dart';

final _logger = Logger('App Links');

WidgetBuilder? resolveAppLinkUri(Uri appLinkUri) {
if (appLinkUri.pathSegments.length < 2 || appLinkUri.pathSegments[1].isEmpty) {
return null;
}

final id = appLinkUri.pathSegments[1];

switch (appLinkUri.pathSegments[0]) {
case 'study':
return (context) => StudyScreen(id: StudyId(id));
case 'training':
return (context) => PuzzleScreen(angle: PuzzleAngle.fromKey('mix'), puzzleId: PuzzleId(id));
case _:
{
final gameId = GameId(appLinkUri.pathSegments[0]);
final orientation = appLinkUri.pathSegments.getOrNull(2);
if (gameId.isValid) {
return (context) => ArchivedGameScreen(
gameId: gameId,
orientation: orientation == 'black' ? Side.black : Side.white,
);
} else {
// TODO if it's not a game, it's a challenge.
// So we should show a accept/decline screen here.
return null;
}
}
}
}

/// Receives incoming app links from the operating system (iOS or Android).
///
/// There are two ways a new app link can be received:
/// 1) The app is not running, the user presses a supported link which then opens the app.
/// 2) Like 1), but the app is already running in the background.
///
/// Use [resolveAppLinkUri] to get the a builder for the screen that should be displayed
/// for the given app link URI.
@Riverpod(keepAlive: true)
class AppLinks extends _$AppLinks {
static const _channel = MethodChannel('mobile.lichess.org/app_links');

@override
Future<Uri?> build() async {
final initialLink = await _channel.invokeMethod<String?>('getAppLink');
_logger.info('Initial app link: $initialLink');

_channel.setMethodCallHandler((call) async {
if (call.method == 'newAppLink') {
final uri = Uri.parse(call.arguments as String);
_logger.info('Received new app link: $uri');
state = AsyncValue.data(uri);
}
});

return initialLink != null ? Uri.parse(initialLink) : null;
}
}

0 comments on commit ae73f9c

Please sign in to comment.