app经常需要给其他的app传送文件,比如QQ里面我们可能需要将图库里面的图片返回,比如图片浏览器想把图片文件传到图片编辑器中,或者文件管理器想让用户在external storage中复制粘贴文件。
为了将文件安全地从我们的应用程序共享给其它应用程序,唯一一种安全的方法就是将文件的URI传输给目标应用并授予该URI临时权限. 因为这权限是对于接收URI的目标应用有效,并且是临时的,会自动失效,所以这种方式是安全的(Android可以用FileProvider中的getUriForFile()来获取文件的URI)
为了传输文件,我们需要建立两个应用,一个应用来请求数据 启动另外一个应用,被启动的应用会共享它能共享 的文件,然后我们选择一个文件,确定后返回到原始应用,这个就会得到一个文件。
下面看一个具体的实现:
用来请求数据的应用我们称之为客户端(Client),另外一个用于分享数据的应用我们称之为服务端(Server)。
要实现的效果是:客户端请求数据,打开服务端,点击服务端中的图片返回到客户端。
为了安全的提供一个文件让其他app访问,你需要将文件以URI的形式来提供出来.Android中有个类可以帮助我们,就是FileProvider,它能够基于xml中相应的配置生成相应的文件的URI。
step 1: 设置FileProvider
为了给应用程序定义一个FileProvider,需要在Manifest清单文件中定义一个entry,该entry指明了需要使用的创建Content URI的Authority。此外,还需要一个XML文件的文件名,该XML文件指定了我们的应用可以共享的目录路径。
下例展示了如何在清单文件中添加标签,来指定FileProvider类,Authority及XML文件名:
服务端(Service)
"http://schemas.android.com/apk/res/android"
package="com.example.service">
...>
"android.support.v4.content.FileProvider"
android:authorities="com.example.service.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
"android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
...
manifest中定义的时候有两点需要注意 :
android:authorities 字段指定了希望使用的Authority,该Authority针对于FileProvider所生成的content URI。本例中我的应用的包名为com.example.service
对于自己的应用,要在我们的应用程序包名(android:package的值)之后继续追加“fileprovider”来指定Authority。
下的
指向了一个XML文件,该文件指定了我们希望共享的目录路径。“android:resource”属性字段是这个文件的路径和名字(无“.xml”后缀)
step 2: 设置合适的路径
一旦在Manifest清单文件中为自己的应用添加了FileProvider,就需要指定我们希望共享文件的目录路径。为指定该路径,首先要在“res/xml/”下创建文件“filepaths.xml”。在这个文件中,为每一个想要共享目录添加一个XML标签。下面的是一个“res/xml/filepaths.xml”的内容样例。
服务端(Service)
<resources>
<paths >
<files-path path="images/" name="myimages" />
paths >
resources>
不知道怎么创建xml文件的可以查看这里。
对于上面的代码有几点需要说明:
标签共享的是在我们应用的内部存储中“files/”目录下的目录(等同于用getFilesDir()返回的路径)。
path属性的值表示的是前面的文件夹下的子文件,比如上例表示的是“files/images”。
name属性表示的是与路径对应的在URI中的值。
这个元素可以包含多个子标签,每一个都可以设置不同的分享路径,除了上例中使用的<files-path>
之外,还可以使用
来分享external storage中的文件,还有
用来分享internal storage中的cache文件。
有一点需要注意的是:
通过xml文件的方式来分享路径是唯一的方式,不能通过代码添加路径.
现在我们有一个完整的FileProvider声明,它在应用程序的内部存储中“files/”目录或其子目录下创建文件的Content URI。当我们的应用为一个文件创建了Content URI,该Content URI将会包含下列信息:
标签中指定的Authority(“com.example.myapp.fileprovider”);
路径“myimages/”;
文件的名字
比如本例子定义了一个FileProvider,然后我们需要一个文件“panda.jpg”的Content URI,FileProvider会返回如下URI:
content://com.example.service.fileprovider/myimages/panda.jpg
通过上述设置好之后,你的app就可以响应其他app的文件请求了.而对于如何响应,一种方法是服务器app(也就是你的app)提供一个文件选择接口,让请求的app来调用,然后它就能通过该接口获得选中文件的URI.
step 1 : 接收文件请求
为了接收文件请求和返回相应的URI,你的app需要提供一个文件选择的Activity,客服端app通过调用startActivityForResult()并携带一个action为ACTION_PICK的intent来启动你的Activity,然后你做相应的处理并返回结果。
step 2 : 创建一个选择文件的Activity
为建立一个选择文件的Activity,首先需要在Manifest清单文件中定义Activity,在其Intent过滤器中,匹配ACTION_PICKAction及CATEGORY_DEFAULT和CATEGORY_OPENABLE这两种Category。另外,还需要为应用程序设置MIME类型过滤器,来表明我们的应用程序可以向其他应用程序提供哪种类型的文件。
下面这段代码展示了如何在清单文件中定义新的Activity和Intent过滤器:
服务端(Service)
"http://schemas.android.com/apk/res/android">
...
...
".MainActivity"
android:label="File Selector" >
"android.intent.action.MAIN" />
"android.intent.action.PICK"/>
"android.intent.category.LAUNCHER" />
"android.intent.category.DEFAULT"/>
"android.intent.category.OPENABLE"/>
"text/plain"/>
"image/*"/>
...
step 3 : 在代码中定义文件选择Activity
定义一个Activity子类,用于显示在内部存储的“files/images/”目录下可以获得的文件,然后允许用户选择期望的文件。下面代码展示了如何定义该Activity,并令其响应用户的选择:
服务端(Service)
MainActivity.java :
我们会先创建一个文件,然后用ListView展示出来
public class MainActivity extends Activity {
private File mPrivateRootDir;
private File mImagesDir;
File[] mImageFiles;
//存放图片
List mBitmapList;
//存放图片名称
List mBitmaoNamesList;
private ListView listView;
FileProviderAdapter myAdapter;
boolean isOK;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createInternalFiles();
getBitmapsAndNames();
listView = (ListView) this.findViewById(R.id.listView);
myAdapter = new FileProviderAdapter(this, mBitmaoNamesList, mBitmapList);
listView.setAdapter(myAdapter);
...
}
//我们用一个数组存放内部文件的名子和bitmap
private void getBitmapsAndNames() {
mImageFiles = mImagesDir.listFiles();
mBitmapList = new ArrayList();
mBitmaoNamesList = new ArrayList();
for (int i = 0; i < mImageFiles.length; i++) {
File image = mImageFiles[i];
mBitmaoNamesList.add(image.getName());
try {
FileInputStream fis = new FileInputStream(image);
Bitmap bitmap = BitmapFactory.decodeStream(fis);
mBitmapList.add(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
//内部存储中创建了一个文件panda.jpg,事先需要我们把panda.jpg文件放入mipmap中
private void createInternalFiles() {
mPrivateRootDir = getFilesDir();
mImagesDir = new File(mPrivateRootDir, "images");
if (!mImagesDir.exists()) {
mImagesDir.mkdirs();
}
File mPandaIcon = new File(mImagesDir, "panda.jpg");
Bitmap pandaBp = BitmapFactory.decodeResource(getResources(), R.mipmap.panda);
saveFiles(mPandaIcon, pandaBp);
}
//保存文件
private void saveFiles(File mPandaIcon, Bitmap pandaBp) {
FileOutputStream fos = null;
if (pandaBp != null) {
try {
fos = new FileOutputStream(mPandaIcon);
pandaBp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
...
}
创建一个ListView的子项布局文件:
list_item.xml :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/adpter_img"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/adapter_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="15dp"
android:textSize="18sp"
android:textColor="@color/colorPrimaryDark"
android:text="Hello"/>
LinearLayout>
现在我们可以用ListView显示出来了,当然我们还要自己写一个Adapter**(FileProviderAdapte)**:
服务端(Service)
public class FileProviderAdapter extends BaseAdapter{
private List mData;
private Context mContext;
private LayoutInflater mLayoutInflater;
private List mBitmaps;
public FileProviderAdapter(Context context, List mBitmaoNamesList) {
this.mContext = context;
this.mData = mBitmaoNamesList;
mLayoutInflater = LayoutInflater.from(mContext);
}
public FileProviderAdapter(Context context, List mBitmaoNamesList, List mBitmapList) {
this.mContext = context;
this.mData = mBitmaoNamesList;
mLayoutInflater = LayoutInflater.from(mContext);
this.mBitmaps = mBitmapList;
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = mLayoutInflater.inflate(R.layout.list_item,null);
viewHolder.img = (ImageView) convertView.findViewById(R.id.adpter_img);
viewHolder.title = (TextView) convertView.findViewById(R.id.adapter_txt);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
if(mBitmaps != null){
viewHolder.img.setImageBitmap(mBitmaps.get(position));
}else{
viewHolder.img.setBackgroundResource(R.mipmap.ic_launcher);
}
viewHolder.title.setText(mData.get(position));
return convertView;
}
public final class ViewHolder{
public ImageView img;
public TextView title;
}
}
接下来我们为listView定义点击事件。
step 4 : 响应一个文件选择
一旦用户选择了一个文件,你的应用就要决定具体是哪个文件并产生相应的URI,比如上例,我们把文件列在ListView中,当用户点击了某个文件,你可以在ListView的onItemClick()f方法中拿到相关信息,就可以知道是哪个文件.然后将该文件之前在
中定义的FileProvider的authority以及Context这三个作为参数,调用getUriForFile()方法,就可以得到一个URI.如下示例:
protected void onCreate(Bundle savedInstanceState) {
...
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
File file = new File(mImagesDir, mBitmaoNamesList.get(position));
Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.example.service.fileprovider", file);
Intent intent = new Intent("com.example.myapp.ACTION_RETURN_FILE");
if (uri != null) {
//给提供的分享文件授权
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uri, getContentResolver().getType(uri));
MainActivity.this.setResult(Activity.RESULT_OK, intent);
} else {
intent.setDataAndType(null, "");
MainActivity.this.setResult(RESULT_CANCELED, intent);
}
isOK = true;
}
});
...
}
记住你要生成的URI的文件必须是在meta-data中定义的路径下的文件,否则会报错。
setFlags是最好的临时授权方式 ,避免用Context.grantUriPermission(),因为一旦调用这个方法,我们必须用Context.revokeUriPermission来取消这个权限, 而这个setFlags,只要这个activity所在的任务栈没被finish掉,临时权限就一起存在 ,也就是说如果你点back button一起返回finish这个任务栈,或者重启,这个权限就自动消失了。
当我们点击了这个item,我们需要一个button来让我们finish掉这个界面 ,我们在actionbar上定义了一个DONE。
在res/menu/中创建menu_main.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_done"
android:title="DONE"
app:showAsAction="ifRoom" />
menu>
服务端(Service)
MainActivity.java:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_done:
if(isOk) {
finish();
isOk = false;
}
default:
return super.onOptionsItemSelected(item);
}
}
现在我们已经完成了setResult工具,(设置一个Intent将相应参数传入,然后传入setResult()中,当该Activity结束的时候,客户端app就会收到这个Intent对象),我们再回到我们请求的Activity中:
Client(客户端)
MainActivity.java:
首先我们要有客户端的请求, 点击按钮,弹出服务端应用:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView)this.findViewById(R.id.textView);
imageView = (ImageView)this.findViewById(R.id.client_img);
button = (Button)this.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mRequestFileIntent = new Intent(Intent.ACTION_PICK);
mRequestFileIntent.setType("image/jpg");
startActivityForResult(Intent.createChooser(mRequestFileIntent, "Get File"), 666);
}
});
}
访问已请求的文件
客户端app在onActivityResult()方法中拿到服务器app返回的URI之后,就可以通过获取该文件的FileDescriptor访问该文件了.
客户端app唯一能够访问的文件就是拿到的URI的匹配文件,服务器app的其他文件它发现不了也打开不了,因为URI中不包含路径.
下面看具体处理示例:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode==600&&resultCode == RESULT_OK) {
Log.d("david", "onActivityResult");
Uri uri = data.getData();
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
//move to fist cursor ,default is -1
cursor.moveToNext();
String name = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
String size = cursor.getString(cursor.getColumnIndex(OpenableColumns.SIZE));
textView.setText("Name : " + name + " Size : " + size);
try {
mInputPFD = getContentResolver().openFileDescriptor(uri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e("MainActivity", "File not found.");
}
FileDescriptor fd = mInputPFD.getFileDescriptor();
FileInputStream fis = new FileInputStream(fd);
Bitmap bitmap = BitmapFactory.decodeStream(fis);
imageView.setImageBitmap(bitmap);
}
}
获得临时授权的uri后,我们可以基本像访问content provider一样,访问这个uri中特定的内容。
ContentResolver.openFileDescriptor(uri,”r”) 得到是一个ParcelFileDescriptor,通过这个ParcelFileDescriptor.getFileDescriptor可以得到FileDescriptor,这个FileDescriptor就可以用来读取文件了
当然我们可以用这个uri做其它的事情
例如 返回mime type
String mimeType = getContentResolver().getType(returnUri);
效果展示:
放上Client应用MainActivity.java的完整代码:
public class MainActivity extends AppCompatActivity {
private TextView textView;
private ImageView imageView;
private Button button;
private Intent mRequestFileIntent;
private ParcelFileDescriptor mInputPFD;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView)this.findViewById(R.id.textView);
imageView = (ImageView)this.findViewById(R.id.client_img);
button = (Button)this.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mRequestFileIntent = new Intent(Intent.ACTION_PICK);
mRequestFileIntent.setType("image/jpg");
startActivityForResult(Intent.createChooser(mRequestFileIntent, "Get File"), 666);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode==600&&resultCode == RESULT_OK) {
Log.d("david", "onActivityResult");
Uri uri = data.getData();
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
//move to fist cursor ,default is -1
cursor.moveToNext();
String name = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
String size = cursor.getString(cursor.getColumnIndex(OpenableColumns.SIZE));
textView.setText("Name : " + name + " Size : " + size);
try {
mInputPFD = getContentResolver().openFileDescriptor(uri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e("MainActivity", "File not found.");
}
FileDescriptor fd = mInputPFD.getFileDescriptor();
FileInputStream fis = new FileInputStream(fd);
Bitmap bitmap = BitmapFactory.decodeStream(fis);
imageView.setImageBitmap(bitmap);
}
}
}
放上Service应用MainActivity.java的完整代码:
public class MainActivity extends AppCompatActivity {
private File mPrivateRootDir;
// The path to the "images" subdirectory
private File mImagesDir;
// Array of files in the images subdirectory
File[] mImageFiles;
// Array of filenames corresponding to mImageFiles
String[] mImageFilenames;
List mBitmapList;
List mBitmaoNamesList;
private ListView listView;
FileProviderAdapter myAdapter;
boolean isOK;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createInternalFiles();
getBitmapsAndNames();
listView = (ListView) this.findViewById(R.id.listView);
myAdapter = new FileProviderAdapter(this, mBitmaoNamesList, mBitmapList);
listView.setAdapter(myAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
File file = new File(mImagesDir, mBitmaoNamesList.get(position));
Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.example.service.fileprovider", file);
Intent intent = new Intent("com.example.myapp.ACTION_RETURN_FILE");
if (uri != null) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uri, getContentResolver().getType(uri));
MainActivity.this.setResult(Activity.RESULT_OK, intent);
} else {
intent.setDataAndType(null, "");
MainActivity.this.setResult(RESULT_CANCELED, intent);
}
isOK = true;
}
});
}
private void getBitmapsAndNames() {
mImageFiles = mImagesDir.listFiles();
mBitmapList = new ArrayList();
mBitmaoNamesList = new ArrayList();
for (int i = 0; i < mImageFiles.length; i++) {
File image = mImageFiles[i];
mBitmaoNamesList.add(image.getName());
try {
FileInputStream fis = new FileInputStream(image);
Bitmap bitmap = BitmapFactory.decodeStream(fis);
mBitmapList.add(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
private void createInternalFiles() {
mPrivateRootDir = getFilesDir();
mImagesDir = new File(mPrivateRootDir, "images");
if (!mImagesDir.exists()) {
mImagesDir.mkdirs();
}
File mPandaIcon = new File(mImagesDir, "panda.jpg");
Bitmap pandaBp = BitmapFactory.decodeResource(getResources(), R.mipmap.panda);
saveFiles(mPandaIcon, pandaBp);
}
private void saveFiles(File mPandaIcon, Bitmap pandaBp) {
FileOutputStream fos = null;
if (pandaBp != null) {
try {
fos = new FileOutputStream(mPandaIcon);
pandaBp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_done:
if (isOK) {
finish();
isOK = false;
}
default:
return super.onOptionsItemSelected(item);
}
}
}