Android 11 在应用之间共享文件
Android 11 sharing file between apps
Android 11 执行了一些存储规则,link 参考:Storage updates in Android 11
用例:
我有 2 个应用程序,应用程序 A 将文件 (.txt) 写入外部存储,应用程序 B 将从外部存储读取文件 而无需用户交互 .但是在 Android 11 上读取/写入时抛出异常,表示权限被拒绝。
所以我做了一些研究,发现只有 MediaStore API 和 Storage Access Framework 允许访问由创建的文件其他申请,link 参考:Data and file storage overview
但这两种方法都不适合我的用例:
- MediaStore API 只能访问媒体文件(图片、音频文件、视频)
- 存储访问框架需要用户交互
那么有没有其他方法可以访问外部存储上由 Android 11 上的不同应用程序创建的非媒体文件?
尽管我进行了所有研究,但我没有找到解决问题的方法。
感谢您的帮助。
更新
我尝试了 FileProvider 但是当我尝试启动时 activity,它总是显示错误
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.testapp, PID: 20141
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.example.app2.action.RECEIVE dat=content://com.example.testapp.fileprovider/myfiles/default_user.txt flg=0x1 }
at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2067)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1727)
at android.app.Activity.startActivityForResult(Activity.java:5320)
这就是我从 App 1
启动 App 2 activity 的方式
File filePath = new File(getFilesDir(), "files");
File newFile = new File(filePath, "default_user.txt");
Intent intent = new Intent();
intent.setAction("com.example.app2.action.RECEIVE");
intent.setData(FileProvider.getUriForFile(this, "com.example.testapp.fileprovider", newFile));
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
应用 1 清单
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestApp">
<activity android:name=".StorageActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity">
</activity>
<service
android:name=".service.TestService"
android:enabled="true"
android:exported="true" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.testapp.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
应用 2 清单
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestApp">
<activity android:name=".ReceiverActivity">
<intent-filter>
<action android:name="com.example.app2.action.RECEIVE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"
android:host="com.example.testapp" />
</intent-filter>
</activity>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
您应该使用启动意图直接启动 app2。
try {
File file = new File( .... );
Uri uri = FileProvider.getUriForFile(context, getPackageName() + ".fileprovider", file);
String apkPackage = "com.example.app2";
Intent intent = context.getPackageManager().getLaunchIntentForPackage(apkPackage);
if ( intent==null )
{
Toast.makeText(context, "Sorry, could not get launch intent for: " + apkPackage, Toast.LENGTH_LONG).show();
return;
}
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.startActivity(intent);
}
catch ( IllegalArgumentException e)
{
e.printStackTrace();
Toast.makeText(context, "IllegalArgumentException: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
catch ( Exception e)
{
e.printStackTrace();
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
}
您不需要 app2 清单中的 intent-filters。
接收方可以通过以下方式获取 uri:
Uri uri = getIntent().getData();
import android.content.res.AssetManager;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity {
PDFiumHelper mPdFiumHelper;
private void copyAssets() {
AssetManager assetManager = getAssets();
String[] files = null;
try {
files = assetManager.list("");
} catch (IOException e) {
Log.e("tag", "Failed to get asset file list.", e);
}
for (String filename : files) {
InputStream in = null;
OutputStream out = null;
try {
in = assetManager.open(filename);
String outDir = getFilesDir().getAbsolutePath();
File outFile = new File(outDir, filename);
out = new FileOutputStream(outFile);
copyFile(in, out);
in.close();
in = null;
out.flush();
out.close();
out = null;
} catch (IOException e) {
Log.e("tag", "Failed to copy asset file: " + filename, e);
}
}
}
private void copyFile(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
copyAssets();
mPdFiumHelper = new PDFiumHelper(this);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mPdFiumHelper.onKeyDown(keyCode,event);
}
}
帮手class
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.FileProvider;
import com.github.barteksc.pdfviewer.PDFView;
import com.github.barteksc.pdfviewer.listener.OnErrorListener;
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener;
import com.github.barteksc.pdfviewer.util.FitPolicy;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.karumi.dexter.MultiplePermissionsReport;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;
import java.io.File;
import java.util.Calendar;
import java.util.List;
public class PDFiumHelper implements MultiplePermissionsListener {
AppCompatActivity mAppCompatActivity;
public PDFiumHelper(AppCompatActivity mAppCompatActivity) {
this.mAppCompatActivity = mAppCompatActivity;
onCreate();
}
public PDFiumHelper() {
}
Button btnFile1, btnFile2;
long firstTime;
PDFView pdfView;
CommonUtility mCommonUtility;
boolean hasPermissions;
FloatingActionButton ibtn_share;
File localPDFFile;
String pdfTitle = "Mathematics Paper 2020";
String pdfExtraText = "this paper is made using the quantum paper.";
ConstraintLayout root;
String path1,path2;
protected void onCreate() {
path1 = mAppCompatActivity.getFilesDir() + "/SatsangDiksha.pdf";
path2 = mAppCompatActivity.getFilesDir() + "/sample.pdf";
btnFile1 = (Button) mAppCompatActivity.findViewById(R.id.btnFile1);
btnFile2 = (Button) mAppCompatActivity.findViewById(R.id.btnFile2);
root = mAppCompatActivity.findViewById(R.id.root);
pdfView = new PDFView(mAppCompatActivity, null);
pdfView.setLayoutParams(new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
root.addView(pdfView);
ibtn_share = new FloatingActionButton(mAppCompatActivity);
ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
int margin = convertDpToPixel(20);
params.setMargins(margin, margin, margin, margin);
//this will constrain FAB programmatically as like XML.
params.bottomToBottom = root.getId();
params.endToEnd = root.getId();
ibtn_share.setLayoutParams(params);
//change FAB icon here
ibtn_share.setImageResource(R.drawable.ic_share);
//change background color of FAB here
ibtn_share.setBackgroundTintList(ColorStateList.valueOf(mAppCompatActivity.getResources().getColor(R.color.purple_500)));
//FAB icon color can be change from here
ibtn_share.setColorFilter(Color.WHITE);
root.addView(ibtn_share);
btnFile1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadPdfFromFile(path1);
}
});
btnFile2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadPdfFromFile(path2);
}
});
mCommonUtility = new CommonUtility(mAppCompatActivity);
if (hasPermissions) {
} else {
mCommonUtility.askForPermissionBeforeStart(this);
}
ibtn_share.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (hasPermissions) {
shareFile();
} else {
mCommonUtility.askForPermissionBeforeStart(PDFiumHelper.this);
}
}
});
}
boolean flag = false;
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) {
if (flag) {
loadPdfFromFile(path1);
flag = false;
} else {
loadPdfFromFile(path2);
flag = true;
}
// Toast.makeText(MainActivity.this, "Left Arrow Pressed", Toast.LENGTH_SHORT).show();
return false;
}
return false;
}
public void shareFile() {
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
if (localPDFFile.exists()) {
intentShareFile.setType("application/pdf");
intentShareFile.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(mAppCompatActivity, BuildConfig.APPLICATION_ID + ".provider", localPDFFile));
intentShareFile.putExtra(Intent.EXTRA_SUBJECT,
pdfTitle);
intentShareFile.putExtra(Intent.EXTRA_TEXT, pdfExtraText);
mAppCompatActivity.startActivity(Intent.createChooser(intentShareFile, "Share File using"));
} else {
Toast.makeText(mAppCompatActivity, "File not loaded.", Toast.LENGTH_SHORT).show();
}
}
public long showCurrentTime() {
Calendar c = Calendar.getInstance();
int hours = c.get(Calendar.HOUR);
int minutes = c.get(Calendar.MINUTE);
int seconds = c.get(Calendar.SECOND);
int mseconds = c.get(Calendar.MILLISECOND);
// Log.e(TAG, "showCurrentTime: Current Time :- " + hours + ":" + minutes + ":" + seconds + ":" + mseconds);
return System.currentTimeMillis();
}
public void loadPdfFromFile(String path) {
firstTime = showCurrentTime();
try {
localPDFFile = new File(path);
pdfView.fromFile(localPDFFile)
.enableAntialiasing(true)
.enableDoubletap(true)
.onLoad(new OnLoadCompleteListener() {
@Override
public void loadComplete(int nbPages) {
double timee = firstTime - showCurrentTime();
double ms = Math.abs(timee) / 1000;
Log.e("===", "onDocumentLoaded: Time Taken :- " + ms + " Seconds");
Toast.makeText(mAppCompatActivity, ms + " Seconds", Toast.LENGTH_SHORT).show();
}
})
.onError(new OnErrorListener() {
@Override
public void onError(Throwable t) {
t.printStackTrace();
Toast.makeText(mAppCompatActivity, "Load some file.", Toast.LENGTH_SHORT).show();
}
})
.scrollHandle(new DefaultScrollHandle(mAppCompatActivity))
.pageFitPolicy(FitPolicy.WIDTH)
.autoSpacing(false)
.load();
} catch (Throwable th) {
// th.printStackTrace();
}
}
@Override
public void onPermissionsChecked(MultiplePermissionsReport multiplePermissionsReport) {
// check if all permissions are granted
if (multiplePermissionsReport.areAllPermissionsGranted()) {
// do work
hasPermissions = true;
if (hasPermissions) {
}
}
if (multiplePermissionsReport.isAnyPermissionPermanentlyDenied()) {
// permission is denied permenantly, navigate user to app settings
mCommonUtility.showSettingsDialog();
}
}
@Override
public void onPermissionRationaleShouldBeShown(List<PermissionRequest> list, PermissionToken permissionToken) {
mCommonUtility.showPermissionRationale(permissionToken);
}
public int convertDpToPixel(int dp) {
return dp * (mAppCompatActivity.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
public int convertPixelsToDp(int px) {
return px / (mAppCompatActivity.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
}
提供程序路径文件。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
</paths>
Android 清单文件。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.qp.pdfiumproject">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PdfiumProject">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>
使用Android11,每个应用程序只能在自己的私有目录中读写文件。这适用于 Java 文件 class 和本机代码。这些是我目前知道的解决此限制的方法:
- 您可以将 SAF 用于所有文件访问。在某些情况下这是不可能的,例如使用 SQLite 时。 SQLite 只接受数据类型 File,不接受 DocumentFile.
- 您可以使用 SAF 将您需要的所有文件复制到您的私人目录,然后使用 File 或本机代码访问它们。如果您想更改它们,您必须随后使用 SAF 将它们复制回它们的原始位置。
- 您可以请求 MANAGE_EXTERNAL_STORAGE 许可(为什么是“外部”?!?)。在这种情况下,您无法在 Play 商店中发布您的应用程序。
- 您可以使用目标 SDK Android 10. 在这种情况下,您无法在 Play 商店中发布您的应用程序。
- 您可以让 Android 相信您的文件(可能是“bla.txt”和“labre.db”)是媒体文件。将它们重命名为“bla.txt.m4a”和“labre.db.jpg”。作为副作用,您的媒体数据库将被不需要的条目污染,即不良音频和不良图片,它们可能会无意中出现在图片或音乐专辑中。
在我看来 Google 可以找到更好的解决方案,不会给开发人员带来太多工作。顺便说一句:SAF 太慢了,即使只是传输原始数据。
Android 11 执行了一些存储规则,link 参考:Storage updates in Android 11
用例: 我有 2 个应用程序,应用程序 A 将文件 (.txt) 写入外部存储,应用程序 B 将从外部存储读取文件 而无需用户交互 .但是在 Android 11 上读取/写入时抛出异常,表示权限被拒绝。
所以我做了一些研究,发现只有 MediaStore API 和 Storage Access Framework 允许访问由创建的文件其他申请,link 参考:Data and file storage overview
但这两种方法都不适合我的用例:
- MediaStore API 只能访问媒体文件(图片、音频文件、视频)
- 存储访问框架需要用户交互
那么有没有其他方法可以访问外部存储上由 Android 11 上的不同应用程序创建的非媒体文件?
尽管我进行了所有研究,但我没有找到解决问题的方法。
感谢您的帮助。
更新
我尝试了 FileProvider 但是当我尝试启动时 activity,它总是显示错误
E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.testapp, PID: 20141 android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.example.app2.action.RECEIVE dat=content://com.example.testapp.fileprovider/myfiles/default_user.txt flg=0x1 } at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2067) at android.app.Instrumentation.execStartActivity(Instrumentation.java:1727) at android.app.Activity.startActivityForResult(Activity.java:5320)
这就是我从 App 1
启动 App 2 activity 的方式File filePath = new File(getFilesDir(), "files");
File newFile = new File(filePath, "default_user.txt");
Intent intent = new Intent();
intent.setAction("com.example.app2.action.RECEIVE");
intent.setData(FileProvider.getUriForFile(this, "com.example.testapp.fileprovider", newFile));
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
应用 1 清单
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestApp">
<activity android:name=".StorageActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity">
</activity>
<service
android:name=".service.TestService"
android:enabled="true"
android:exported="true" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.testapp.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
应用 2 清单
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestApp">
<activity android:name=".ReceiverActivity">
<intent-filter>
<action android:name="com.example.app2.action.RECEIVE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"
android:host="com.example.testapp" />
</intent-filter>
</activity>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
您应该使用启动意图直接启动 app2。
try {
File file = new File( .... );
Uri uri = FileProvider.getUriForFile(context, getPackageName() + ".fileprovider", file);
String apkPackage = "com.example.app2";
Intent intent = context.getPackageManager().getLaunchIntentForPackage(apkPackage);
if ( intent==null )
{
Toast.makeText(context, "Sorry, could not get launch intent for: " + apkPackage, Toast.LENGTH_LONG).show();
return;
}
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.startActivity(intent);
}
catch ( IllegalArgumentException e)
{
e.printStackTrace();
Toast.makeText(context, "IllegalArgumentException: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
catch ( Exception e)
{
e.printStackTrace();
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
}
您不需要 app2 清单中的 intent-filters。
接收方可以通过以下方式获取 uri:
Uri uri = getIntent().getData();
import android.content.res.AssetManager;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity {
PDFiumHelper mPdFiumHelper;
private void copyAssets() {
AssetManager assetManager = getAssets();
String[] files = null;
try {
files = assetManager.list("");
} catch (IOException e) {
Log.e("tag", "Failed to get asset file list.", e);
}
for (String filename : files) {
InputStream in = null;
OutputStream out = null;
try {
in = assetManager.open(filename);
String outDir = getFilesDir().getAbsolutePath();
File outFile = new File(outDir, filename);
out = new FileOutputStream(outFile);
copyFile(in, out);
in.close();
in = null;
out.flush();
out.close();
out = null;
} catch (IOException e) {
Log.e("tag", "Failed to copy asset file: " + filename, e);
}
}
}
private void copyFile(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
copyAssets();
mPdFiumHelper = new PDFiumHelper(this);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mPdFiumHelper.onKeyDown(keyCode,event);
}
}
帮手class
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.FileProvider;
import com.github.barteksc.pdfviewer.PDFView;
import com.github.barteksc.pdfviewer.listener.OnErrorListener;
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener;
import com.github.barteksc.pdfviewer.util.FitPolicy;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.karumi.dexter.MultiplePermissionsReport;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;
import java.io.File;
import java.util.Calendar;
import java.util.List;
public class PDFiumHelper implements MultiplePermissionsListener {
AppCompatActivity mAppCompatActivity;
public PDFiumHelper(AppCompatActivity mAppCompatActivity) {
this.mAppCompatActivity = mAppCompatActivity;
onCreate();
}
public PDFiumHelper() {
}
Button btnFile1, btnFile2;
long firstTime;
PDFView pdfView;
CommonUtility mCommonUtility;
boolean hasPermissions;
FloatingActionButton ibtn_share;
File localPDFFile;
String pdfTitle = "Mathematics Paper 2020";
String pdfExtraText = "this paper is made using the quantum paper.";
ConstraintLayout root;
String path1,path2;
protected void onCreate() {
path1 = mAppCompatActivity.getFilesDir() + "/SatsangDiksha.pdf";
path2 = mAppCompatActivity.getFilesDir() + "/sample.pdf";
btnFile1 = (Button) mAppCompatActivity.findViewById(R.id.btnFile1);
btnFile2 = (Button) mAppCompatActivity.findViewById(R.id.btnFile2);
root = mAppCompatActivity.findViewById(R.id.root);
pdfView = new PDFView(mAppCompatActivity, null);
pdfView.setLayoutParams(new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
root.addView(pdfView);
ibtn_share = new FloatingActionButton(mAppCompatActivity);
ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
int margin = convertDpToPixel(20);
params.setMargins(margin, margin, margin, margin);
//this will constrain FAB programmatically as like XML.
params.bottomToBottom = root.getId();
params.endToEnd = root.getId();
ibtn_share.setLayoutParams(params);
//change FAB icon here
ibtn_share.setImageResource(R.drawable.ic_share);
//change background color of FAB here
ibtn_share.setBackgroundTintList(ColorStateList.valueOf(mAppCompatActivity.getResources().getColor(R.color.purple_500)));
//FAB icon color can be change from here
ibtn_share.setColorFilter(Color.WHITE);
root.addView(ibtn_share);
btnFile1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadPdfFromFile(path1);
}
});
btnFile2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadPdfFromFile(path2);
}
});
mCommonUtility = new CommonUtility(mAppCompatActivity);
if (hasPermissions) {
} else {
mCommonUtility.askForPermissionBeforeStart(this);
}
ibtn_share.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (hasPermissions) {
shareFile();
} else {
mCommonUtility.askForPermissionBeforeStart(PDFiumHelper.this);
}
}
});
}
boolean flag = false;
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) {
if (flag) {
loadPdfFromFile(path1);
flag = false;
} else {
loadPdfFromFile(path2);
flag = true;
}
// Toast.makeText(MainActivity.this, "Left Arrow Pressed", Toast.LENGTH_SHORT).show();
return false;
}
return false;
}
public void shareFile() {
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
if (localPDFFile.exists()) {
intentShareFile.setType("application/pdf");
intentShareFile.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(mAppCompatActivity, BuildConfig.APPLICATION_ID + ".provider", localPDFFile));
intentShareFile.putExtra(Intent.EXTRA_SUBJECT,
pdfTitle);
intentShareFile.putExtra(Intent.EXTRA_TEXT, pdfExtraText);
mAppCompatActivity.startActivity(Intent.createChooser(intentShareFile, "Share File using"));
} else {
Toast.makeText(mAppCompatActivity, "File not loaded.", Toast.LENGTH_SHORT).show();
}
}
public long showCurrentTime() {
Calendar c = Calendar.getInstance();
int hours = c.get(Calendar.HOUR);
int minutes = c.get(Calendar.MINUTE);
int seconds = c.get(Calendar.SECOND);
int mseconds = c.get(Calendar.MILLISECOND);
// Log.e(TAG, "showCurrentTime: Current Time :- " + hours + ":" + minutes + ":" + seconds + ":" + mseconds);
return System.currentTimeMillis();
}
public void loadPdfFromFile(String path) {
firstTime = showCurrentTime();
try {
localPDFFile = new File(path);
pdfView.fromFile(localPDFFile)
.enableAntialiasing(true)
.enableDoubletap(true)
.onLoad(new OnLoadCompleteListener() {
@Override
public void loadComplete(int nbPages) {
double timee = firstTime - showCurrentTime();
double ms = Math.abs(timee) / 1000;
Log.e("===", "onDocumentLoaded: Time Taken :- " + ms + " Seconds");
Toast.makeText(mAppCompatActivity, ms + " Seconds", Toast.LENGTH_SHORT).show();
}
})
.onError(new OnErrorListener() {
@Override
public void onError(Throwable t) {
t.printStackTrace();
Toast.makeText(mAppCompatActivity, "Load some file.", Toast.LENGTH_SHORT).show();
}
})
.scrollHandle(new DefaultScrollHandle(mAppCompatActivity))
.pageFitPolicy(FitPolicy.WIDTH)
.autoSpacing(false)
.load();
} catch (Throwable th) {
// th.printStackTrace();
}
}
@Override
public void onPermissionsChecked(MultiplePermissionsReport multiplePermissionsReport) {
// check if all permissions are granted
if (multiplePermissionsReport.areAllPermissionsGranted()) {
// do work
hasPermissions = true;
if (hasPermissions) {
}
}
if (multiplePermissionsReport.isAnyPermissionPermanentlyDenied()) {
// permission is denied permenantly, navigate user to app settings
mCommonUtility.showSettingsDialog();
}
}
@Override
public void onPermissionRationaleShouldBeShown(List<PermissionRequest> list, PermissionToken permissionToken) {
mCommonUtility.showPermissionRationale(permissionToken);
}
public int convertDpToPixel(int dp) {
return dp * (mAppCompatActivity.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
public int convertPixelsToDp(int px) {
return px / (mAppCompatActivity.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
}
提供程序路径文件。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
</paths>
Android 清单文件。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.qp.pdfiumproject">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PdfiumProject">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>
使用Android11,每个应用程序只能在自己的私有目录中读写文件。这适用于 Java 文件 class 和本机代码。这些是我目前知道的解决此限制的方法:
- 您可以将 SAF 用于所有文件访问。在某些情况下这是不可能的,例如使用 SQLite 时。 SQLite 只接受数据类型 File,不接受 DocumentFile.
- 您可以使用 SAF 将您需要的所有文件复制到您的私人目录,然后使用 File 或本机代码访问它们。如果您想更改它们,您必须随后使用 SAF 将它们复制回它们的原始位置。
- 您可以请求 MANAGE_EXTERNAL_STORAGE 许可(为什么是“外部”?!?)。在这种情况下,您无法在 Play 商店中发布您的应用程序。
- 您可以使用目标 SDK Android 10. 在这种情况下,您无法在 Play 商店中发布您的应用程序。
- 您可以让 Android 相信您的文件(可能是“bla.txt”和“labre.db”)是媒体文件。将它们重命名为“bla.txt.m4a”和“labre.db.jpg”。作为副作用,您的媒体数据库将被不需要的条目污染,即不良音频和不良图片,它们可能会无意中出现在图片或音乐专辑中。
在我看来 Google 可以找到更好的解决方案,不会给开发人员带来太多工作。顺便说一句:SAF 太慢了,即使只是传输原始数据。