premission issue fix

This commit is contained in:
Surendiran 2026-06-11 12:20:28 +05:30
parent ab44ad5815
commit 74039d751c
9 changed files with 188 additions and 75 deletions

View File

@ -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()

View File

@ -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>

View File

@ -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)
}
} }

View File

@ -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;

View 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});
}

View File

@ -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();
}
} }
} }

View File

@ -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);
}, },
); );

View File

@ -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.
}
}

View File

@ -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'