原文链接:http://android.xsoftlab.net/training/building-content-sharing.html
第一篇文章,虽然是翻译,也会学习到一些内容的,以后会不定时更新的
这篇文章主要讲如何构建在不同应用和设备间共享数据的应用。该部分总共包含三部分,分别是:
- 共享简单数据
- 共享文件
- 通过NFC共享文件
共享简单数据
1. 发送简单数据到其他应用
1.1 发送文本内容
在应用间使用Intent发送、接收数据是最简单的分享内容的做法。Intent可以让用户使用他们喜欢的应用快速、简单的分享内容。
注意:在ActionBar上添加分享动作的最好做法就是使用ShareActionProvider,它从API14开始可用。
ACTION_SEND
action最直接、常见的用法是在activity间发送文本内容。例如内置的浏览器应用可以把当前显示页面的URL作为文本内容分享给任何应用。这对于朋友间通过email或社交网络分享文章或网址来说很重要。下面是一个例子:
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
sendIntent.setType("text/plain");
startActivity(sendIntent);
如果安装的应用有匹配的,就运行。如果匹配的应用超过一个,就显示一个应用选择列表,来让用户选择要启动的应用。然而,如果调用
Intent.createChooser()
,传入构建好的
Intent
对象,将返回一个永远展示选择器的Intent。 以下是改动后的代码:
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_to)));
效果图如下:
1.2 发送二进制内容
二进制数据通过ACTION_SEND action并设置合适的MIME类型,使用EXTRA_STREAM来标示附加 的URI数据。这经常用来分享图片,也可以分享任何类型的二进制数据。
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uriToImage);
shareIntent.setType("image/jpeg");
startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.send_to)));
注意以下几点:
- MIME类型也可以使用“*/*”,但是这样只有处理一般数据流的activity才能匹配
- 目标应用需要有相应的权限才能访问uri指向的数据。推荐的做法是:(1)在自己的ContentProvider中保存数据,确保其他应用有访问的权限。更好的提供访问权限的机制是使用per-URI权限,这是临时的并且只赋给目标应用。一个创建这样的ContentProvider的简单做法是使用FileProvider帮助类。(2)使用系统的MediaStore。MediaStore只有匹配video,audio和image的MIME类型,然而从Android 3.0 (APIlevel 11)开始,它也可以存储非media类型。content://类型的Uri传递给onScanCompleted()回调方法后,使用scanFile()就可以把文件插入MediaStore。要注意的是一旦添加到系统的MediaStore,这些内容就可以被设备的其他应用访问。
1.3 发送多条内容
为了发送多条内容,使用ACTION_SEND_MULTIPLE action,以及指向了目标内容的URI集合。MIME类型由要分享的内容而定。比如,要分享3张JPEG格式的图片,MIME类型还是“image/jpeg”。如果图片的格式不一致,MIME类型就是“image/*”,以此来匹配可以处理任何类型图片的Activity。如果分享的内容的类型有很多种,MIME类型是“*/*”。如前所述,由接收应用来解析、处理数据。示例如下:
ArrayList<Uri> imageUris = new ArrayList<Uri>();
imageUris.add(imageUri1); // Add your image URIs here
imageUris.add(imageUri2);
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
shareIntent.setType("image/*");
startActivity(Intent.createChooser(shareIntent, "Share images to.."));
和以前一样,要确保接收应用可以访问URI列表指向的数据。
2. 接收其他应用发送的简单数据
2.1 更新manifest文件
Intent 过滤器告诉系统一个应用的组件可以接收什么intent。和上一节创建带有ACTION_SEND动作的Intent相似,创建Intent过滤器是为了可以接收带有这种动作的Intent。在manifest文件中使用<intent-filter>节点添加intent过滤器。例如,如果你的应用可以接收文本内容,任何类型的一张图片,或任何类型的多张图片,manifest文件是这样:
<activity android:name=".ui.MyActivity" >
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
当另一个应用想要分享这些东西时,你的应用就会作为带选项显示在列表里。如果用户选择了你的应用,相应的activity(
.ui.MyActivity
)就会启动。然后就可以自行处理这些内容。
2.2 处理收到的内容
为了处理Intent传递的内容,调用getIntent()得到Intent对象,然后就可以决定下一步要做什么。始终要记住的一点是如果这个Activity可以被系统的其他部分启动,如Launcher,那么当你检查intent时要考虑到这一点。
void onCreate (Bundle savedInstanceState) {
...
// Get intent, action and MIME type
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent); // Handle text being sent
} else if (type.startsWith("image/")) {
handleSendImage(intent); // Handle single image being sent
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) {
if (type.startsWith("image/")) {
handleSendMultipleImages(intent); // Handle multiple images being sent
}
} else {
// Handle other intents, such as being started from the home screen
}
...
}
void handleSendText(Intent intent) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
// Update UI to reflect text being shared
}
}
void handleSendImage(Intent intent) {
Uri imageUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (imageUri != null) {
// Update UI to reflect image being shared
}
}
void handleSendMultipleImages(Intent intent) {
ArrayList<Uri> imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if (imageUris != null) {
// Update UI to reflect multiple images being shared
}
}
警告:要仔细检查收到的数据, 你不知道是否有其他的应用发送过数据。例如,发送的数据的MIME类型不对,或者发送的图片太大。而且,记得在另一个线程中处理二进制数据而不是在主线程("UI"线程)。UI的更新可以和定位一个EditText一样简单,也可以和在图片上使用一个图片过滤器一样复杂。这都取决于你的应用接下来会做什么。
3. 添加简单的分享动作
Android4.0(API14)引进的ActionProvider让我们在ActionBar上添加高效、易用的分享动作变得非常简单,一旦在ActionBar上添加一个菜单项,它就会处理菜单的外观和动作。使用ShareActionProvider时,我们提供一个Intent,然后Intent处理其他的部分。
注意:ShareActionProvider也是从API14开始引进的。
3.1 更新菜单说明
为了使用ShareActionProvider,在menu.xml文件的对应项定义android:actionProviderClass
属性:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_item_share"
android:showAsAction="ifRoom"
android:title="Share"
android:actionProviderClass=
"android.widget.ShareActionProvider" />
...
</menu>
这表明了item的外观和ShareActionProvider的功能。然而你应该告诉它你想要分享什么。.
3.2 设置分享intent
为了让ShareActionProvider起作用,你必须提供给它一个分享Intent。这个分享Intent应该和在“发送简单数据到其他应用”一节中描述的一样,有ACTION_SEND动作和通过特定标签,如EXTRA_TEXT和EXTRA_STREAM,附加的数据。为了分配一个分享Intent,首先在Activity或Fragment中找到对应的菜单项,然后调用MenuItem.getActionProvider()得到ShareActionProvider的一个实例对象。使用setShareIntent()设置Intent。下面是一个例子:
private ShareActionProvider mShareActionProvider;
...
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate menu resource file.
getMenuInflater().inflate(R.menu.share_menu, menu);
// Locate MenuItem with ShareActionProvider
MenuItem item = menu.findItem(R.id.menu_item_share);
// Fetch and store ShareActionProvider
mShareActionProvider = (ShareActionProvider) item.getActionProvider();
// Return true to display menu
return true;
}
// Call to update the share intent
private void setShareIntent(Intent shareIntent) {
if (mShareActionProvider != null) {
mShareActionProvider.setShareIntent(shareIntent);
}
}
你也许只需要在创建菜单栏的设置分享Intent,或者在UI变化时更新。例如,当使用图库应用全屏查看图片时,随着图片的滑动,分享Intent也会变。
共享文件
1. 设置文件分享
1.1 指定FileProvider
为应用定义FileProvider需要在manifest.xml文件中添加入口。该入口明确了生成URI时使用的authority,以及一个指明应用可以分享的目录的XML文件。下面是示例:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<application
...>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.myapp.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
...
</application>
</manifest>
例子中的 android:authorities
明确了使用FileProvider生成的URI的URI authority。例如"com.example.myapp.fileprovider
"。对于自己的应用定义authority时可以使用包名+fileprovider。<meta-data>子节点指向了定义应用可以分享的目录的XML文件。
android:resource
定义了文件的路径和名字。
在manifest.xml文件添加FileProvider后,在/res/xml/目录下定义
filepaths.xml
文件指明应用可以分享的目录和名字:
<paths>
<files-path path="images/" name="myimages" />
</paths>
<files-path>
标签分享应用内存的files/目录的子目录。
images/
目录实际代表的是files/images/
子目录。
<paths>
元素可以包含许多子元素,每一个都指明了要分享的不同目录。如使用<external-path>
分享内存中的目录;使用<cache-path>分享内存缓存目录。
当这一切都完成后,当你的应用为一个文件产生URI时,它会包含在manifest.xml文件中定义的authority,路径和文件名。如下所示:
content://com.example.myapp.fileprovider/myimages/default_image.jpg
2. 分享一个文件
2.1 接收文件请求
应用应该包含一个文件选择Activity,其他应用使用带有ACTION_PICK的intent,并调用startActivityForResult()。在manifest.xml文件中注册Activity时,应该包含以下内容:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application>
...
<activity
android:name=".FileSelectActivity"
android:label="@"File Selector" >
<intent-filter>
<action android:name="android.intent.action.PICK"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.OPENABLE"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
action:PICK;
category:DEFAULT和OPENABLE。
2.2 定义文件选择Activity
public class MainActivity extends Activity {
// The path to the root of this app's internal storage
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;
// Initialize the Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Set up an Intent to send back to apps that request a file
mResultIntent =
new Intent("com.example.myapp.ACTION_RETURN_FILE");
// Get the files/ subdirectory of internal storage
mPrivateRootDir = getFilesDir();
// Get the files/images subdirectory;
mImagesDir = new File(mPrivateRootDir, "images");
// Get the files in the images subdirectory
mImageFiles = mImagesDir.listFiles();
// Set the Activity's result to null to begin with
setResult(Activity.RESULT_CANCELED, null);
/*
* Display the file names in the ListView mFileListView.
* Back the ListView with the array mImageFilenames, which
* you can create by iterating through mImageFiles and
* calling File.getAbsolutePath() for each File
*/
...
}
...
}
2.3 响应文件选择
应用以ListView的形式列出所有的文件,当回调onItemClick()时,就代表选择了要分享的文件。根据得到的文件名字,生成一个File对象,并传递给getUriForFile(),生成一个URI。
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks on a file in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
/*
* When a filename in the ListView is clicked, get its
* content URI and send it to the requesting app
*/
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
/*
* Get a File for the selected file name.
* Assume that the file names are in the
* mImageFilename array.
*/
File requestFile = new File(mImageFilename[position]);
/*
* Most file-related method calls need to be in
* try-catch blocks.
*/
// Use the FileProvider to get a content URI
try {
fileUri = FileProvider.getUriForFile(
MainActivity.this,
"com.example.myapp.fileprovider",
requestFile);
} catch (IllegalArgumentException e) {
Log.e("File Selector",
"The selected file can't be shared: " +
clickedFilename);
}
...
}
});
...
}
记住,只可以为在文件中指明了的目录下的文件生成URI,否则会抛出IllegalArgumentException。
2.4 为文件授权
为指定文件生成URI后,还需要为其指定读的权限。此时可以在Intent中添加权限标志。此时授予的是临时权限,应用的回退栈结束后,权限就无效了:
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
// Grant temporary read permission to the content URI
mResultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
...
}
...
});
...
}
警告:使用setFlags()是唯一安全的授予临时权限访问文件的方式。避免使用Context.grantUriPermission(),因为这样授予的权限只能使用Context.removeUriPermission()移除。
2.5 在应用间分享文件
为了和请求应用分享文件,使用setResult()把包含URI值得Intent传递进去。系统会把Intent发送给请求应用:
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks on a file in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
...
// Put the Uri and MIME type in the result Intent
mResultIntent.setDataAndType(
fileUri,
getContentResolver().getType(fileUri));
// Set the result
MainActivity.this.setResult(Activity.RESULT_OK,
mResultIntent);
} else {
mResultIntent.setDataAndType(null, "");
MainActivity.this.setResult(RESULT_CANCELED,
mResultIntent);
}
}
});
给用户提供一种方式:当他们选择文件后,立即返回。此时可以在完成按钮上使用android:onCkick属性绑定一个监听来立即结束Activity:
public void onDoneClick(View v) {
// Associate a method with the Done button
finish();
}
3. 请求分享文件
3.1 发送文件请求
定义好Intent后,即设置了ACTION_PICK动作和数据的MIME类型,调用startActivityForResult()来启动目标Activity:
public class MainActivity extends Activity {
private Intent mRequestFileIntent;
private ParcelFileDescriptor mInputPFD;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRequestFileIntent = new Intent(Intent.ACTION_PICK);
mRequestFileIntent.setType("image/jpg");
...
}
...
protected void requestFile() {
/**
* When the user requests a file, send an Intent to the
* server app.
* files.
*/
startActivityForResult(mRequestFileIntent, 0);
...
}
...
}
3.2 访问请求到的文件
客户端在onActivityResult()中处理服务器端发送过来的Intent数据。得到文件的URI后,就可以使用FileDescriptor来访问文件。
在此过程中,文件安全是受保护的,因为URI是客户端收到的唯一数据。由于URI不包含目录路径,客户端就不能访问服务器端的任何文件。只有客户端可以使用服务器端授予的权限访问文件。由于权限是临时的,一旦客户端结束,在服务器外,文件就不可访问:
public void onActivityResult(int requestCode, int resultCode,
Intent returnIntent) {
// If the selection didn't work
if (resultCode != RESULT_OK) {
// Exit without doing anything else
return;
} else {
// Get the file's content URI from the incoming Intent
Uri returnUri = returnIntent.getData();
/*
* Try to open the file for "read" access using the
* returned URI. If the file isn't found, write to the
* error log and return.
*/
try {
/*
* Get the content resolver instance for this context, and use it
* to get a ParcelFileDescriptor for the file.
*/
mInputPFD = getContentResolver().openFileDescriptor(returnUri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e("MainActivity", "File not found.");
return;
}
// Get a regular file descriptor for the file
FileDescriptor fd = mInputPFD.getFileDescriptor();
...
}
}
openFileDescriptor()得到文件的ParcelFileDescriptor对象,使用该对象可以得到用于访问文件的FileDescriptor对象。
4. 检索文件信息
4.1 检索文件的MIME类型
客户端根据文件的数据类型决定如何处理。调用ContentResolver.getType()得到文件的类型,返回文件的MIME类型。默认FileProvider根据文件扩展名决定文件的MIME类型:
...
/*
* Get the file's content URI from the incoming Intent, then
* get the file's MIME type
*/
Uri returnUri = returnIntent.getData();
String mimeType = getContentResolver().getType(returnUri);
...
4.2 检索文件名和大小
FileProvider默认实现的query()方法可以根据文件URI得到的Cursor对象返回文件的名字和大小。默认返回两列:
- DISPLAY_NAME:文件名,String类型。和通过File.getName()得到的一样。
- SIZE:文件大小,long类型。和通过File.lengh()得到的一样。
...
/*
* Get the file's content URI from the incoming Intent,
* then query the server app to get the file's display name
* and size.
*/
Uri returnUri = returnIntent.getData();
Cursor returnCursor =
getContentResolver().query(returnUri, null, null, null, null);
/*
* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
TextView nameView = (TextView) findViewById(R.id.filename_text);
TextView sizeView = (TextView) findViewById(R.id.filesize_text);
nameView.setText(returnCursor.getString(nameIndex));
sizeView.setText(Long.toString(returnCursor.getLong(sizeIndex)));
...
通过NFC共享文件
1. 发送文件到其他设备
2. 从其他设备接收文件
最后的NFC还没有学习过,所以暂时没有翻译,等以后学习会回来更新的。