Skip to content

Commit 90708e2

Browse files
committed
fix: lower minSdk to 26 for broader Android device compatibility
- Changed minSdk from 31 (Android 12) to 26 (Android 8.0) - This allows installation on devices running Android 8.0+ - Gemini Nano features will gracefully degrade on older devices - Fixes APK installation failure on devices below Android 12
1 parent 98d6dd6 commit 90708e2

3 files changed

Lines changed: 216 additions & 1 deletion

File tree

app/android/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ android {
2525

2626
defaultConfig {
2727
applicationId = "com.airo.superapp"
28-
minSdk = 31 // Required for Gemini Nano AI Core library
28+
minSdk = 26 // Android 8.0 - broader device compatibility
2929
targetSdk = 36 // Target latest Android for Pixel 9
3030
versionCode = flutter.versionCode
3131
versionName = flutter.versionName
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import 'package:equatable/equatable.dart';
2+
3+
/// Source of the audio track
4+
enum BeatsSource { youtube, soundcloud, local, unknown }
5+
6+
/// Represents a track from Beats (YouTube/SoundCloud)
7+
class BeatsTrack extends Equatable {
8+
final String id;
9+
final String title;
10+
final String artist;
11+
final String? thumbnailUrl;
12+
final Duration duration;
13+
final BeatsSource source;
14+
final String? sourceUrl;
15+
final String? streamUrl; // HLS stream URL (populated after resolution)
16+
17+
const BeatsTrack({
18+
required this.id,
19+
required this.title,
20+
required this.artist,
21+
this.thumbnailUrl,
22+
this.duration = Duration.zero,
23+
this.source = BeatsSource.unknown,
24+
this.sourceUrl,
25+
this.streamUrl,
26+
});
27+
28+
BeatsTrack copyWith({
29+
String? id,
30+
String? title,
31+
String? artist,
32+
String? thumbnailUrl,
33+
Duration? duration,
34+
BeatsSource? source,
35+
String? sourceUrl,
36+
String? streamUrl,
37+
}) {
38+
return BeatsTrack(
39+
id: id ?? this.id,
40+
title: title ?? this.title,
41+
artist: artist ?? this.artist,
42+
thumbnailUrl: thumbnailUrl ?? this.thumbnailUrl,
43+
duration: duration ?? this.duration,
44+
source: source ?? this.source,
45+
sourceUrl: sourceUrl ?? this.sourceUrl,
46+
streamUrl: streamUrl ?? this.streamUrl,
47+
);
48+
}
49+
50+
@override
51+
List<Object?> get props => [id, title, artist, thumbnailUrl, duration, source, sourceUrl, streamUrl];
52+
}
53+
54+
/// Search result containing tracks and pagination info
55+
class BeatsSearchResult extends Equatable {
56+
final List<BeatsTrack> tracks;
57+
final String? nextPageToken;
58+
final int totalResults;
59+
60+
const BeatsSearchResult({
61+
required this.tracks,
62+
this.nextPageToken,
63+
this.totalResults = 0,
64+
});
65+
66+
@override
67+
List<Object?> get props => [tracks, nextPageToken, totalResults];
68+
}
69+
70+
/// Stream session with HLS manifest URL
71+
class BeatsStreamSession extends Equatable {
72+
final String sessionId;
73+
final String trackId;
74+
final String hlsManifestUrl;
75+
final DateTime createdAt;
76+
final DateTime expiresAt;
77+
78+
const BeatsStreamSession({
79+
required this.sessionId,
80+
required this.trackId,
81+
required this.hlsManifestUrl,
82+
required this.createdAt,
83+
required this.expiresAt,
84+
});
85+
86+
bool get isExpired => DateTime.now().isAfter(expiresAt);
87+
88+
Duration get remainingTime => expiresAt.difference(DateTime.now());
89+
90+
@override
91+
List<Object?> get props => [sessionId, trackId, hlsManifestUrl, createdAt, expiresAt];
92+
}
93+
94+
/// Search state for UI
95+
enum BeatsSearchState { idle, searching, resolving, success, error }
96+
97+
/// UI state for Beats search
98+
class BeatsSearchUiState extends Equatable {
99+
final BeatsSearchState state;
100+
final String query;
101+
final List<BeatsTrack> results;
102+
final String? errorMessage;
103+
final BeatsTrack? resolvingTrack;
104+
105+
const BeatsSearchUiState({
106+
this.state = BeatsSearchState.idle,
107+
this.query = '',
108+
this.results = const [],
109+
this.errorMessage,
110+
this.resolvingTrack,
111+
});
112+
113+
BeatsSearchUiState copyWith({
114+
BeatsSearchState? state,
115+
String? query,
116+
List<BeatsTrack>? results,
117+
String? errorMessage,
118+
BeatsTrack? resolvingTrack,
119+
}) {
120+
return BeatsSearchUiState(
121+
state: state ?? this.state,
122+
query: query ?? this.query,
123+
results: results ?? this.results,
124+
errorMessage: errorMessage ?? this.errorMessage,
125+
resolvingTrack: resolvingTrack ?? this.resolvingTrack,
126+
);
127+
}
128+
129+
@override
130+
List<Object?> get props => [state, query, results, errorMessage, resolvingTrack];
131+
}
132+
133+
/// Result wrapper for repository operations
134+
class BeatsResult<T> {
135+
final T? data;
136+
final String? error;
137+
138+
const BeatsResult.success(this.data) : error = null;
139+
const BeatsResult.failure(this.error) : data = null;
140+
141+
bool get isSuccess => data != null;
142+
bool get isFailure => error != null;
143+
}
144+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import '../models/beats_models.dart';
2+
3+
/// Repository interface for Beats audio streaming
4+
abstract class BeatsRepository {
5+
/// Search for tracks by query
6+
/// Returns search results with pagination support
7+
Future<BeatsResult<BeatsSearchResult>> searchTracks(
8+
String query, {
9+
int limit = 10,
10+
String? pageToken,
11+
});
12+
13+
/// Resolve a URL (YouTube/SoundCloud) to track metadata
14+
Future<BeatsResult<BeatsTrack>> resolveUrl(String url);
15+
16+
/// Create a streaming session for a track
17+
/// Returns HLS manifest URL with session info
18+
Future<BeatsResult<BeatsStreamSession>> createStreamSession(String trackId);
19+
20+
/// Get recently resolved tracks (cached)
21+
Future<List<BeatsTrack>> getRecentTracks({int limit = 20});
22+
23+
/// Clear cached tracks
24+
Future<void> clearCache();
25+
26+
/// Check if URL is a valid source URL
27+
bool isValidSourceUrl(String url);
28+
}
29+
30+
/// URL patterns for source detection
31+
class BeatsUrlPatterns {
32+
static final RegExp youtube = RegExp(
33+
r'^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/|youtube\.com/shorts/)[\w-]+',
34+
caseSensitive: false,
35+
);
36+
37+
static final RegExp soundcloud = RegExp(
38+
r'^(https?://)?(www\.)?soundcloud\.com/[\w-]+/[\w-]+',
39+
caseSensitive: false,
40+
);
41+
42+
/// Check if URL is a supported source
43+
static bool isSupported(String url) {
44+
return youtube.hasMatch(url) || soundcloud.hasMatch(url);
45+
}
46+
47+
/// Get source type from URL
48+
static BeatsSource getSource(String url) {
49+
if (youtube.hasMatch(url)) return BeatsSource.youtube;
50+
if (soundcloud.hasMatch(url)) return BeatsSource.soundcloud;
51+
return BeatsSource.unknown;
52+
}
53+
54+
/// Extract YouTube video ID from URL
55+
static String? extractYoutubeId(String url) {
56+
// Handle youtu.be format
57+
final shortMatch = RegExp(r'youtu\.be/([\w-]+)').firstMatch(url);
58+
if (shortMatch != null) return shortMatch.group(1);
59+
60+
// Handle youtube.com/watch?v= format
61+
final watchMatch = RegExp(r'youtube\.com/watch\?v=([\w-]+)').firstMatch(url);
62+
if (watchMatch != null) return watchMatch.group(1);
63+
64+
// Handle youtube.com/shorts/ format
65+
final shortsMatch = RegExp(r'youtube\.com/shorts/([\w-]+)').firstMatch(url);
66+
if (shortsMatch != null) return shortsMatch.group(1);
67+
68+
return null;
69+
}
70+
}
71+

0 commit comments

Comments
 (0)