如何創(chuàng)建apps與設備之間共享數(shù)據(jù)的app.
Sharing Simple Data
使用Intent
和ActionProvider
在app之間收發(fā)簡單數(shù)據(jù)剃盾。
Send Simple Data to Other Apps
當你構造一個intent時腺占,你必須確認你想讓intent來trigger
什么行為.
Android 定義了幾種行為淤袜,其中有一種ACTION_SEND
,用來在activity之間傳遞數(shù)據(jù)衰伯,甚至在進程之間也可以铡羡。
Note 最佳實踐是使用
ShareActionProvider
(Lesson -- Adding an Easy Share Action)添加action到ActionBar
中(> API 14).
Send Text Content
Talk is cheap, I will show you the code.
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);
如果有多個App接收到該intent,系統(tǒng)會自動顯示一個可選的dialog嚎研,當然蓖墅,你可以調(diào)用Intent.createChooser()
,在任何版本任何情況下都會顯示選擇框临扮。優(yōu)點:
即使用戶之前選擇了默認的action论矾,選擇框還是會顯示
如果沒有App match到,Android會提示系統(tǒng)消息
-
你可以自定義選擇框的title
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_to));
作為可選項杆勇,你也可以為intent設置一些基本的extra信息:EXTRA_EMAIL
, EXTRA_CC
, EXTRA_BCC
, EXTRA_SUBJECT
.
Note 一些e-mail應用(例如Gmail),會期望添加
String[]
作為extra.
Send Binary Content
使用ACTION_SEND
與EXTRA_STREAM
(URI)結(jié)合來發(fā)送二進制內(nèi)容贪壳。
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)));
Note
可以使用
*/*
MIME類型,但只能match到處理普通數(shù)據(jù)流的receiver.-
接收方需要權限來訪問Uri指向的數(shù)據(jù)蚜退。這里有兩種比較推薦的解決辦法:
- 創(chuàng)建app自身的
ContentProvider
闰靴,確保其他app可以訪問你的provider:使用per-URI permissions
-- 短期將權限授予給接收方.創(chuàng)建一個ContentProvider
的簡單方法是使用FileProvider
的幫助類. - 使用系統(tǒng)
MediaStore
. 主要包含video, audio, image MIME 類型(在Android 3.0 以下還會包含一些非媒體類型). 文件可被插入到MediaStore
中:使用scanFile()
,然后將適合共享的Uri(content://)傳遞給onScanCompleted()
回調(diào).
- 創(chuàng)建app自身的
Send Multiple Pieces of Content
使用ACTION_SEND_MULTIPLE
action來發(fā)送多塊數(shù)據(jù)(多條指向content的uri).
MIME 類型match你共享的所有文件.
ArrayList<Uri> imageUris = new ArrayList<Uri>();
imageUris.add(imageUri1);
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.."));
Receiving Simple Data from Other Apps
Update Your Manifest
Intent filters告訴系統(tǒng)App可以接受怎樣的intent事件.
<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_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
Handle the Incoming Content
void onCreate(Bundle savedInstanceState) {
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);
} else if (type.startsWith("image/")) {
handleSendImage(intent);
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) {
if (type.startsWith("image/")) {
handleSendMultipleImages(intent);
} else {
//Handle other intents
}
}
}
void handleSendText(Intent intent) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
//Update UI
}
}
void handleSendImage(Intent intent) {
Uri imageUri = (Uri) intent.getParcelbleExtra(Intent.EXTRA_STREAM);
if (imageUri != null) {
//Update
}
}
void handleSendMultipleImages(Intent intent) {
ArrayList<Uri> imageUris = intent.getParcelbleArrayList(Intent.EXTRA_STREAM);
if (imageUris != null) {
//Update
}
}
Adding an Easy Share Action
使用 ShareActionProvider
在ActionBar上添加share action(> Android 4.0).
Update Menu Declarations
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_item_share"
android:showAsAction="isRoom"
android:title="Share"
android:actionProviderClass="android.widget.ShareActionProvider" />
</menu>
Set the Share Intent
使用ShareActionProvider
钻注,必須為其提供一個intent蚂且,需要先調(diào)用MenuItem.getActionProvider()
來取回ShareActionProvider
實例,再調(diào)用setShareIntent()
為其指定intent.
private ShareActionProvider shareActionProvider;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.share_menu, menu);
MenuItem item = menu.findItem(R.id.menu_item_share);
shareActionProvider = (ShareActionProvider) item.getActionProvider();
return true;
}
private void setShareIntent(Intent shareIntent) {
if (shareActionProvider != null) {
shareActionProvider.setShareIntent(shareIntent);
}
}
Sharing Files
共享文件:提供App文件的URI給接收方幅恋,并給予短暫的可讀/可寫權限.
FileProvider
=> getUriForFile()
=> 生成文件的URI
共享較小的text/numeric數(shù)據(jù):發(fā)送包含數(shù)據(jù)的Intent.
Setting Up File Sharing
FileProvider
是 v4 Support Library
的一部分.
Specify the FileProvider
在配置文件中定義 FileProvider
:
<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>
-
android:authorities
: FileProvider 生成的content URIs
Specify Sharable Directories
一旦在配置文件中添加了FileProvider
杏死,就需要指定一個目錄來放置你想共享的文件:
在res/xml中創(chuàng)建filepaths.xml
.
<paths>
<files-path path="images/" name="myimages" />
</paths>
files-path
-
external-path
: share directories in external storage -
cache-path
: share directories in internal storage
所有準備工作做完以后,FileProvider
會生成固定格式的訪問URI:
content://com.example.myapp.fileprovider/myimages/<filename>
Sharing a File
從server app提供文件選擇接口捆交,以便讓其他app可以喚起.
Receive File Requests
如果收到文件訪問請求淑翼,你的app應該提供一個文件選擇Activity
. 請求方通過調(diào)用 startActivityForResult()
(包含ACTION_PICK
的Intent)啟動該Activity.
Create a File Selection Activity
<intent-filter>
- action:
ACTION_PICK
- category:
CATEGORY_DEFAULT
&CATEGORY_OPENABLE
- data: mimeType
Define the file selection Activity in code
定義Activity來展示你的app中files/images
的文件,并允許用戶點擊想要的文件.
public class MainActivity extends Activity {
private File mPrivareRootDir;
private File mImagesDir;
File[] mImageFiles;
String[] mImageFilenames;
@Override
protected void onCreate(Bundle savedInstanceState) {
mResultIntent = new Intent("com.example.myapp.ACTION_RETURN_FILE");
mPrivateRootDir = getFilesDir();
mImagesDir = new File(mPrivateRootDir, "images");
mImageFiles = mImageDir.listFiles();
setResult(Activity.RESULT_CANCELED, null);
}
}
Respond to a File Selection
如果用戶選擇了文件品追,你的app必須考慮為選中的文件提供URI.
因為Android 6.0以上只支持運行時授予權限玄括,所以需要避免使用Uri.fromFile()
:
- 不支持文件共享
- 你的app需要獲得
WRITE_EXTERNAL_STORAGE
權限(ANDROID 4.4 及以下) - 接收文件的app需要有
READ_EXTERNAL_STORAGE
權限
onItemClick()
=> File
Object => call getUriFromFile()
.
protected void onCreate(Bundle savedInstanceState) {
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
File requestFile = new File(mImageFilename[position]);
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);
}
}
});
}
Grant Permissions for the File
通過給 Intent 設置permission flags賦予讀取文件的權限.
protected void onCreate(Bundle savedInstanceState) {
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
mResultIntent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
});
}
避免使用Context.grantUriPermission()
:只能使用Context.revokeUriPermission()
收回權限
Share the File with the Requesting App
setResult
protected void onCreate(Bundle savedInstanceState) {
...
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
...
mResultIntent.setDataAndType(
fileUri,
getContentResolver().getType(fileUri));
MainActivity.this.setResult(Activity.RESULT_OK,
mResultIntent);
} else {
mResultIntent.setDataAndType(null, "");
MainActivity.this.setResult(Activity.RESULT_CANCELED,
mResultIntent);
}
}
});
}
Requesting a Shared File
Send a Request for the File
從server app請求文件數(shù)據(jù)的方式一般為:startActivityForResult()
+ Intent
(包含action
、MIME type
).
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);
...
}
...
}
Access the Requested File
override onActivityResult()
來處理接收文件肉瓦,一旦客戶端app有了文件的content URI遭京,可以通過獲取其FileDescriptor
來處理文件。
只有在server app賦予了訪問權限泞莉,client app獲取到文件訪問入口洁墙,文件才可被處理。由于權限是臨時的戒财,所以一旦client app的任務棧結(jié)束,文件不再可被外部訪問捺弦。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent returnIntent) {
if (resultCode != RESULT_OK) {
return;
} else {
Uri returnUri = returnIntent.getData();
try {
mInputPFD = getContentResolver().openFileDescriptor(returnUri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e("MainActivity", "File not found.");
return;
}
FileDescriptor fd = mInputPFD.getFileDescriptor();
...
}
}
Retrieving File Information
使用FileProvider
來獲取文件的類型和大小饮寞。
Retrieve a File's MIME Type
調(diào)用ContentResolver.getType()
獲取文件的數(shù)據(jù)類型(MIME)孝扛。一般地,FileProvider
定義文件的MIME類型為其文件后綴幽崩。
Uri returnUri = returnIntent.getData();
String mimeType = getContentResolver().getType(returnUri);
Retrieve a File's Name and Size
FileProvider
有默認的query()
實現(xiàn):返回一個Cursor
對象苦始,用來查詢含有文件名稱和大小的content URI.
默認的實現(xiàn)中有兩列:
DISPLAY_NAME
: 文件名稱,和File.getName()
返回的數(shù)據(jù)一致-
SIZE
: 文件大小(long)慌申,和File.length()
返回的數(shù)據(jù)一致Uri returnUri = returnIntent.getData(); Cursor returnCursor = getContentResolver().query(returnUri, null, null, null, null); 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(returnCursor.getString(sizeIndex));
Sharing Files with NFC
使用Android Beam(Android 自己的一個app陌选,僅支持Android 4.0以上) 文件傳輸功能傳輸較大的文件.
雖然Android Beam 傳輸API處理大量的數(shù)據(jù),但是Android 4.0中引入的Android Beam NDFF 傳輸API只能處理少量數(shù)據(jù).
Sending Files to Another Device
使用Android Beam傳輸大型文件. 完成功能之前蹄溉,需要申請使用NFC和外部存儲的權限咨油,測試你的設備是否支持NFC,提供URI給Android Beam文件傳輸.
Android Beam文件傳輸功能有以下需求:
- Android 4.1 以上
- 傳輸文件必須在外部存儲中
- 所傳輸?shù)奈募仨毷侨挚勺x的. => 調(diào)用
File.setReadable(true, false)
來設置 - 必須提供文件的URI. Android Beam 文件傳輸不能處理通過
FileProvider.getUriForFile
生成的URI
Declare Features in the Manifest
Request Permissions
- NFC:
<uses-permission android:name="android.permission.NFC" />
- READ_EXTERNAL_STORAGE:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Specify the NFC feature
在<manifest>
下添加<uses-feature>
標簽柒爵,并設置android:required="true"
=> 聲明如果沒有NFC役电,app將無法工作.
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
Test for Android Beam File Transfer Support
如果沒有NFC,你的app也可以工作棉胀,則設置required="false"
.
測試是否支持Android Beam文件傳輸:PackageManager.hasSystemFeature(FEATURE_NFC)
法瑟,然后檢查Android版本是否在4.1以上.
如果支持:獲取NFC控制器的實例(和NFC硬件進行交互).
public class MainActivity extends Activity {
NfcAdapter mNfcAdapter;
boolean mAndroidBeamAvailable = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
if (!PackageManager.hasSystemFeature(PackageManager.FEATURE_NFC)) {
//Disable NFC feature
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
mAndroidBeamAvailable = false;
} else {
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
}
}
}
Create a Callback Method that Provides Files
Android Beam文件傳輸檢測到用戶想要給其他的NFC設備傳輸文件時,系統(tǒng)調(diào)用自定義的callback. 在這個callback方法中唁奢,返回一個Uri對象數(shù)組. Android Beam文件傳輸將這些文件傳輸給接收方.
實現(xiàn)NfcAdapter.CreateBeamUrisCallback
接口以及其方法createBeamUris()
.
public class MainActivity extends Activity {
private Uri[] mFileUris = new Uri[10];
private class FileUriCallback implements NfcAdapter.CreateBeamUrisCallback {
public FileUriCallback() {}
@Override
public Uri[] createBeamUri(NfcEvent event) {
return mFileUris;
}
}
}
實現(xiàn)以后霎挟,通過setBeamPushUrisCallback()
激活該callback.
public class MainActivity extends Activity {
private FileUriCallback mFileUriCallback;
@Override
protected void onCreate(Bundle savedInstanceState) {
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
mFileUriCallback = new FileUriCallback();
mNfcAdapter.setBeamPushUrisCallback(mFileUriCallback, this);
}
}
你也可以直接提供一組uri給NfcAdapter:
NfcAdapter.setBeamPushUris()
Specify the Files to Send
private Uri[] mFileUris = new Uri[10];
String transferFile = "transferimage.jpg";
File extDir = getExternalFileDir(null);
File requestFile = new File(extDir, transferFile);
requestFile.setReadable(true, false);
fileUri = Uri.fromFile(requestFile);
if (fileUri != null) {
mFileUris[0] = fileUri;
} else {
Log.e("My Activity", "No File URI available for file.");
}
Receiving Files from Another Device
使用Android Media Scanner查看文件,使用MediaStore
provider為媒體文件添加內(nèi)容.
Respond to a Request to Display Data
當Android Beam文件傳輸完畢麻掸,會發(fā)送一個包含ACTION_VIEW
和MIME Type的Intent.
接收者需要定義<intent-filter>
來接收對應的喚起事件:
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.CATEGORY_DEFAULT" />
<data android:mimeType="mime-type" />
示例
<activity
android:name="com.example.android.nfctransfer.ViewActivity"
android:label="Android Beam Viewer" >
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
...
</intent-filter>
</activity>
Request File Permissions
權限申請:
- 讀:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- 寫:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Get the Directory for Copied Files
Android Beam一次性傳輸多個文件酥夭,傳輸結(jié)束調(diào)用Intent的URI指向第一個文件. 然而,你的app可能也會接收到來自其他文件傳輸?shù)膇ntent(ACTION_VIEW
). 為了處理接收時間论笔,你需要檢查其scheme和authority.
public class MainActivity extends Activity {
private File mParentPath;
private Intent mIntent;
private void handleViewIntent() {
mIntent = getIntent();
String action = mIntent.getAction();
if (TextUtils.equals(action, Intent.ACTION_VIEW)) {
Uri beamUri = mIntent.getData();
if (TextUtils.equals(beamUri.getScheme(), "file") {
mParentPath = handleFileUri(beamUri);
} else if (TextUtils.equals(beamUri.getScheme(), "content")) {
mParentPath = handleContentUri(beamUri);
}
}
}
}
Get the directory from a file URI
如果接收到的intent包含文件的URI采郎,該URI包含文件的絕對路徑和文件名稱.
public String handleFileUri(Uri beamUri) {
String fileName = beamUri.getPath();
File copiedFile = new File(fileName);
return copiedFile.getParent();
}
Get the directory from a content URI
如果接收到的intent包含內(nèi)容的URI,該URI指向存儲在MediaStore
的內(nèi)容提供者(指向文件夾和文件名稱).你可以通過測試URI的認證值來檢測MediaStore
的content URI.
你也可以通過接收ACTION_VIEW
intent狂魔,包含content URI(除MediaStore之外的content provider).這種情況下蒜埋,content URI不包含MediaStore權限值,并且content URI通常不指向目錄最楷。
Determine the content provider
調(diào)用Uri.getAuthority()
獲取URI的認證級別:
-
MediaStore.AUTHORITY
: 該URI用于由MediaStore追蹤的一個或多個文件.從MediaStore檢索完整的文件名整份,并從文件名獲取目錄. - Any other authority value: 來自其他content provider的content URI. 只可以顯示該文件,不能獲取文件目錄.
為了獲取MediaStore的content URI籽孙,通過過濾條件:收到的content URI(Uri)和MediaColumns.DATA
(projection)查找對應的目標.返回的Cursor
對象包含所有的傳輸文件的完整路徑和文件名稱.
public String handleContentUri(Uri beamUri) {
int filenameIndex;
File copiedFile;
String fileName;
if (!TextUtils.equals(beamUri.getAuthority(), MediaStore.AUTHORITY) {
//other content provider
} else {
String[] projection = { MediaStore.MediaColumns.DATA };
Cursor pathCursor = getContentResolver().query(beamUri, projection, null, null, null);
if (pathCursor != null && pathCursor.moveToFirst()) {
filenameIndex = pathCursor.getColumnIndex(MediaStore.MediaColumns.DATA);
fileName = pathCursor.getString(filenameIndex);
copiedFile = new File(fileName);
return new File(copiedFile.getParent());
}
} else {
return null;
}
}