Skip to content

Commit bf3f8bc

Browse files
committed
fix continue watching layout, permissions, focus scroll
1 parent 9cfb9b4 commit bf3f8bc

9 files changed

Lines changed: 187 additions & 38 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
3232

3333
<uses-permission android:name="android.permission.READ_TV_LISTINGS" />
34+
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
3435

3536
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
3637
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

android/app/src/main/java/com/leanbitlab/ltvL/MainActivity.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public class MainActivity extends FlutterActivity {
7474
private final String APPS_EVENT_CHANNEL = "me.efesser.flauncher/event_apps";
7575
private final String NETWORK_EVENT_CHANNEL = "me.efesser.flauncher/event_network";
7676
private final String NOTIFICATIONS_EVENT_CHANNEL = "me.efesser.flauncher/event_notifications";
77+
private MethodChannel.Result pendingPermissionResult;
7778

7879
@Override
7980
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
@@ -147,6 +148,17 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
147148
case "requestOverlayPermission" -> result.success(requestOverlayPermission());
148149
case "checkAccessibilityPermission" -> result.success(isAccessibilityServiceEnabled());
149150
case "requestAccessibilityPermission" -> result.success(openAccessibilitySettings());
151+
case "checkWatchNextPermission" -> result.success(checkWatchNextPermission());
152+
case "requestWatchNextPermission" -> {
153+
if (checkWatchNextPermission()) {
154+
result.success(true);
155+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
156+
pendingPermissionResult = result;
157+
requestPermissions(new String[]{"android.permission.READ_TV_LISTINGS"}, 1002);
158+
} else {
159+
result.success(true);
160+
}
161+
}
150162
case "getWatchNextPrograms" -> result.success(getWatchNextPrograms());
151163
case "getWatchNextPoster" -> {
152164
String posterArtUri = call.argument("posterArtUri");
@@ -1135,4 +1147,29 @@ private boolean openAccessibilitySettings() {
11351147
} catch (Exception ignored) {}
11361148
return false;
11371149
}
1150+
1151+
private boolean checkWatchNextPermission() {
1152+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1153+
return checkSelfPermission("android.permission.READ_TV_LISTINGS") == PackageManager.PERMISSION_GRANTED;
1154+
}
1155+
return true;
1156+
}
1157+
1158+
private void requestWatchNextPermission() {
1159+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1160+
requestPermissions(new String[]{"android.permission.READ_TV_LISTINGS"}, 1002);
1161+
}
1162+
}
1163+
1164+
@Override
1165+
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
1166+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
1167+
if (requestCode == 1002) {
1168+
if (pendingPermissionResult != null) {
1169+
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
1170+
pendingPermissionResult.success(granted);
1171+
pendingPermissionResult = null;
1172+
}
1173+
}
1174+
}
11381175
}

android/app/src/main/java/me/efesser/flauncher/MainActivity.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public class MainActivity extends FlutterActivity {
7474
private final String APPS_EVENT_CHANNEL = "me.efesser.flauncher/event_apps";
7575
private final String NETWORK_EVENT_CHANNEL = "me.efesser.flauncher/event_network";
7676
private final String NOTIFICATIONS_EVENT_CHANNEL = "me.efesser.flauncher/event_notifications";
77+
private MethodChannel.Result pendingPermissionResult;
7778

7879
@Override
7980
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
@@ -147,6 +148,17 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
147148
case "requestOverlayPermission" -> result.success(requestOverlayPermission());
148149
case "checkAccessibilityPermission" -> result.success(isAccessibilityServiceEnabled());
149150
case "requestAccessibilityPermission" -> result.success(openAccessibilitySettings());
151+
case "checkWatchNextPermission" -> result.success(checkWatchNextPermission());
152+
case "requestWatchNextPermission" -> {
153+
if (checkWatchNextPermission()) {
154+
result.success(true);
155+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
156+
pendingPermissionResult = result;
157+
requestPermissions(new String[]{"android.permission.READ_TV_LISTINGS"}, 1002);
158+
} else {
159+
result.success(true);
160+
}
161+
}
150162
case "getWatchNextPrograms" -> result.success(getWatchNextPrograms());
151163
case "getWatchNextPoster" -> {
152164
String posterArtUri = call.argument("posterArtUri");
@@ -1095,4 +1107,29 @@ private boolean openAccessibilitySettings() {
10951107
} catch (Exception ignored) {}
10961108
return false;
10971109
}
1110+
1111+
private boolean checkWatchNextPermission() {
1112+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1113+
return checkSelfPermission("android.permission.READ_TV_LISTINGS") == PackageManager.PERMISSION_GRANTED;
1114+
}
1115+
return true;
1116+
}
1117+
1118+
private void requestWatchNextPermission() {
1119+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1120+
requestPermissions(new String[]{"android.permission.READ_TV_LISTINGS"}, 1002);
1121+
}
1122+
}
1123+
1124+
@Override
1125+
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
1126+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
1127+
if (requestCode == 1002) {
1128+
if (pendingPermissionResult != null) {
1129+
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
1130+
pendingPermissionResult.success(granted);
1131+
pendingPermissionResult = null;
1132+
}
1133+
}
1134+
}
10981135
}

