Merge pull request 'feature/unit2/UNIT2-#3-Add-download-new-version-feature' (#4) from feature/unit2/UNIT2-#3-Add-download-new-version-feature into develop

Reviewed-on: http://git.agusandelnorte.gov.ph:3000/SoftwareDevelopmentSection/unit2-null-safety-repository/pulls/4
feature/passo/PASSO-#1-Sync-data-from-device-to-postgre-and-vice-versa
rodolfobacuinjr 2023-01-25 17:52:10 +08:00
commit 1ffc20f3b6
19 changed files with 519 additions and 119 deletions

View File

@ -51,6 +51,7 @@ android {
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {

View File

@ -1,10 +1,25 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.unit2">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DELETE_PACKAGES" />
<application
android:label="unit2"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<provider
android:authorities = "${applicationId}.fileprovider"
android:exported = "false"
android:grantUriPermissions = "true"
android:name = "androidx.core.content.FileProvider">
<meta-data
android:name = "android.support.FILE_PROVIDER_PATHS"
android:resource = "@xml/file_provider_path" />
</provider >
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -0,0 +1,25 @@
// Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app;
import android.app.Application;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link android.app.Application}, adding multidex support.
*/
public class FlutterMultiDexApplication extends Application {
@Override
@CallSuper
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="."/>
<files-path
name="files"
path="."/>
<cache-path
name="cache"
path="."/>
<external-path
name="external"
path="."/>
<external-cache-path
name="external_cache"
path="."/>
<external-files-path
name="external_file"
path="."/>
</paths>

View File

@ -3,6 +3,7 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:unit2/model/login_data/user_info/user_data.dart';
import 'package:unit2/model/login_data/version_info.dart';
import 'package:unit2/screens/unit2/login/functions/get_app_version.dart';
import 'package:unit2/sevices/login_service/auth_service.dart';
import '../../utils/scanner.dart';
@ -21,7 +22,8 @@ class UserBloc extends Bloc<UserEvent, UserState> {
emit(SplashScreen());
VersionInfo versionInfo = await AuthService.instance.getVersionInfo();
_versionInfo = versionInfo;
emit(VersionLoaded(versionInfo: _versionInfo));
String apkVersion = await getAppVersion();
emit(VersionLoaded(versionInfo: _versionInfo,apkVersion: apkVersion));
} catch (e) {
emit(UserError(
message: e.toString(),

View File

@ -27,6 +27,7 @@ class GetUuid extends UserEvent {
GetUuid();
}
class LoadVersion extends UserEvent {}
class UuidLogin extends UserEvent {

View File

@ -37,7 +37,8 @@ class UserLoggedIn extends UserState{
class VersionLoaded extends UserState {
final VersionInfo? versionInfo;
VersionLoaded({this.versionInfo});
final String? apkVersion;
VersionLoaded({this.versionInfo,this.apkVersion});
@override
List<Object> get props => [versionInfo!];
}

View File

@ -0,0 +1,17 @@
import 'package:cool_alert/cool_alert.dart';
import 'package:flutter/material.dart';
import '../../../../theme-data.dart/colors.dart';
showAlert(context,Function confirm) {
CoolAlert.show(
context: context,
type: CoolAlertType.error,
title: 'Download Failed!',
text: 'Make sure you have internet connection. Please try again.',
loopAnimation: false,
confirmBtnText: "Try again",
confirmBtnColor: primary,
onConfirmBtnTap: confirm(),
backgroundColor: Colors.black);
}

View File

@ -1,96 +1,248 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_app_installer/easy_app_installer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_progress_hud/flutter_progress_hud.dart';
import 'package:flutter_svg/svg.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:unit2/bloc/bloc/user_bloc.dart';
import 'package:unit2/screens/unit2/login/components/showAlert.dart';
import 'package:unit2/theme-data.dart/btn-style.dart';
import '../../../../theme-data.dart/colors.dart';
import '../../../../utils/cpu_architecture.dart';
import '../../../../utils/text_container.dart';
import '../../../../widgets/wave.dart';
class Update extends StatefulWidget {
const Update({super.key});
final String apkVersion;
final String currenVersion;
const Update(
{super.key, required this.apkVersion, required this.currenVersion});
@override
State<Update> createState() => _UpdateState();
}
class _UpdateState extends State<Update> {
String progressRating = '0';
bool downloading = false;
bool isDownloaded = false;
bool asyncCall = false;
Dio dio = Dio();
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 25),
height: MediaQuery.of(context).size.height,
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
'assets/svgs/download.svg',
height: 200.0,
width: 200.0,
allowDrawingOutsideViewBox: true,
child: Stack(children: [
BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is VersionLoaded) {
return ProgressHUD(
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 40, vertical: 25),
height: MediaQuery.of(context).size.height,
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
'assets/svgs/download.svg',
height: 200.0,
width: 200.0,
allowDrawingOutsideViewBox: true,
),
const SizedBox(
height: 5,
),
Text("UPDATE REQUIRED!",
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.displaySmall!
.copyWith(color: primary, fontSize: 28)),
],
),
const SizedBox(
height: 10,
),
RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text: 'Your app version ',
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
children: <TextSpan>[
TextSpan(
text: widget.apkVersion,
style: const TextStyle(
color: primary,
fontWeight: FontWeight.bold)),
const TextSpan(
text:
" did not match with the latest version ",
style: TextStyle(color: Colors.black)),
TextSpan(
text: widget.currenVersion.toString(),
style: const TextStyle(
color: primary,
fontWeight: FontWeight.bold)),
const TextSpan(
text:
". Download the app latest version to procceed using the uniT-App.",
style: TextStyle(color: Colors.black)),
])),
const SizedBox(
height: 12.0,
),
Container(
child: downloading
? FittedBox(
child: Text(
'Downloading application $progressRating%',
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(fontWeight: FontWeight.bold),
),
)
: Container(),
),
SizedBox(
height: 60,
width: MediaQuery.of(context).size.width,
child: downloading
? Container()
: ElevatedButton.icon(
icon: const Icon(Icons.download),
style: mainBtnStyle(
primary, Colors.transparent, second),
onPressed: () async {
final progress = ProgressHUD.of(context);
progress?.showWithText(
'Please wait...',
);
setState(() {
downloading = true;
progressRating = '0';
});
await openFile();
},
label: const Text(
"Download Latest App Version.")),
),
],
),
),
const SizedBox(
height: 5,
),
Text("UPDATE REQUIRED!",
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.displaySmall!
.copyWith(color: primary, fontSize: 28)),
],
),
const SizedBox(
height: 10,
),
RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text: 'Your app version ',
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
children: <TextSpan>[
const TextSpan(
text: "mobileVersion",
style: TextStyle(
color: primary, fontWeight: FontWeight.bold)),
const TextSpan(
text: " did not match with the latest version ",
style: TextStyle(color: Colors.black)),
TextSpan(
text:
"widget.currentVersion.data.version".toString(),
style: const TextStyle(
color: primary, fontWeight: FontWeight.bold)),
const TextSpan(
text:
". Download the app latest version to procceed using the uniT-App.",
style: TextStyle(color: Colors.black)),
])),
const SizedBox(
height: 12.0,
),
SizedBox(
height: 60,
width: MediaQuery.of(context).size.width,
child: ElevatedButton.icon(
icon: const Icon(Icons.download),
style: mainBtnStyle(primary, Colors.transparent, second),
onPressed: () async {},
label: const Text("Download Latest App Version.")),
),
],
),
),
);
}
if (state is UserError) {
showAlert(context, () {
setState(() {
downloading = false;
});
});
}
return Container();
},
),
),
const Positioned(bottom: 0, child: WaveReverse(height: 80))
]),
);
}
Future<void> openFile() async {
try {
final filePath = await downloadFile();
await openAPK(filePath);
} catch (e) {
print(e.toString());
}
}
Future<void> openAPK(String path) async {
PermissionStatus result = await Permission.storage.request();
if (result.isGranted) {
await EasyAppInstaller.instance.installApk(path);
}
}
Future<String> downloadFile() async {
final progress = ProgressHUD.of(context);
final appStorage = await getApplicationDocumentsDirectory();
try {
String url = await getCPUArchitecture();
final response = await dio.download(url, '${appStorage.path}/uniT.apk',
deleteOnError: true,
options: Options(
receiveTimeout: 20000,
sendTimeout: 20000,
receiveDataWhenStatusError: true,
responseType: ResponseType.bytes,
followRedirects: false,
validateStatus: (status) {
return status! < 500;
}), onReceiveProgress: (recv, total) {
setState(() {
progressRating = ((recv / total) * 100).toStringAsFixed(0);
downloading = true;
});
if (progressRating == '100') {
final progress = ProgressHUD.of(context);
progress!.dismiss();
setState(() {
downloading = false;
isDownloaded = true;
});
}
});
} on TimeoutException catch (_) {
progress!.dismiss();
showAlert(context, () {
setState(() {
downloading = false;
});
});
throw TimeoutException(timeoutError);
} on SocketException catch (_) {
progress!.dismiss();
showAlert(context, () {
setState(() {
downloading = false;
});
});
throw const SocketException(timeoutError);
} on DioError catch (_) {
progress!.dismiss();
showAlert(context, () {
setState(() {
downloading = false;
});
});
throw TimeoutException(timeoutError);
} catch (_) {
progress!.dismiss();
showAlert(context, () {
setState(() {
downloading = false;
});
});
throw TimeoutException(timeoutError);
}
return '${appStorage.path}/uniT.apk';
}
}

