Android允許你通過(guò)Android Beam功能來(lái)傳輸大文件,不過(guò)該功能只能在帶有NFC并且Android版本在(4.1及以上 API 16)的設(shè)備上運(yùn)行.更多關(guān)于A(yíng)ndroid Beam的內(nèi)容可以看Beaming NDEF Messages to Other Devices,NFC的可以看Near Field Communication.
1. 發(fā)送文件到其他設(shè)備
要通過(guò)NFC發(fā)送文件,需要做到以下三點(diǎn):
- 請(qǐng)求權(quán)限(NFC和external storage的使用權(quán)限).
- 測(cè)試設(shè)備是否支持NFC.
- 給Android Beam提供URI.
對(duì)于A(yíng)ndroid Beam文件傳輸功能,有四點(diǎn)要求:
- 只支持Android 4.1(API 16)及以上.
- 傳輸?shù)奈募仨毷窃?strong>external storage中.
- 所有你要發(fā)送的文件的必須是world-readable的,你也可以通過(guò)File.setReadable(true,false)來(lái)設(shè)置該權(quán)限.
- 你必須要給要傳輸?shù)奈募?strong>提供相應(yīng)的URI(不能用 FileProvider.getUriForFile生成的Content URI).
1.1 在Manifest中聲明
1.1.1 請(qǐng)求權(quán)限
// NFC
<uses-permission android:name="android.permission.NFC" />
//READ_EXTERNAL_STORAGE
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE" />
注意READ_EXTERNAL_STORAGE
這個(gè)權(quán)限,在A(yíng)PI 19以前不需要,但是從API 19開(kāi)始就需要了,所以這里還是要加上,為了兼容.
1.1.2 設(shè)置NFC feature
因?yàn)镹FC是屬于hardware feature,需要添加<uses-feature>標(biāo)簽,如下:
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
注意:
-
android:required
為true表示如果你的設(shè)備沒(méi)有該feature,則你的app就不能運(yùn)行. 如果該功能只是你的一個(gè)可選擇的功能,你應(yīng)該將android:required
的值設(shè)為false. - 這個(gè)屬性只是告知性的,Google Play會(huì)用這個(gè)屬性來(lái)過(guò)濾你的設(shè)備不支持的應(yīng)用.
- 設(shè)置了<uses-feature>還要記得添加相應(yīng)的權(quán)限.
1.1.3 設(shè)置SDK版本
因?yàn)锳ndroid Beam只支持Android 4.1(API 16)及以上,你過(guò)你的app必須要該功能,那么需要設(shè)置android:minSdkVersion="16"
.
1.2 測(cè)試設(shè)備是否支持Android Beam
要測(cè)試設(shè)備是否支持Android Beam,需要三步:
i. 將NFC這個(gè)featrue設(shè)置成optional,如下:
<uses-feature
android:name="android.hardware.nfc"
android:required="false" />
ii. 測(cè)試設(shè)備是否支持NFC.調(diào)用PackageManager.hasSystemFeature()方法并將FEATURE_NFC作為參數(shù)傳入.
// NFC isn't available on the device
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)) {
/*
* Disable NFC features here.
* For example, disable menu items or buttons that activate
* NFC-related features
*/
// Android Beam file transfer isn't supported
}
iii. 測(cè)試設(shè)備是否支持Android Beam.通過(guò)判斷Android版本來(lái)測(cè)試.
// Android Beam file transfer isn't supported
if (Build.VERSION.SDK_INT <
Build.VERSION_CODES.JELLY_BEAN_MR1) {
// If Android Beam isn't available, don't continue.
mAndroidBeamAvailable = false;
/*
* Disable Android Beam file transfer features here.
*/
// Android Beam file transfer is available, continue
}
在驗(yàn)證設(shè)備已經(jīng)支持NFC和Android Beam后,使用時(shí)還有判斷設(shè)備是否開(kāi)啟了NFC和Android Beam功能,可以分別用下面兩個(gè)方法:
a. NfcAdapter的isEnabled()來(lái)驗(yàn)證NFC功能是否開(kāi)啟.
b. NfcAdapter的isNdefPushEnabled ()來(lái)驗(yàn)證是否開(kāi)啟了Android Beam功能.
1.3 創(chuàng)建提供文件的回調(diào)方法
一旦你驗(yàn)證了你的設(shè)備支持Android Beam來(lái)進(jìn)行文件傳輸之后,需要添加一個(gè)回調(diào)方法來(lái)返回一組Uri對(duì)象,這些對(duì)象是你想要傳輸?shù)奈募膹?fù)制品的URI,但Android Beam檢測(cè)到你想要分享文件給其他設(shè)備時(shí),系統(tǒng)就會(huì)調(diào)用該回調(diào)方法. 要添加該回調(diào)方法,要實(shí)現(xiàn)NfcAdapter.CreateBeamUrisCallback這個(gè)接口然后override里面的createBeamUris()這個(gè)抽象方法,如下示例:
public class MainActivity extends Activity {
...
// List of URIs to provide to Android Beam
private Uri[] mFileUris = new Uri[10];
...
/**
* Callback that Android Beam file transfer calls to get
* files to share
*/
private class FileUriCallback implements
NfcAdapter.CreateBeamUrisCallback {
public FileUriCallback() {
}
/**
* Create content URIs as needed to share with another device
*/
@Override
public Uri[] createBeamUris(NfcEvent event) {
return mFileUris;
}
}
...
}
實(shí)現(xiàn)了接口后,就可以將該接口實(shí)現(xiàn)類(lèi)的實(shí)例提供給Android Beam,通過(guò)setBeamPushUrisCallback()方法:
public class MainActivity extends Activity {
...
// Instance that returns available files from this app
private FileUriCallback mFileUriCallback;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Android Beam file transfer is available, continue
...
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
/*
* Instantiate a new FileUriCallback to handle requests for
* URIs
*/
mFileUriCallback = new FileUriCallback();
// Set the dynamic callback for URI requests.
mNfcAdapter.setBeamPushUrisCallback(mFileUriCallback,this);
...
}
...
}
注意: 除了通過(guò)setBeamPushUrisCallback()來(lái)提供URI之外,還可以使用setBeamPushUris()(方法,只不過(guò)前者是動(dòng)態(tài)的,后者是固定的.
1.4 設(shè)置要發(fā)送的文件
/*
* Create a list of URIs, get a File,
* and set its permissions
*/
private Uri[] mFileUris = new Uri[10];
String transferFile = "transferimage.jpg";
File extDir = getExternalFilesDir(null);
File requestFile = new File(extDir, transferFile);
requestFile.setReadable(true, false);
// Get a URI for the File and add it to the list of URIs
fileUri = Uri.fromFile(requestFile);
if (fileUri != null) {
mFileUris[0] = fileUri;
} else {
Log.e("My Activity", "No File URI available for file.");
}
- 注意: 上述文件URI的獲取是通過(guò)Uri.fromFile()方法.
- 上述的代碼只是使用Android Beam的一部分,若想了解更多可以看Beaming NDEF Messages to Other Devices.
2. 從其他設(shè)備接收文件
2.1 響應(yīng)展示數(shù)據(jù)的請(qǐng)求
當(dāng)Android Beam傳輸問(wèn)文件之后,它會(huì)彈出一個(gè)notification,里面包含有intent,這個(gè)intent由一個(gè)action為ACTION_VIEW,MIME類(lèi)型為第一個(gè)文件的文件類(lèi)型以及第一個(gè)文件的URI組成.當(dāng)用戶(hù)點(diǎn)擊這個(gè)notification之后,這個(gè)intent就會(huì)被發(fā)出. 要讓你的app來(lái)響應(yīng)該intent,你需要在manifest中相應(yīng)的Activity下添加<intent-filter>標(biāo)簽.如下示例:
<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>
2.2 請(qǐng)求文件讀取權(quán)限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
2.3 獲取復(fù)制過(guò)來(lái)的文件的路徑
Android Beam將所有復(fù)制過(guò)來(lái)的文件放在一個(gè)路徑下,上述提到的intent中包含了第一個(gè)文件的URI,但是你的app也有可能被其他的app啟動(dòng)而不一定是Android Beam,而其他啟動(dòng)你的app的intent攜帶的URI可能是不一樣的格式,所以你要判斷Uri的scheme和authority來(lái)決定如何處理:
public class MainActivity extends Activity {
...
// A File object containing the path to the transferred files
private File mParentPath;
// Incoming Intent
private Intent mIntent;
...
/*
* Called from onNewIntent() for a SINGLE_TOP Activity
* or onCreate() for a new Activity. For onNewIntent(),
* remember to call setIntent() to store the most
* current Intent
*
*/
private void handleViewIntent() {
...
// Get the Intent action
mIntent = getIntent();
String action = mIntent.getAction();
/*
* For ACTION_VIEW, the Activity is being asked to display data.
* Get the URI.
*/
if (TextUtils.equals(action, Intent.ACTION_VIEW)) {
// Get the URI from the Intent
Uri beamUri = mIntent.getData();
/*
* Test for the type of URI, by getting its scheme value
*/
if (TextUtils.equals(beamUri.getScheme(), "file")) {
mParentPath = handleFileUri(beamUri);
} else if (TextUtils.equals(
beamUri.getScheme(), "content")) {
mParentPath = handleContentUri(beamUri);
}
}
...
}
...
}
2.3.1 處理file URI
File URI包含文件的絕對(duì)路徑和文件名,處理如下:
...
public String handleFileUri(Uri beamUri) {
// Get the path part of the URI
String fileName = beamUri.getPath();
// Create a File object for this filename
File copiedFile = new File(fileName);
// Get a string containing the file's parent directory
return copiedFile.getParent();
}
...
2.3.2 處理Content URI
如果intent包含的是content URI,那么該URI指向的文件的路徑和文件名可能存在MediaStore中.你可以通過(guò)測(cè)試URI的authority的值來(lái)判斷其是否來(lái)自MediaStore. MediaStore的URI可能是來(lái)自Android Beam傳輸過(guò)來(lái)的也有可能是其他app傳過(guò)來(lái)的,但是兩者都能拿到路徑和文件名.
根據(jù)URI的authority來(lái)判斷content provider的類(lèi)型,然后進(jìn)行相應(yīng)的處理:
...
public String handleContentUri(Uri beamUri) {
// Position of the filename in the query Cursor
int filenameIndex;
// File object for the filename
File copiedFile;
// The filename stored in MediaStore
String fileName;
// Test the authority of the URI
if (!TextUtils.equals(beamUri.getAuthority(), MediaStore.AUTHORITY)) {
/*
* Handle content URIs for other content providers
*/
// For a MediaStore content URI
} else {
// Get the column that contains the file name
String[] projection = { MediaStore.MediaColumns.DATA };
Cursor pathCursor =
getContentResolver().query(beamUri, projection,
null, null, null);
// Check for a valid cursor
if (pathCursor != null &&
pathCursor.moveToFirst()) {
// Get the column index in the Cursor
filenameIndex = pathCursor.getColumnIndex(
MediaStore.MediaColumns.DATA);
// Get the full file name including path
fileName = pathCursor.getString(filenameIndex);
// Create a File object for the filename
copiedFile = new File(fileName);
// Return the parent directory of the file
return new File(copiedFile.getParent());
} else {
// The query didn't work; return null
return null;
}
}
}
...