lib/flauncher.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import 'package:flauncher/widgets/focus_aware_app_bar.dart';
2929
import 'package:flutter/material.dart';
3030
import 'package:provider/provider.dart';
3131
import 'package:flauncher/widgets/continue_watching_row.dart';
32+
import 'package:flauncher/providers/watch_next_service.dart';
33+
import 'package:flauncher/providers/settings_service.dart';
3234
import 'package:flauncher/l10n/app_localizations.dart';
3335

3436
import 'models/category.dart';
@@ -99,8 +101,12 @@ class _FLauncherState extends State<FLauncher> {
99101
);
100102

101103
Widget _sections(List<LauncherSection> sections) {
104+
final settingsService = Provider.of<SettingsService>(context, listen: false);
105+
final watchNextService = Provider.of<WatchNextService>(context, listen: false);
106+
final bool continueWatchingActive = settingsService.showContinueWatching && watchNextService.programs.isNotEmpty;
107+
102108
List<Widget> children = [];
103-
bool firstCategoryFound = false;
109+
bool firstCategoryFound = continueWatchingActive;
104110

105111
for (var section in sections) {
106112
final Key sectionKey = Key(section.id.toString());

lib/flauncher_channel.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,22 @@ class FLauncherChannel {
215215
return false;
216216
}
217217
}
218+
219+
Future<bool> checkWatchNextPermission() async {
220+
try {
221+
final bool? allowed = await _methodChannel.invokeMethod<bool>("checkWatchNextPermission");
222+
return allowed ?? false;
223+
} catch (_) {
224+
return false;
225+
}
226+
}
227+
228+
Future<bool> requestWatchNextPermission() async {
229+
try {
230+
final bool? allowed = await _methodChannel.invokeMethod<bool>("requestWatchNextPermission");
231+
return allowed ?? false;
232+
} catch (_) {
233+
return false;
234+
}
235+
}
218236
}