View File

@ -0,0 +1,13 @@
import 'package:package_info_plus/package_info_plus.dart';
Future<String> getAppVersion() async{
String appVersion;
try{
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appVersion = packageInfo.version;
}catch(e){
throw(e.toString());
}
return appVersion;
}

View File

@ -4,17 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:flutter_progress_hud/flutter_progress_hud.dart';
import 'package:unit2/bloc/bloc/user_bloc.dart';
import 'package:unit2/model/login_data/user_info/user_data.dart';
import 'package:unit2/screens/unit2/login/qr_login.dart';
import 'package:unit2/screens/unit2/login/components/update_required.dart';
import 'package:unit2/utils/text_container.dart';
import 'package:unit2/widgets/error_state.dart';
import '../../../utils/scanner.dart';
import '../../../widgets/splash_screen.dart';
import '../../../widgets/wave.dart';
import '../../../utils/global.dart';
@ -53,10 +48,11 @@ class _UniT2LoginState extends State<UniT2Login> {
}, builder: (context, state) {
if (state is VersionLoaded) {
return Builder(builder: (context) {
return SizedBox(
if(state.versionInfo!.version == state.apkVersion){
return SizedBox(
child: SingleChildScrollView(
child: Stack(
alignment: Alignment.center,
alignment: Alignment.center,
children: [
Positioned(
bottom: 0,
@ -273,6 +269,10 @@ class _UniT2LoginState extends State<UniT2Login> {
),
),
);
}else{
return Update(apkVersion: state.apkVersion!,currenVersion: state.versionInfo!.version!,);
}
});
}
if (state is UserError) {

View File

@ -0,0 +1,44 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:unit2/model/login_data/version_info.dart';
import 'package:system_info2/system_info2.dart';
Future<String> getCPUArchitecture() async {
String downloadURL = "";
String cpuArchitecture = SysInfo.kernelArchitecture;
VersionInfo? apkVersion;
Dio dio = Dio();
const String apkUrl = 'https://unitylb1.agusandelnorte.gov.ph/unit2/api/sys/apk_version/latest/';
try {
Response response = await dio.get(apkUrl,
options: Options(
receiveTimeout: 20000,
receiveDataWhenStatusError: true,
responseType: ResponseType.json,
followRedirects: false,
validateStatus: (status) {
return status! < 500;
}));
if (response.statusCode == 200) {
apkVersion = VersionInfo.fromJson(response.data['data']);
if (cpuArchitecture.toUpperCase() == 'ARM' ||
cpuArchitecture.toUpperCase() == 'MIPS') {
downloadURL = apkVersion.armeabiv7aDownloadUrl!;
} else if (cpuArchitecture.toUpperCase() == 'I686' ||
cpuArchitecture.toUpperCase() == 'UNKNOWN' ||
cpuArchitecture.toUpperCase() == 'X86_64') {
downloadURL = apkVersion.x8664DownloadUrl!;
} else if (cpuArchitecture.toUpperCase() == 'AARCH64') {
downloadURL = apkVersion.arm64v8aDownloadUrl!;
}
}
} on TimeoutException catch (_) {
throw TimeoutException("Connection timeout");
} on SocketException catch (_) {
throw const SocketException("Connection timeout");
} on DioError catch (_) {
throw DioErrorType.connectTimeout;
}
return downloadURL;
}

View File

@ -19,7 +19,7 @@ class Request {
Map<String, String>? param}) async {
Response response;
try {
response = await get(Uri.http(host, path!, param), headers: headers)
response = await get(Uri.https(host, path!, param), headers: headers)
.timeout(Duration(seconds: requestTimeout));
} on TimeoutException catch (_) {
Fluttertoast.showToast(
@ -60,39 +60,39 @@ class Request {
Map? body,
Map<String, String>? param}) async {
Response response;
// try {
response = await post(Uri.http(host, path!, param), headers: headers,body: jsonEncode(body))
try {
response = await post(Uri.https(host, path!, param), headers: headers,body: jsonEncode(body))
.timeout(Duration(seconds: requestTimeout));
// } on TimeoutException catch (_) {
// Fluttertoast.showToast(
// msg: timeoutError,
// toastLength: Toast.LENGTH_LONG,
// gravity: ToastGravity.BOTTOM,
// backgroundColor: Colors.black,
// );
// throw (timeoutError);
// } on SocketException catch (_) {
// Fluttertoast.showToast(
// msg: timeoutError,
// toastLength: Toast.LENGTH_LONG,
// gravity: ToastGravity.BOTTOM,
// backgroundColor: Colors.black,
// );
// throw (timeoutError);
// } on FormatException catch (_) {
// throw const FormatException(formatError);
// } on HttpException catch (_) {
// throw const HttpException(httpError);
// } on Error catch (e) {
// debugPrint("post request error: $e");
// Fluttertoast.showToast(
// msg: onError,
// toastLength: Toast.LENGTH_LONG,
// gravity: ToastGravity.BOTTOM,
// backgroundColor: Colors.black,
// );
// throw (e.toString());
// }
} on TimeoutException catch (_) {
Fluttertoast.showToast(
msg: timeoutError,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.black,
);
throw (timeoutError);
} on SocketException catch (_) {
Fluttertoast.showToast(
msg: timeoutError,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.black,
);
throw (timeoutError);
} on FormatException catch (_) {
throw const FormatException(formatError);
} on HttpException catch (_) {
throw const HttpException(httpError);
} on Error catch (e) {
debugPrint("post request error: $e");
Fluttertoast.showToast(
msg: onError,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.black,
);
throw (e.toString());
}
return response;
}
}

View File

@ -3,8 +3,10 @@ class Url{
static Url get instance => _instance;
String host(){
return '192.168.10.219:3000';
// return '192.168.10.219:3000';
return 'agusandelnorte.gov.ph';
}
String authentication(){
return '/api/account/auth/login/';

View File

@ -5,11 +5,13 @@
import FlutterMacOS
import Foundation
import package_info_plus
import path_provider_macos
import shared_preferences_macos
import sqflite
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@ -120,6 +120,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0+1"
cool_alert:
dependency: "direct main"
description:
name: cool_alert
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
crypto:
dependency: transitive
description:
@ -148,6 +155,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.6"
easy_app_installer:
dependency: "direct main"
description:
name: easy_app_installer
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
equatable:
dependency: "direct main"
description:
@ -190,6 +211,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
flare_flutter:
dependency: transitive
description:
name: flare_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
flutter:
dependency: "direct main"
description: flutter
@ -242,6 +270,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
flutter_progress_hud:
dependency: "direct main"
description:
@ -371,6 +406,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
lottie:
dependency: transitive
description:
name: lottie
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.3"
matcher:
dependency: transitive
description:
@ -406,6 +448,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
path:
dependency: transitive
description:
@ -428,7 +484,7 @@ packages:
source: hosted
version: "1.0.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
@ -483,6 +539,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "10.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
version: "10.2.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.7"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.9.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
petitparser:
dependency: transitive
description:

View File

@ -60,6 +60,12 @@ dependencies:
system_info2: ^2.0.4
flutter_bloc: ^8.0.0
equatable: ^2.0.5
package_info_plus: ^3.0.2
easy_app_installer: ^1.0.0
path_provider: ^2.0.11
dio: ^4.0.6
cool_alert: ^1.1.0
permission_handler: ^10.2.0
dev_dependencies:
flutter_test:

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST