mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-15 03:47:42 +00:00
added inbuit update downloader for android
This commit is contained in:
parent
0ee748cd10
commit
a4548c69e9
7 changed files with 244 additions and 13 deletions
45
App.tsx
45
App.tsx
|
|
@ -11,7 +11,8 @@ import {
|
|||
StyleSheet,
|
||||
I18nManager,
|
||||
Platform,
|
||||
LogBox
|
||||
LogBox,
|
||||
Linking
|
||||
} from 'react-native';
|
||||
import './src/i18n'; // Initialize i18n
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
|
|
@ -104,6 +105,45 @@ const ThemedApp = () => {
|
|||
|
||||
// GitHub major/minor release overlay
|
||||
const githubUpdate = useGithubMajorUpdate();
|
||||
const [isDownloadingGitHub, setIsDownloadingGitHub] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
|
||||
const handleGithubUpdateAction = async () => {
|
||||
console.log('handleGithubUpdateAction triggered. Release data exists:', !!githubUpdate.releaseData);
|
||||
if (Platform.OS === 'android') {
|
||||
setIsDownloadingGitHub(true);
|
||||
setDownloadProgress(0);
|
||||
try {
|
||||
const { default: AndroidUpdateService } = await import('./src/services/androidUpdateService');
|
||||
if (githubUpdate.releaseData) {
|
||||
console.log('Calling AndroidUpdateService with:', githubUpdate.releaseData.tag_name);
|
||||
const success = await AndroidUpdateService.downloadAndInstallUpdate(
|
||||
githubUpdate.releaseData,
|
||||
(progress) => {
|
||||
setDownloadProgress(progress);
|
||||
}
|
||||
);
|
||||
console.log('AndroidUpdateService result:', success);
|
||||
if (!success) {
|
||||
console.log('Update failed, falling back to browser');
|
||||
// If download fails or no APK found, fallback to browser
|
||||
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
|
||||
}
|
||||
} else if (githubUpdate.releaseUrl) {
|
||||
console.log('No release data, falling back to browser');
|
||||
Linking.openURL(githubUpdate.releaseUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update via Android service', error);
|
||||
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
|
||||
} finally {
|
||||
setIsDownloadingGitHub(false);
|
||||
setDownloadProgress(0);
|
||||
}
|
||||
} else {
|
||||
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// Check onboarding status and initialize services
|
||||
useEffect(() => {
|
||||
|
|
@ -202,6 +242,9 @@ const ThemedApp = () => {
|
|||
releaseUrl={githubUpdate.releaseUrl}
|
||||
onDismiss={githubUpdate.onDismiss}
|
||||
onLater={githubUpdate.onLater}
|
||||
onUpdateAction={handleGithubUpdateAction}
|
||||
isDownloading={isDownloadingGitHub}
|
||||
downloadProgress={downloadProgress}
|
||||
/>
|
||||
<CampaignManager />
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
|
|
|||
|
|
@ -10,13 +10,18 @@ interface Props {
|
|||
releaseUrl?: string;
|
||||
onDismiss: () => void;
|
||||
onLater: () => void;
|
||||
onUpdateAction?: () => void;
|
||||
isDownloading?: boolean;
|
||||
downloadProgress?: number;
|
||||
}
|
||||
|
||||
const MajorUpdateOverlay: React.FC<Props> = ({ visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater }) => {
|
||||
const MajorUpdateOverlay: React.FC<Props> = ({ visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater, onUpdateAction, isDownloading, downloadProgress }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const progressPercent = downloadProgress ? Math.round(downloadProgress * 100) : 0;
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" statusBarTranslucent presentationStyle="overFullScreen" supportedOrientations={['portrait', 'landscape', 'landscape-left', 'landscape-right']}>
|
||||
<View style={styles.backdrop}>
|
||||
|
|
@ -40,10 +45,16 @@ const MajorUpdateOverlay: React.FC<Props> = ({ visible, latestTag, releaseNotes,
|
|||
)}
|
||||
|
||||
<View style={styles.actions}>
|
||||
{releaseUrl ? (
|
||||
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: currentTheme.colors.primary }]} onPress={() => Linking.openURL(releaseUrl)}>
|
||||
<MaterialIcons name="open-in-new" size={18} color="#fff" />
|
||||
<Text style={styles.primaryText}>View release</Text>
|
||||
{releaseUrl || onUpdateAction ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryBtn, { backgroundColor: currentTheme.colors.primary, opacity: isDownloading ? 0.7 : 1 }]}
|
||||
onPress={onUpdateAction || (() => releaseUrl && Linking.openURL(releaseUrl))}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
<MaterialIcons name={isDownloading ? "downloading" : "system-update"} size={18} color="#fff" />
|
||||
<Text style={styles.primaryText}>
|
||||
{isDownloading ? `Downloading... ${progressPercent}%` : (onUpdateAction ? 'Update Now' : 'View release')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ import { fetchLatestGithubRelease, isAnyUpgrade } from '../services/githubReleas
|
|||
|
||||
const DISMISSED_KEY = '@github_major_update_dismissed_version';
|
||||
|
||||
import { GithubReleaseInfo } from '../services/githubReleaseService';
|
||||
|
||||
export interface MajorUpdateData {
|
||||
visible: boolean;
|
||||
latestTag?: string;
|
||||
releaseNotes?: string;
|
||||
releaseUrl?: string;
|
||||
releaseData?: GithubReleaseInfo;
|
||||
onDismiss: () => void;
|
||||
onLater: () => void;
|
||||
refresh: () => void;
|
||||
|
|
@ -22,6 +25,7 @@ export function useGithubMajorUpdate(): MajorUpdateData {
|
|||
const [latestTag, setLatestTag] = useState<string | undefined>();
|
||||
const [releaseNotes, setReleaseNotes] = useState<string | undefined>();
|
||||
const [releaseUrl, setReleaseUrl] = useState<string | undefined>();
|
||||
const [releaseData, setReleaseData] = useState<GithubReleaseInfo | undefined>();
|
||||
|
||||
const check = useCallback(async () => {
|
||||
if (Platform.OS === 'ios') return;
|
||||
|
|
@ -47,6 +51,7 @@ export function useGithubMajorUpdate(): MajorUpdateData {
|
|||
setLatestTag(info.tag_name);
|
||||
setReleaseNotes(info.body);
|
||||
setReleaseUrl(info.html_url);
|
||||
setReleaseData(info);
|
||||
setVisible(true);
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -67,7 +72,7 @@ export function useGithubMajorUpdate(): MajorUpdateData {
|
|||
setVisible(false);
|
||||
}, []);
|
||||
|
||||
return { visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater, refresh: check };
|
||||
return { visible, latestTag, releaseNotes, releaseUrl, releaseData, onDismiss, onLater, refresh: check };
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -230,13 +230,48 @@ const UpdateScreen: React.FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const installUpdate = async () => {
|
||||
const installUpdate = async (options?: { forceGithub?: boolean }) => {
|
||||
try {
|
||||
setIsInstalling(true);
|
||||
setUpdateStatus('downloading');
|
||||
setUpdateProgress(0);
|
||||
setLastOperation(t('updates.status_downloading'));
|
||||
|
||||
const forceGithub = options?.forceGithub === true;
|
||||
|
||||
// If it's a GitHub release update
|
||||
if ((updateInfo?.source === 'github' || forceGithub) && Platform.OS === 'android') {
|
||||
const { default: AndroidUpdateService } = await import('../services/androidUpdateService');
|
||||
|
||||
// We need the full release info with assets
|
||||
const fullRelease = await import('../services/githubReleaseService').then(m => m.fetchLatestGithubRelease());
|
||||
|
||||
if (!fullRelease || !fullRelease.assets) {
|
||||
throw new Error('Could not fetch release assets');
|
||||
}
|
||||
|
||||
setLastOperation('Downloading APK...');
|
||||
// Note: Progress is not currently supported by FileSystem.downloadAsync in the simple way
|
||||
// We'll simulate it for now or implement a more complex downloader later if needed
|
||||
const success = await AndroidUpdateService.downloadAndInstallUpdate(
|
||||
fullRelease,
|
||||
(progress) => {
|
||||
setUpdateProgress(progress * 100);
|
||||
}
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setUpdateProgress(100);
|
||||
setUpdateStatus('success');
|
||||
setLastOperation(t('updates.status_success'));
|
||||
// No alert needed, system installer takes over
|
||||
} else {
|
||||
throw new Error('Download or installation failed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback for OTA / Expo Updates
|
||||
// Simulate progress updates
|
||||
const progressInterval = setInterval(() => {
|
||||
setUpdateProgress(prev => {
|
||||
|
|
@ -509,7 +544,7 @@ const UpdateScreen: React.FC = () => {
|
|||
{ backgroundColor: currentTheme.colors.success || '#34C759' },
|
||||
(isInstalling) && styles.disabledAction
|
||||
]}
|
||||
onPress={installUpdate}
|
||||
onPress={() => installUpdate()}
|
||||
disabled={isInstalling}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
|
|
@ -634,6 +669,23 @@ const UpdateScreen: React.FC = () => {
|
|||
|
||||
<View style={[styles.actionSection, { marginTop: 8 }]}>
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
{Platform.OS === 'android' && (
|
||||
<TouchableOpacity
|
||||
style={[styles.modernButton, { backgroundColor: currentTheme.colors.success || '#34C759', flex: 1 }]}
|
||||
onPress={() => installUpdate({ forceGithub: true })}
|
||||
disabled={isInstalling}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<MaterialIcons name="downloading" size={18} color="white" />
|
||||
) : (
|
||||
<MaterialIcons name="system-update" size={18} color="white" />
|
||||
)}
|
||||
<Text style={styles.modernButtonText}>
|
||||
{isInstalling ? `${t('updates.status_downloading')}...` : t('updates.action_install')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.modernButton, { backgroundColor: currentTheme.colors.primary, flex: 1 }]}
|
||||
onPress={() => github.releaseUrl ? Linking.openURL(github.releaseUrl as string) : null}
|
||||
|
|
|
|||
111
src/services/androidUpdateService.ts
Normal file
111
src/services/androidUpdateService.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as IntentLauncher from 'expo-intent-launcher';
|
||||
import * as Device from 'expo-device';
|
||||
import { Platform } from 'react-native';
|
||||
import { GithubReleaseInfo } from './githubReleaseService';
|
||||
|
||||
class AndroidUpdateService {
|
||||
/**
|
||||
* Downloads and installs the APK from the given GitHub release.
|
||||
* Matches the device architecture to the correct APK asset.
|
||||
*
|
||||
* @param release The GitHub release info containing assets
|
||||
* @returns Promise<boolean> true if installation started, false otherwise
|
||||
*/
|
||||
/**
|
||||
* Downloads and installs the APK from the given GitHub release.
|
||||
* Matches the device architecture to the correct APK asset.
|
||||
*
|
||||
* @param release The GitHub release info containing assets
|
||||
* @param onProgress Optional callback for download progress (0-1)
|
||||
* @returns Promise<boolean> true if installation started, false otherwise
|
||||
*/
|
||||
async downloadAndInstallUpdate(release: GithubReleaseInfo, onProgress?: (progress: number) => void): Promise<boolean> {
|
||||
if (Platform.OS !== 'android') return false;
|
||||
|
||||
const apkUrl = this.getBestApkUrl(release);
|
||||
if (!apkUrl) {
|
||||
console.warn('No suitable APK found for this device architecture');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a temporary file path
|
||||
const filename = `nuvio-update-${release.tag_name}.apk`;
|
||||
// @ts-ignore
|
||||
const downloadDest = `${FileSystem.cacheDirectory}${filename}`;
|
||||
|
||||
// Create a resumable download to track progress
|
||||
const callback = (downloadProgress: FileSystem.DownloadProgressData) => {
|
||||
const progress = downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite;
|
||||
if (onProgress) onProgress(progress);
|
||||
};
|
||||
|
||||
const downloadResumable = FileSystem.createDownloadResumable(
|
||||
apkUrl,
|
||||
downloadDest,
|
||||
{},
|
||||
callback
|
||||
);
|
||||
|
||||
// Download the APK
|
||||
const downloadRes = await downloadResumable.downloadAsync();
|
||||
|
||||
if (!downloadRes || downloadRes.status !== 200) {
|
||||
console.error('Failed to download APK', downloadRes?.status);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get Content URI using Expo's FileSystem
|
||||
const contentUri = await FileSystem.getContentUriAsync(downloadDest);
|
||||
|
||||
// Launch the intent to install
|
||||
console.log('AndroidUpdateService: Starting installation intent with content URI:', contentUri);
|
||||
await IntentLauncher.startActivityAsync('android.intent.action.VIEW', {
|
||||
data: contentUri,
|
||||
flags: 1 | 268435456, // FLAG_GRANT_READ_URI_PERMISSION (1) | FLAG_ACTIVITY_NEW_TASK (0x10000000)
|
||||
type: 'application/vnd.android.package-archive',
|
||||
});
|
||||
|
||||
console.log('AndroidUpdateService: Installation intent started successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error downloading or installing update:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the best APK URL based on device architecture.
|
||||
* Priority: Specific Arch > Universal > First APK found
|
||||
*/
|
||||
private getBestApkUrl(release: GithubReleaseInfo): string | null {
|
||||
console.log('AndroidUpdateService: Finding best APK for release assets:', release.assets?.length);
|
||||
if (!release.assets || release.assets.length === 0) return null;
|
||||
|
||||
const supportedArchs = Device.supportedCpuArchitectures; // e.g. ['arm64-v8a', 'armeabi-v7a']
|
||||
console.log('Device architectures:', supportedArchs);
|
||||
|
||||
// Helper to find asset containing string (case-insensitive)
|
||||
const findAsset = (keyword: string) =>
|
||||
release.assets?.find(a =>
|
||||
a.name.toLowerCase().includes(keyword.toLowerCase()) &&
|
||||
a.name.toLowerCase().endsWith('.apk')
|
||||
);
|
||||
|
||||
// 1. Try to match supported architectures in order
|
||||
if (supportedArchs) {
|
||||
for (const arch of supportedArchs) {
|
||||
const match = findAsset(arch);
|
||||
if (match) return match.browser_download_url;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. No fallback: If no specific architecture match is found, return null.
|
||||
// User requested strict matching to avoid downloading incompatible APKs.
|
||||
console.warn('AndroidUpdateService: No matching APK found for device architectures:', supportedArchs);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AndroidUpdateService();
|
||||
|
|
@ -6,6 +6,13 @@ export interface GithubReleaseInfo {
|
|||
body?: string;
|
||||
html_url?: string;
|
||||
published_at?: string;
|
||||
assets?: Array<{
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
content_type: string;
|
||||
size: number;
|
||||
download_count: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const GITHUB_LATEST_RELEASE_URL = 'https://api.github.com/repos/tapframe/NuvioStreaming/releases/latest';
|
||||
|
|
@ -27,6 +34,7 @@ export async function fetchLatestGithubRelease(): Promise<GithubReleaseInfo | nu
|
|||
body: json.body,
|
||||
html_url: json.html_url,
|
||||
published_at: json.published_at,
|
||||
assets: json.assets,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -69,7 +77,7 @@ export async function fetchTotalDownloads(): Promise<number | null> {
|
|||
});
|
||||
if (!res.ok) return null;
|
||||
const releases = await res.json();
|
||||
|
||||
|
||||
let total = 0;
|
||||
releases.forEach((release: any) => {
|
||||
if (release.assets && Array.isArray(release.assets)) {
|
||||
|
|
@ -78,7 +86,7 @@ export async function fetchTotalDownloads(): Promise<number | null> {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return total;
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -102,12 +110,12 @@ export async function fetchContributors(): Promise<GitHubContributor[] | null> {
|
|||
'User-Agent': `Nuvio/${Platform.OS}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
if (__DEV__) console.error('GitHub API error:', res.status, res.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const contributors = await res.json();
|
||||
return contributors;
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue