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')
if (flutterVersionCode == null) {
flutterVersionCode = '65'
flutterVersionCode = '67'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '2.0.27'
flutterVersionName = '2.0.28'
}
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">
<uses-permission android:name="android.permission.INTERNET"/>
<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.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.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<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
android:usesCleartextTraffic="true"
android:label="Nhance Benefits"
@ -52,5 +61,9 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:mimeType="application/pdf"/>
</intent>
</queries>
</manifest>

View File

@ -1,32 +1,103 @@
package nh.ind.nhance.benefits
import android.media.MediaScannerConnection
import io.flutter.embedding.engine.FlutterEngine
import android.content.ContentValues
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.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.IOException
class MainActivity: FlutterFragmentActivity() {
private val mediaScanChannel = "nhance/media_scan"
class MainActivity : FlutterFragmentActivity() {
private val downloadsChannel = "nhance/downloads"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, mediaScanChannel)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, downloadsChannel)
.setMethodCallHandler { call, result ->
if (call.method == "scanFile") {
val path = call.argument<String>("path")
if (path.isNullOrBlank()) {
result.error("invalid_path", "Path is required", null)
return@setMethodCallHandler
when (call.method) {
"getSdkInt" -> result.success(Build.VERSION.SDK_INT)
"saveToDownloads" -> {
val fileName = call.argument<String>("fileName")
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(
applicationContext,
arrayOf(path),
null
) { _, _ -> }
result.success(true)
} else {
result.notImplemented()
"openFile" -> {
val path = call.argument<String>("path")
if (path.isNullOrBlank()) {
result.error("invalid_path", "Path is required", null)
return@setMethodCallHandler
}
try {
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
/// 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;

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:permission_handler/permission_handler.dart';
import 'android_download_helper.dart';
class AppPermissionService {
AppPermissionService._();
@ -10,7 +12,10 @@ class AppPermissionService {
if (kIsWeb || !Platform.isAndroid) return;
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_local_notifications/flutter_local_notifications.dart';
import 'package:open_filex/open_filex.dart';
import 'android_download_helper.dart';
class EcardDownloadNotificationService {
EcardDownloadNotificationService._();
@ -25,6 +29,10 @@ class EcardDownloadNotificationService {
onDidReceiveNotificationResponse: (NotificationResponse response) async {
final filePath = response.payload;
if (filePath == null || filePath.isEmpty) return;
if (Platform.isAndroid) {
await openAndroidDownloadedFile(filePath);
return;
}
await OpenFilex.open(filePath);
},
);

View File

@ -1,29 +1,26 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'android_download_helper.dart';
import 'download_result.dart';
final Dio _dio = Dio();
const MethodChannel _mediaScanChannel = MethodChannel('nhance/media_scan');
Future<bool> ensureStorageAccessImpl() async {
if (Platform.isAndroid) {
// Request notification permission (best-effort for Android 13+).
await Permission.notification.request();
// Prefer broad access for writing to public Download directory.
if (await Permission.manageExternalStorage.isGranted) return true;
final manage = await Permission.manageExternalStorage.request();
if (manage.isGranted) return true;
// Fallback for older devices.
final storage = await Permission.storage.request();
return storage.isGranted;
final sdkInt = await androidSdkInt();
if (sdkInt != null && sdkInt < 29) {
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 filePath = '${downloadDir.path}/$safeFileName';
final file = File(filePath);
if (await file.exists()) {
await file.delete();
final tempDir = await getTemporaryDirectory();
final tempPath = '${tempDir.path}/$safeFileName';
final tempFile = File(tempPath);
if (await tempFile.exists()) {
await tempFile.delete();
}
await _dio.download(
url,
filePath,
tempPath,
options: Options(
headers: headers,
responseType: ResponseType.bytes,
@ -71,18 +64,23 @@ Future<DownloadResult> downloadEcardImpl({
deleteOnError: true,
);
if (!await file.exists()) {
if (!await tempFile.exists()) {
return DownloadResult.failure('Download failed');
}
final length = await file.length();
if (length == 0) {
await file.delete();
final bytes = await tempFile.readAsBytes();
if (bytes.isEmpty) {
await tempFile.delete();
return DownloadResult.failure('Download failed');
}
await _scanFileForAndroidMediaIndex(filePath);
final savedPath = await savePdfToAndroidDownloads(
fileName: safeFileName,
bytes: bytes,
);
await tempFile.delete();
return DownloadResult.success(
savedPath: filePath,
savedPath: savedPath,
message: 'File saved under Download/$safeFileName',
);
} on FileSystemException {
@ -93,6 +91,8 @@ Future<DownloadResult> downloadEcardImpl({
? 'Download failed (${e.response!.statusCode})'
: 'Download failed';
return DownloadResult.failure(message);
} on PlatformException catch (e) {
return DownloadResult.failure(e.message ?? 'Download failed');
} catch (_) {
return DownloadResult.failure('Download failed');
}
@ -153,23 +153,3 @@ String _sanitizeFileName(String fileName) {
if (replaced.isEmpty) return 'ecard.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
# of the product and file versions while build-number is used as the build suffix.
#version: 1.2.41+98
version: 1.0.38+44
#version: 2.0.27+65
version: 1.0.39+45
#version: 2.0.28+67
environment:
sdk: '>=3.3.3 <4.0.0'