Build Apps with Content Sharing

如何创建apps与设备之间共享数据的app.

Sharing Simple Data

使用IntentActionProvider在app之间收发简单数据。

Send Simple Data to Other Apps

当你构造一个intent时,你必须确认你想让intent来trigger什么行为.

Android 定义了几种行为,其中有一种ACTION_SEND,用来在activity之间传递数据,甚至在进程之间也可以。

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,系统会自动显示一个可选的dialog,当然,你可以调用Intent.createChooser(),在任何版本任何情况下都会显示选择框。优点:

  • 即使用户之前选择了默认的action,选择框还是会显示

  • 如果没有App match到,Android会提示系统消息

  • 你可以自定义选择框的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_SENDEXTRA_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)));

Note

  • 可以使用*/* MIME类型,但只能match到处理普通数据流的receiver.

  • 接收方需要权限来访问Uri指向的数据。这里有两种比较推荐的解决办法:

    • 创建app自身的ContentProvider,确保其他app可以访问你的provider:使用per-URI permissions -- 短期将权限授予给接收方.创建一个ContentProvider的简单方法是使用FileProvider的帮助类.
    • 使用系统MediaStore. 主要包含video, audio, image MIME 类型(在Android 3.0 以下还会包含一些非媒体类型). 文件可被插入到MediaStore中:使用scanFile(),然后将适合共享的Uri(content://)传递给onScanCompleted()回调.

Send Multiple Pieces of Content

使用ACTION_SEND_MULTIPLE action来发送多块数据(多条指向content的uri).
MIME 类型match你共享的所有文件.

ArrayList imageUris = new ArrayList();
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告诉系统App可以接受怎样的intent事件.


    
        
        
        
    
    
        
        
        
    

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 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


    

Set the Share Intent

使用ShareActionProvider,必须为其提供一个intent,需要先调用MenuItem.getActionProvider()来取回ShareActionProvider实例,再调用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数据:发送包含数据的Intent.

Setting Up File Sharing

FileProviderv4 Support Library 的一部分.

Specify the FileProvider

在配置文件中定义 FileProvider:


    
        
    
    ...

  • android:authorities: FileProvider 生成的content URIs

Specify Sharable Directories

一旦在配置文件中添加了FileProvider,就需要指定一个目录来放置你想共享的文件:

在res/xml中创建filepaths.xml.


    

  • files-path
  • external-path: share directories in external storage
  • cache-path: share directories in internal storage

所有准备工作做完以后,FileProvider会生成固定格式的访问URI:
content://com.example.myapp.fileprovider/myimages/

Sharing a File

从server app提供文件选择接口,以便让其他app可以唤起.

Receive File Requests

如果收到文件访问请求,你的app应该提供一个文件选择Activity. 请求方通过调用 startActivityForResult() (包含ACTION_PICK的Intent)启动该Activity.

Create a File Selection Activity

  • 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请求文件数据的方式一般为:startActivityForResult() + Intent(包含actionMIME 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的任务栈结束,文件不再可被外部访问。

@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

调用ContentResolver.getType()获取文件的数据类型(MIME)。一般地,FileProvider定义文件的MIME类型为其文件后缀。

Uri returnUri = returnIntent.getData();
String mimeType = getContentResolver().getType(returnUri);

Retrieve a File's Name and Size

FileProvider 有默认的query()实现:返回一个Cursor对象,用来查询含有文件名称和大小的content URI.

默认的实现中有两列:

  • DISPLAY_NAME: 文件名称,和File.getName()返回的数据一致

  • SIZE: 文件大小(long),和File.length()返回的数据一致

      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处理大量的数据,但是Android 4.0中引入的Android Beam NDFF 传输API只能处理少量数据.

Sending Files to Another Device

使用Android Beam传输大型文件. 完成功能之前,需要申请使用NFC和外部存储的权限,测试你的设备是否支持NFC,提供URI给Android Beam文件传输.

Android Beam文件传输功能有以下需求:

  • Android 4.1 以上
  • 传输文件必须在外部存储中
  • 所传输的文件必须是全局可读的. => 调用File.setReadable(true, false)来设置
  • 必须提供文件的URI. Android Beam 文件传输不能处理通过FileProvider.getUriForFile生成的URI

Declare Features in the Manifest

Request Permissions

  • NFC:
  • READ_EXTERNAL_STORAGE:

Specify the NFC feature

下添加标签,并设置android:required="true" => 声明如果没有NFC,app将无法工作.


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设备传输文件时,系统调用自定义的callback. 在这个callback方法中,返回一个Uri对象数组. Android Beam文件传输将这些文件传输给接收方.

实现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;    
        }
    }
}

实现以后,通过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查看文件,使用MediaStoreprovider为媒体文件添加内容.

Respond to a Request to Display Data

当Android Beam文件传输完毕,会发送一个包含ACTION_VIEW和MIME Type的Intent.
接收者需要定义来接收对应的唤起事件:

示例


    ...
    
        
        
        ...
    

Request File Permissions

权限申请:

  • 读:
  • 写:

Get the Directory for Copied Files

Android Beam一次性传输多个文件,传输结束调用Intent的URI指向第一个文件. 然而,你的app可能也会接收到来自其他文件传输的intent(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包含内容的URI,该URI指向存储在MediaStore的内容提供者(指向文件夹和文件名称).你可以通过测试URI的认证值来检测MediaStore的content URI.

你也可以通过接收ACTION_VIEWintent,包含content URI(除MediaStore之外的content provider).这种情况下,content URI不包含MediaStore权限值,并且content URI通常不指向目录。

Determine the content provider

调用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;    
    }
}

你可能感兴趣的:(Build Apps with Content Sharing)