lib/providers/watch_next_service.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ class WatchNextService extends ChangeNotifier {
2727

2828
Future<void> refresh() async {
2929
try {
30+
final bool hasPermission = await checkPermission();
31+
if (!hasPermission) {
32+
if (_programs.isNotEmpty) {
33+
_programs = [];
34+
notifyListeners();
35+
}
36+
return;
37+
}
38+
3039
final List<Map<dynamic, dynamic>> list = await _channel.getWatchNextPrograms();
3140
final List<WatchNextProgram> newPrograms = [];
3241
for (final map in list) {
@@ -55,6 +64,18 @@ class WatchNextService extends ChangeNotifier {
5564
}
5665
}
5766

67+
Future<bool> checkPermission() async {
68+
return await _channel.checkWatchNextPermission();
69+
}
70+
71+
Future<bool> requestPermission() async {
72+
final bool granted = await _channel.requestWatchNextPermission();
73+
if (granted) {
74+
await refresh();
75+
}
76+
return granted;
77+
}
78+
5879
Future<bool> launch(WatchNextProgram program) async {
5980
if (program.intentUri.isNotEmpty) {
6081
return await _channel.launchWatchNextProgram(program.intentUri);

lib/widgets/continue_watching_row.dart

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flauncher/models/watch_next_program.dart';
22
import 'package:flauncher/providers/apps_service.dart';
33
import 'package:flauncher/providers/settings_service.dart';
44
import 'package:flauncher/providers/watch_next_service.dart';
5+
import 'package:flauncher/actions.dart';
56
import 'package:flutter/material.dart';
67
import 'package:flutter/services.dart';
78
import 'package:provider/provider.dart';
@@ -23,45 +24,48 @@ class ContinueWatchingRow extends StatelessWidget {
2324
return const SizedBox.shrink();
2425
}
2526

26-
return Column(
27-
crossAxisAlignment: CrossAxisAlignment.start,
28-
children: [
29-
Padding(
30-
padding: const EdgeInsets.only(left: 16, bottom: 8, top: 8),
31-
child: Text(
32-
"Continue Watching",
33-
style: Theme.of(context).textTheme.titleLarge!.copyWith(
34-
shadows: [
35-
const Shadow(
36-
color: Colors.black54,
37-
offset: Offset(1, 1),
38-
blurRadius: 8,
39-
)
40-
],
27+
return Padding(
28+
padding: const EdgeInsets.symmetric(vertical: 8),
29+
child: Column(
30+
crossAxisAlignment: CrossAxisAlignment.start,
31+
children: [
32+
Padding(
33+
padding: const EdgeInsets.only(left: 16, bottom: 8),
34+
child: Text(
35+
"Continue Watching",
36+
style: Theme.of(context).textTheme.titleLarge!.copyWith(
37+
shadows: [
38+
const Shadow(
39+
color: Colors.black54,
40+
offset: Offset(1, 1),
41+
blurRadius: 8,
42+
)
43+
],
44+
),
4145
),
4246
),
43-
),
44-
SizedBox(
45-
height: 150, // Height of card + padding
46-
child: ListView.builder(
47-
clipBehavior: Clip.none,
48-
padding: const EdgeInsets.all(8),
49-
scrollDirection: Axis.horizontal,
50-
itemCount: programs.length,
51-
itemBuilder: (context, index) {
52-
final program = programs[index];
53-
return Padding(
54-
padding: const EdgeInsets.symmetric(horizontal: 8),
55-
child: WatchNextCard(
56-
program: program,
57-
appsService: appsService,
58-
watchNextService: watchNextService,
59-
),
60-
);
61-
},
47+
SizedBox(
48+
height: 150, // Height of card + padding
49+
child: ListView.builder(
50+
clipBehavior: Clip.none,
51+
padding: const EdgeInsets.all(8),
52+
scrollDirection: Axis.horizontal,
53+
itemCount: programs.length,
54+
itemBuilder: (context, index) {
55+
final program = programs[index];
56+
return Padding(
57+
padding: const EdgeInsets.symmetric(horizontal: 8),
58+
child: WatchNextCard(
59+
program: program,
60+
appsService: appsService,
61+
watchNextService: watchNextService,
62+
),
63+
);
64+
},
65+
),
6266
),
63-
),
64-
],
67+
],
68+
),
6569
);
6670
},
6771
);
@@ -96,6 +100,14 @@ class _WatchNextCardState extends State<WatchNextCard> {
96100
setState(() {
97101
_focused = _focusNode.hasFocus;
98102
});
103+
if (_focusNode.hasFocus) {
104+
Scrollable.ensureVisible(
105+
context,
106+
alignment: 0.5,
107+
curve: Curves.easeInOut,
108+
duration: const Duration(milliseconds: 100),
109+
);
110+
}
99111
});
100112
}
101113

@@ -137,6 +149,9 @@ class _WatchNextCardState extends State<WatchNextCard> {
137149
event.logicalKey == LogicalKeyboardKey.gameButtonA) {
138150
_onPressed();
139151
return KeyEventResult.handled;
152+
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
153+
Actions.invoke(context, const MoveFocusToSettingsIntent());
154+
return KeyEventResult.handled;
140155
}
141156
}
142157
return KeyEventResult.ignored;

lib/widgets/settings/misc_panel_page.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import 'package:flauncher/providers/settings_service.dart';
3+
import 'package:flauncher/providers/watch_next_service.dart';
34
import 'package:flauncher/widgets/rounded_switch_list_tile.dart';
45
import 'package:flauncher/widgets/settings/focusable_settings_tile.dart';
56
import 'package:flutter/material.dart';
@@ -63,7 +64,19 @@ class MiscPanelPage extends StatelessWidget {
6364
),
6465
RoundedSwitchListTile(
6566
value: settingsService.showContinueWatching,
66-
onChanged: (value) => settingsService.setShowContinueWatching(value),
67+
onChanged: (value) async {
68+
if (value) {
69+
final watchNextService = Provider.of<WatchNextService>(context, listen: false);
70+
final hasPermission = await watchNextService.checkPermission();
71+
if (!hasPermission) {
72+
final granted = await watchNextService.requestPermission();
73+
if (!granted) {
74+
return;
75+
}
76+
}
77+
}
78+
settingsService.setShowContinueWatching(value);
79+
},
6780
title: Text("Show Continue Watching on Home", style: Theme.of(context).textTheme.bodyMedium),
6881
secondary: const Icon(Icons.play_circle_outline),
6982
),

test/providers/watch_next_service_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ void main() {
1313
mockChannel = MockFLauncherChannel();
1414
// Default stubs
1515
when(mockChannel.getWatchNextPrograms()).thenAnswer((_) async => []);
16+
when(mockChannel.checkWatchNextPermission()).thenAnswer((_) async => true);
1617
});
1718

1819
group('WatchNextService Initialization', () {

0 commit comments

Comments
 (0)