premission issue fix
This commit is contained in:
parent
ab44ad5815
commit
74039d751c
@ -19,12 +19,12 @@ if (project.hasProperty('google-services.json')) {
|
|||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '65'
|
flutterVersionCode = '67'
|
||||||
}
|
}
|
||||||
|
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
if (flutterVersionName == null) {
|
if (flutterVersionName == null) {
|
||||||
flutterVersionName = '2.0.27'
|
flutterVersionName = '2.0.28'
|
||||||
}
|
}
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
def keystoreProperties = new Properties()
|
||||||
|
|||||||
@ -1,14 +1,23 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="nh.ind.nhance.benefits">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="nh.ind.nhance.benefits">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||||
|
|
||||||
|
<!-- Strip media/storage permissions merged from dependencies (e.g. open_filex). -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" tools:node="remove" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" tools:node="remove" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:label="Nhance Benefits"
|
android:label="Nhance Benefits"
|
||||||
@ -52,5 +61,9 @@
|
|||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<data android:mimeType="application/pdf"/>
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -1,32 +1,103 @@
|
|||||||
package nh.ind.nhance.benefits
|
package nh.ind.nhance.benefits
|
||||||
|
|
||||||
import android.media.MediaScannerConnection
|
import android.content.ContentValues
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class MainActivity: FlutterFragmentActivity() {
|
class MainActivity : FlutterFragmentActivity() {
|
||||||
private val mediaScanChannel = "nhance/media_scan"
|
private val downloadsChannel = "nhance/downloads"
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, mediaScanChannel)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, downloadsChannel)
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result ->
|
||||||
if (call.method == "scanFile") {
|
when (call.method) {
|
||||||
val path = call.argument<String>("path")
|
"getSdkInt" -> result.success(Build.VERSION.SDK_INT)
|
||||||
if (path.isNullOrBlank()) {
|
"saveToDownloads" -> {
|
||||||
result.error("invalid_path", "Path is required", null)
|
val fileName = call.argument<String>("fileName")
|
||||||
return@setMethodCallHandler
|
val bytes = call.argument<ByteArray>("bytes")
|
||||||
|
if (fileName.isNullOrBlank() || bytes == null) {
|
||||||
|
result.error("invalid_args", "fileName and bytes are required", null)
|
||||||
|
return@setMethodCallHandler
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
result.success(saveToDownloads(fileName, bytes))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.error("save_failed", e.message, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MediaScannerConnection.scanFile(
|
"openFile" -> {
|
||||||
applicationContext,
|
val path = call.argument<String>("path")
|
||||||
arrayOf(path),
|
if (path.isNullOrBlank()) {
|
||||||
null
|
result.error("invalid_path", "Path is required", null)
|
||||||
) { _, _ -> }
|
return@setMethodCallHandler
|
||||||
result.success(true)
|
}
|
||||||
} else {
|
try {
|
||||||
result.notImplemented()
|
openFile(path)
|
||||||
|
result.success(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.error("open_failed", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveToDownloads(fileName: String, bytes: ByteArray): String {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val resolver = applicationContext.contentResolver
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||||
|
}
|
||||||
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
?: throw IOException("Unable to create download entry")
|
||||||
|
resolver.openOutputStream(uri)?.use { it.write(bytes) }
|
||||||
|
?: throw IOException("Unable to write download")
|
||||||
|
contentValues.clear()
|
||||||
|
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||||
|
resolver.update(uri, contentValues, null, null)
|
||||||
|
return uri.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs()
|
||||||
|
}
|
||||||
|
val file = File(dir, fileName)
|
||||||
|
file.writeBytes(bytes)
|
||||||
|
return file.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openFile(path: String) {
|
||||||
|
val uri: Uri = if (path.startsWith("content://")) {
|
||||||
|
Uri.parse(path)
|
||||||
|
} else {
|
||||||
|
val file = File(path)
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
this,
|
||||||
|
"${applicationContext.packageName}.fileProvider.com.crazecoder.openfile",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "application/pdf")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ class Environment {
|
|||||||
static Flavor flavor = Flavor.dev; // overwritten by each main_*.dart
|
static Flavor flavor = Flavor.dev; // overwritten by each main_*.dart
|
||||||
|
|
||||||
/// Chatbot FAB + window. Set to `true` when ready to enable.
|
/// Chatbot FAB + window. Set to `true` when ready to enable.
|
||||||
static const bool chatbotEnabled = false;
|
static const bool chatbotEnabled = true;
|
||||||
|
|
||||||
static bool get isProd => flavor == Flavor.prod || flavor == Flavor.prod1;
|
static bool get isProd => flavor == Flavor.prod || flavor == Flavor.prod1;
|
||||||
|
|
||||||
|
|||||||
36
lib/pages/helpers/android_download_helper.dart
Normal file
36
lib/pages/helpers/android_download_helper.dart
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
const MethodChannel _downloadsChannel = MethodChannel('nhance/downloads');
|
||||||
|
|
||||||
|
Future<int?> androidSdkInt() async {
|
||||||
|
if (!Platform.isAndroid) return null;
|
||||||
|
try {
|
||||||
|
final sdkInt = await _downloadsChannel.invokeMethod<int>('getSdkInt');
|
||||||
|
return sdkInt;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> savePdfToAndroidDownloads({
|
||||||
|
required String fileName,
|
||||||
|
required List<int> bytes,
|
||||||
|
}) async {
|
||||||
|
final savedPath = await _downloadsChannel.invokeMethod<String>(
|
||||||
|
'saveToDownloads',
|
||||||
|
{
|
||||||
|
'fileName': fileName,
|
||||||
|
'bytes': bytes,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (savedPath == null || savedPath.isEmpty) {
|
||||||
|
throw const FileSystemException('Unable to save file to Downloads');
|
||||||
|
}
|
||||||
|
return savedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openAndroidDownloadedFile(String path) async {
|
||||||
|
await _downloadsChannel.invokeMethod<void>('openFile', {'path': path});
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ import 'dart:io';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
import 'android_download_helper.dart';
|
||||||
|
|
||||||
class AppPermissionService {
|
class AppPermissionService {
|
||||||
AppPermissionService._();
|
AppPermissionService._();
|
||||||
|
|
||||||
@ -10,7 +12,10 @@ class AppPermissionService {
|
|||||||
if (kIsWeb || !Platform.isAndroid) return;
|
if (kIsWeb || !Platform.isAndroid) return;
|
||||||
|
|
||||||
await Permission.notification.request();
|
await Permission.notification.request();
|
||||||
// Keep app-open flow lightweight: no redirection to Android settings page.
|
|
||||||
await Permission.storage.request();
|
final sdkInt = await androidSdkInt();
|
||||||
|
if (sdkInt != null && sdkInt < 29) {
|
||||||
|
await Permission.storage.request();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:open_filex/open_filex.dart';
|
import 'package:open_filex/open_filex.dart';
|
||||||
|
|
||||||
|
import 'android_download_helper.dart';
|
||||||
|
|
||||||
class EcardDownloadNotificationService {
|
class EcardDownloadNotificationService {
|
||||||
EcardDownloadNotificationService._();
|
EcardDownloadNotificationService._();
|
||||||
|
|
||||||
@ -25,6 +29,10 @@ class EcardDownloadNotificationService {
|
|||||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {
|
onDidReceiveNotificationResponse: (NotificationResponse response) async {
|
||||||
final filePath = response.payload;
|
final filePath = response.payload;
|
||||||
if (filePath == null || filePath.isEmpty) return;
|
if (filePath == null || filePath.isEmpty) return;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await openAndroidDownloadedFile(filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await OpenFilex.open(filePath);
|
await OpenFilex.open(filePath);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,29 +1,26 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
import 'android_download_helper.dart';
|
||||||
import 'download_result.dart';
|
import 'download_result.dart';
|
||||||
|
|
||||||
final Dio _dio = Dio();
|
final Dio _dio = Dio();
|
||||||
const MethodChannel _mediaScanChannel = MethodChannel('nhance/media_scan');
|
|
||||||
|
|
||||||
Future<bool> ensureStorageAccessImpl() async {
|
Future<bool> ensureStorageAccessImpl() async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
// Request notification permission (best-effort for Android 13+).
|
|
||||||
await Permission.notification.request();
|
await Permission.notification.request();
|
||||||
|
|
||||||
// Prefer broad access for writing to public Download directory.
|
final sdkInt = await androidSdkInt();
|
||||||
if (await Permission.manageExternalStorage.isGranted) return true;
|
if (sdkInt != null && sdkInt < 29) {
|
||||||
final manage = await Permission.manageExternalStorage.request();
|
final storage = await Permission.storage.request();
|
||||||
if (manage.isGranted) return true;
|
return storage.isGranted;
|
||||||
|
}
|
||||||
// Fallback for older devices.
|
return true;
|
||||||
final storage = await Permission.storage.request();
|
|
||||||
return storage.isGranted;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -49,21 +46,17 @@ Future<DownloadResult> downloadEcardImpl({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Directory downloadDir = await _resolveDownloadDirectory();
|
|
||||||
if (!await downloadDir.exists()) {
|
|
||||||
await downloadDir.create(recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
final safeFileName = _sanitizeFileName(fileName);
|
final safeFileName = _sanitizeFileName(fileName);
|
||||||
final filePath = '${downloadDir.path}/$safeFileName';
|
final tempDir = await getTemporaryDirectory();
|
||||||
final file = File(filePath);
|
final tempPath = '${tempDir.path}/$safeFileName';
|
||||||
if (await file.exists()) {
|
final tempFile = File(tempPath);
|
||||||
await file.delete();
|
if (await tempFile.exists()) {
|
||||||
|
await tempFile.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _dio.download(
|
await _dio.download(
|
||||||
url,
|
url,
|
||||||
filePath,
|
tempPath,
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: headers,
|
headers: headers,
|
||||||
responseType: ResponseType.bytes,
|
responseType: ResponseType.bytes,
|
||||||
@ -71,18 +64,23 @@ Future<DownloadResult> downloadEcardImpl({
|
|||||||
deleteOnError: true,
|
deleteOnError: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!await file.exists()) {
|
if (!await tempFile.exists()) {
|
||||||
return DownloadResult.failure('Download failed');
|
return DownloadResult.failure('Download failed');
|
||||||
}
|
}
|
||||||
final length = await file.length();
|
final bytes = await tempFile.readAsBytes();
|
||||||
if (length == 0) {
|
if (bytes.isEmpty) {
|
||||||
await file.delete();
|
await tempFile.delete();
|
||||||
return DownloadResult.failure('Download failed');
|
return DownloadResult.failure('Download failed');
|
||||||
}
|
}
|
||||||
await _scanFileForAndroidMediaIndex(filePath);
|
|
||||||
|
final savedPath = await savePdfToAndroidDownloads(
|
||||||
|
fileName: safeFileName,
|
||||||
|
bytes: bytes,
|
||||||
|
);
|
||||||
|
await tempFile.delete();
|
||||||
|
|
||||||
return DownloadResult.success(
|
return DownloadResult.success(
|
||||||
savedPath: filePath,
|
savedPath: savedPath,
|
||||||
message: 'File saved under Download/$safeFileName',
|
message: 'File saved under Download/$safeFileName',
|
||||||
);
|
);
|
||||||
} on FileSystemException {
|
} on FileSystemException {
|
||||||
@ -93,6 +91,8 @@ Future<DownloadResult> downloadEcardImpl({
|
|||||||
? 'Download failed (${e.response!.statusCode})'
|
? 'Download failed (${e.response!.statusCode})'
|
||||||
: 'Download failed';
|
: 'Download failed';
|
||||||
return DownloadResult.failure(message);
|
return DownloadResult.failure(message);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
return DownloadResult.failure(e.message ?? 'Download failed');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return DownloadResult.failure('Download failed');
|
return DownloadResult.failure('Download failed');
|
||||||
}
|
}
|
||||||
@ -153,23 +153,3 @@ String _sanitizeFileName(String fileName) {
|
|||||||
if (replaced.isEmpty) return 'ecard.pdf';
|
if (replaced.isEmpty) return 'ecard.pdf';
|
||||||
return replaced.toLowerCase().endsWith('.pdf') ? replaced : '$replaced.pdf';
|
return replaced.toLowerCase().endsWith('.pdf') ? replaced : '$replaced.pdf';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> _resolveDownloadDirectory() async {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
final dir = Directory('/storage/emulated/0/Download');
|
|
||||||
if (!await dir.exists()) {
|
|
||||||
await dir.create(recursive: true);
|
|
||||||
}
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
return getApplicationDocumentsDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _scanFileForAndroidMediaIndex(String filePath) async {
|
|
||||||
if (!Platform.isAndroid) return;
|
|
||||||
try {
|
|
||||||
await _mediaScanChannel.invokeMethod<void>('scanFile', {'path': filePath});
|
|
||||||
} catch (_) {
|
|
||||||
// Non-fatal: file is already saved even if scan fails.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -17,8 +17,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
#version: 1.2.41+98
|
#version: 1.2.41+98
|
||||||
version: 1.0.38+44
|
version: 1.0.39+45
|
||||||
#version: 2.0.27+65
|
#version: 2.0.28+67
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.3 <4.0.0'
|
sdk: '>=3.3.3 <4.0.0'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user