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')
|
||||
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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
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: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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user