网盘存储
个人开发者往往没有自己的后台服务器,但同时又想获取app的运行信息,这就要借助于第三方的网络存储(也叫网盘、云盘、微盘等等)。通过让app自动在网盘上存取文件,可以间接实现后台服务器的存储功能,同时开发者也能及时找到app的用户信息。
曾几何时,各大公司纷纷推出免费的个人网盘服务,还开放了文件管理api给开发者调用,一时间涌现了网盘提供商的八大金刚:百度网盘、阿里云、华为网盘、腾讯微云、新浪微盘、360云盘、金山快盘、115网盘。可是好景不长,出于盈利、监管等等因素,各大网盘开放平台要么停止免费服务、推出收费服务,要么停止个人服务、推出企业服务,要么保留老用户、不再接受新用户,总之现在开发者已不能无偿使用网盘的sdk集成功能了。要想实现app的云存储,得用点真金白银了。
百度网盘
百度的个人网盘又称pcs(Personal Cloud Storage),百度网盘的存储服务本身是免费的,只是对开发者来说,已经不再接受新应用接入pcs。之前已申请的老应用则不受影响(大约是2015年之前的老应用),所以博主寻寻觅觅,总算找到了一个曲线救国道路,让新开发者也能在app中使用网盘服务。
具体方法如下:
1、先注册成为百度用户,并开通个人网盘服务,正常的话访问http://pan.baidu.com/就能看到网盘的管理页面;
2、把代码中的mbApiKey和mbRootPath替换为wp2pcs应用的信息,注意存储路径只能是wp2pcs,因为该应用在申请时就指定了这个默认路径,如果使用其他路径会报没权限;
private final static String mbApiKey = "CuOLkaVfoz1zGsqFKDgfvI0h";
private final static String mbRootPath = "/apps/wp2pcs";
3、app首次运行进行登录时,要把你的帐号授权给wp2pcs应用,以便创建目录、读写文件等操作;下面是百度pcs的登录授权页面截图
百度网盘的代码调用流程是:先调用BaiduOAuth对象的startOAuth鉴权方法(logout是注销方法),从返回包中获取AccessToken,后续的文件读写操作会用到这个AccessToken。接着实例化一个AccessToken对象,然后对该对象设置AccessToken,再根据业务要求分别调用相应的方法,常用方法说明如下:
quota : 获取磁盘空间。返回PCSQuotaResponse对象。
list : 获取文件/目录列表。返回PCSListInfoResponse对象。
makeDir : 创建目录。返回PCSFileInfoResponse对象。
deleteFiles : 删除文件。返回PCSSimplefiedResponse对象。
downloadFileFromStream : 下载文件。返回PCSSimplefiedResponse对象。
uploadFile : 上传文件。返回PCSFileInfoResponse对象。
rename : 文件改名。返回PCSFileFromToResponse对象。
下面是app操作百度pcs的页面截图
下面是使用百度网盘的代码示例:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.baidu.oauth.BaiduOAuth;
import com.baidu.oauth.BaiduOAuth.BaiduOAuthResponse;
import com.baidu.pcs.BaiduPCSActionInfo;
import com.baidu.pcs.BaiduPCSActionInfo.PCSCommonFileInfo;
import com.baidu.pcs.BaiduPCSActionInfo.PCSFileFromToInfo;
import com.baidu.pcs.BaiduPCSActionInfo.PCSFileFromToResponse;
import com.baidu.pcs.BaiduPCSActionInfo.PCSFileInfoResponse;
import com.baidu.pcs.BaiduPCSActionInfo.PCSListInfoResponse;
import com.baidu.pcs.BaiduPCSActionInfo.PCSQuotaResponse;
import com.baidu.pcs.BaiduPCSActionInfo.PCSSimplefiedResponse;
import com.baidu.pcs.BaiduPCSClient;
import com.baidu.pcs.BaiduPCSStatusListener;
import com.example.exmnetdisk.R;
import com.example.exmnetdisk.pcs.ConfirmDialogFragment.ConfirmCallbacks;
import com.example.exmnetdisk.pcs.InputDialogFragment.InputCallbacks;
public class PCSActivity extends Activity implements OnClickListener
,OnItemClickListener,OnItemLongClickListener,ConfirmCallbacks,InputCallbacks {
private static final String TAG = "PCSActivity";
private TextView tv_desc;
private TextView tv_path;
private ListView lv_content;
private Button btn_login;
private Button btn_logout;
private Button btn_upload;
private Button btn_rename;
private Button btn_back;
private Button btn_create;
private String mbOauth = null;
private final static String mbApiKey = "CuOLkaVfoz1zGsqFKDgfvI0h";
private final static String mbRootPath = "/apps/wp2pcs";
private String mCurrentPath = mbRootPath;
private String mLocalPath;
private Handler mbUiThreadHandler = new Handler();
private final static String POSITION = "position";
private final static String OPERATION = "operation";
private final static String FILEPATH = "file_path";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pcs);
tv_desc = (TextView)findViewById(R.id.tv_desc);
tv_path = (TextView)findViewById(R.id.tv_path);
lv_content = (ListView)findViewById(R.id.lv_content);
lv_content.setOnItemClickListener(this);
lv_content.setOnItemLongClickListener(this);
btn_login = (Button)findViewById(R.id.btn_login);
btn_logout = (Button)findViewById(R.id.btn_logout);
btn_upload = (Button)findViewById(R.id.btn_upload);
btn_rename = (Button)findViewById(R.id.btn_rename);
btn_back = (Button)findViewById(R.id.btn_back);
btn_create = (Button)findViewById(R.id.btn_create);
btn_login.setOnClickListener(this);
btn_logout.setOnClickListener(this);
btn_upload.setOnClickListener(this);
btn_rename.setOnClickListener(this);
btn_back.setOnClickListener(this);
btn_create.setOnClickListener(this);
mLocalPath = Environment.getExternalStorageDirectory().toString()+"/Download";
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_login) {
login();
} else if (v.getId() == R.id.btn_logout) {
logout();
} else if (v.getId() == R.id.btn_upload) {
upload("2.txt");
} else if (v.getId() == R.id.btn_rename) {
rename();
} else if (v.getId() == R.id.btn_back) {
if (null != mbOauth) {
if (mCurrentPath.equals(mbRootPath)) {
showTip("当前已是根目录");
} else {
mCurrentPath = mCurrentPath.substring(0, mCurrentPath.lastIndexOf("/"));
getList(mCurrentPath);
}
}
} else if (v.getId() == R.id.btn_create) {
InputDialogFragment dialog = InputDialogFragment.newInstance("请输入目录名称");
String fragTag = getResources().getString(R.string.app_name);
dialog.show(getFragmentManager(), fragTag);
}
}
private void showTip(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
}
//登录
private void login() {
BaiduOAuth oauthClient = new BaiduOAuth();
oauthClient.startOAuth(this, mbApiKey, new String[]{"basic", "netdisk"}, new BaiduOAuth.OAuthListener() {
@Override
public void onException(String msg) {
showTip("Login failed " + msg);
}
@Override
public void onComplete(BaiduOAuthResponse response) {
if (null != response) {
mbOauth = response.getAccessToken();
showTip("Token: " + mbOauth + " User name:" + response.getUserName());
tv_desc.setText("当前登录用户是"+response.getUserName()+",token为"+mbOauth);
getQuota();
getList(mbRootPath);
}
}
@Override
public void onCancel() {
showTip("Login cancelled");
}
});
}
//获取磁盘空间
private void getQuota() {
if (null != mbOauth) {
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduPCSClient api = new BaiduPCSClient();
api.setAccessToken(mbOauth);
final PCSQuotaResponse info = api.quota();
mbUiThreadHandler.post(new Runnable() {
public void run() {
if (null != info) {
if (0 == info.status.errorCode) {
showTip("Quota :" + info.total + " used: " + info.used);
tv_desc.setText(tv_desc.getText()+"\n网盘总容量是"+info.total/1024/1024+"M,已经使用"+info.used/1024/1024+"M.");
} else {
showTip("Quota failed: " + info.status.errorCode + " " + info.status.message);
}
}
}
});
}
});
workThread.start();
}
}
//注销
private void logout() {
if (null != mbOauth) {
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduOAuth oauth = new BaiduOAuth();
final boolean ret = oauth.logout(mbOauth);
mbUiThreadHandler.post(new Runnable() {
@Override
public void run() {
showTip("Logout " + ret);
}
});
}
});
workThread.start();
}
}
private List<PCSCommonFileInfo> mFileList = new ArrayList<PCSCommonFileInfo>();
//获取文件/目录列表
private void getList(final String path) {
if (null != mbOauth) {
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduPCSClient api = new BaiduPCSClient();
api.setAccessToken(mbOauth);
final PCSListInfoResponse ret = api.list(path, "name", "asc");
mbUiThreadHandler.post(new Runnable() {
public void run() {
if (ret.status.errorCode == 0) {
mFileList.clear();
mFileList = ret.list;
FileListAdapter adapter = new FileListAdapter(getApplicationContext(), mFileList);
lv_content.setAdapter(adapter);
tv_path.setText("当前目录是"+mCurrentPath);
} else {
tv_path.setText("获取目录"+mCurrentPath+"失败,原因是"+ret.status.message);
}
}
});
}
});
workThread.start();
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
PCSCommonFileInfo file = mFileList.get(position);
if (file.isDir == true) {
mCurrentPath = file.path;
showTip("您点击了目录"+mCurrentPath);
getList(mCurrentPath);
} else {
ConfirmDialogFragment fsf = ConfirmDialogFragment.newInstance(
R.drawable.ic_launcher, "下载确认", "是否下载文件"+file.path+"?");
Map<String, Object> map_param = new HashMap<String, Object>();
map_param.put(OPERATION, "download");
map_param.put(POSITION, position);
map_param.put(FILEPATH, file.path);
fsf.setParam(map_param);
fsf.show(getFragmentManager(), "");
}
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
PCSCommonFileInfo file = mFileList.get(position);
String desc = String.format("是否删除%s%s?", file.isDir?"目录":"文件", file.path);
ConfirmDialogFragment fsf = ConfirmDialogFragment.newInstance(
R.drawable.ic_launcher, "删除确认", desc);
Map<String, Object> map_param = new HashMap<String, Object>();
map_param.put(OPERATION, "delete");
map_param.put(POSITION, position);
map_param.put(FILEPATH, file.path);
fsf.setParam(map_param);
fsf.show(getFragmentManager(), "");
return true;
}
@Override
public void onConfirmSelect(Map<String, Object> map_param) {
String operation = (String) map_param.get(OPERATION);
if (operation.equals("delete")) {
int position = (Integer) map_param.get(POSITION);
PCSCommonFileInfo file = mFileList.get(position);
deletePCSFile(file.path);
} else if (operation.equals("download")) {
String file = (String) map_param.get(FILEPATH);
download(file);
}
}
@Override
public void onInput(String dir) {
createDir(dir);
}
//创建目录
private void createDir(final String dir) {
if (null != mbOauth) {
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduPCSClient api = new BaiduPCSClient();
api.setAccessToken(mbOauth);
String path = mCurrentPath + "/" + dir;
final PCSFileInfoResponse ret = api.makeDir(path);
mbUiThreadHandler.post(new Runnable() {
public void run() {
showTip("Mkdir: " + ret.status.errorCode + " " + ret.status.message);
getList(mCurrentPath);
}
});
}
});
workThread.start();
}
}
//删除文件。注意不能用deleteFile这个函数名,因为deleteFile在ContextWrapper类中已经定义
private void deletePCSFile(final String file_path) {
if (null != mbOauth) {
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduPCSClient api = new BaiduPCSClient();
api.setAccessToken(mbOauth);
List<String> files = new ArrayList<String>();
files.add(file_path);
final PCSSimplefiedResponse ret = api.deleteFiles(files);
mbUiThreadHandler.post(new Runnable() {
public void run() {
showTip("Delete files: " + ret.errorCode + " " + ret.message);
getList(mCurrentPath);
}
});
}
});
workThread.start();
}
}
private BaiduPCSStatusListener mStatusListener = new BaiduPCSStatusListener() {
@Override
public void onProgress(long bytes, long total) {
final long bs = bytes;
final long tl = total;
mbUiThreadHandler.post(new Runnable() {
public void run() {
showTip("total: " + tl + " loaded:" + bs);
}
});
}
@Override
public long progressInterval() {
return 500;
}
};
//下载文件
private void download(final String file) {
if (null != mbOauth) {
final String local_file = String.format("%s/%s", mLocalPath, file.substring(1+file.lastIndexOf("/")));
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduPCSClient api = new BaiduPCSClient();
api.setAccessToken(mbOauth);
final PCSSimplefiedResponse ret = api.downloadFileFromStream(file, local_file, mStatusListener);
mbUiThreadHandler.post(new Runnable() {
public void run() {
Log.d(TAG, "Download files: " + ret.errorCode + " " + ret.message);
showTip("download: " + ret.errorCode + " " + ret.message);
}
});
}
});
workThread.start();
}
}
// 上传文件
private void upload(String file) {
if (null != mbOauth) {
final String local_file = String.format("%s/%s", mLocalPath, file);
final String remote_file = String.format("%s/%s", mCurrentPath, file);
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduPCSClient api = new BaiduPCSClient();
api.setAccessToken(mbOauth);
final PCSFileInfoResponse response = api.uploadFile(local_file, remote_file, mStatusListener);
mbUiThreadHandler.post(new Runnable() {
public void run() {
showTip("upload: " + response.status.errorCode + " " + response.status.message + " " + response.commonFileInfo.blockList);
getList(mCurrentPath);
}
});
}
});
workThread.start();
}
}
//文件改名
private void rename() {
if (null != mbOauth) {
Thread workThread = new Thread(new Runnable() {
@Override
public void run() {
BaiduPCSClient api = new BaiduPCSClient();
api.setAccessToken(mbOauth);
List<PCSFileFromToInfo> info = new ArrayList<PCSFileFromToInfo>();
PCSFileFromToInfo data1 = new PCSFileFromToInfo();
data1.from = mCurrentPath + "/2.txt";
data1.to = "1.txt";
info.add(data1);
Log.d(TAG, "from="+data1.from+",to="+data1.to);
final PCSFileFromToResponse ret = api.rename(info);
mbUiThreadHandler.post(new Runnable() {
public void run() {
showTip("Rename: " + ret.status.errorCode + " " + ret.status.message);
getList(mCurrentPath);
}
});
}
});
workThread.start();
}
}
}
阿里云
阿里云的存储服务又称oss(Online Storage Service),现只提供收费服务,最经济的是40G空间半年5元,或一年9元,个人开发者的app有一定用户量时可以考虑购买。
阿里云的sdk的集成比较简单,官方也有提供demo,这里就不再赘述了。下面是阿里云对文件存储进行基本操作的代码示例:
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import com.alibaba.sdk.android.oss.ClientConfiguration;
import com.alibaba.sdk.android.oss.OSS;
import com.alibaba.sdk.android.oss.OSSClient;
import com.alibaba.sdk.android.oss.common.OSSLog;
import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider;
import com.alibaba.sdk.android.oss.common.auth.OSSPlainTextAKSKCredentialProvider;
import com.example.exmnetdisk.R;
public class AliyunActivity extends Activity {
private OSS oss;
// 以下字段为注册账号并开通oss服务后由阿里云自动分配
private static final String endpoint = "http://oss-cn-shanghai.aliyuncs.com";
private static final String accessKeyId = "<accessKeyId>";
private static final String accessKeySecret = "<accessKeySecret>";
private static final String testBucket = "<testBucket>";
// 需付费购买存储空间,才能正常使用上传、下载、列表等功能
private static final String uploadFilePath = "<upload_file_path>"; //上传的目录
private static final String uploadObject = "sampleObject"; //上传的文件
private static final String downloadObject = "sampleObject"; //下载的文件
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_aliyun);
OSSCredentialProvider credentialProvider = new OSSPlainTextAKSKCredentialProvider(accessKeyId, accessKeySecret);
ClientConfiguration conf = new ClientConfiguration();
conf.setConnectionTimeout(15 * 1000); // 连接超时,默认15秒
conf.setSocketTimeout(15 * 1000); // socket超时,默认15秒
conf.setMaxConcurrentRequest(5); // 最大并发请求书,默认5个
conf.setMaxErrorRetry(2); // 失败后最大重试次数,默认2次
OSSLog.enableLog();
oss = new OSSClient(getApplicationContext(), endpoint, credentialProvider, conf);
// 上传
Button upload = (Button) findViewById(R.id.upload);
upload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
new PutObjectSamples(oss, testBucket, uploadObject, uploadFilePath).asyncPutObjectFromLocalFile();
}
}).start();
}
});
// 下载
Button download = (Button) findViewById(R.id.download);
download.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
new GetObjectSamples(oss, testBucket, downloadObject).asyncGetObjectSample();
}
}).start();
}
});
// 罗列
Button list = (Button) findViewById(R.id.list);
list.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
new ListObjectsSamples(oss, testBucket).asyncListObjectsWithPrefix();
}
}).start();
}
});
// 管理
Button manage = (Button) findViewById(R.id.manage);
manage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
new ManageObjectSamples(oss, testBucket, uploadObject).headObject();
}
}).start();
}
});
// 分块上传
Button multipart = (Button) findViewById(R.id.multipart);
multipart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
new MultipartUploadSamples(oss, testBucket, uploadObject, uploadFilePath).multipartUpload();
}
catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
});
// 断点续传
Button resumable = (Button) findViewById(R.id.resumable);
resumable.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
new ResuambleUploadSamples(oss, testBucket, uploadObject, uploadFilePath).resumableUpload();
}
}).start();
}
});
// 签名URL
Button sign = (Button) findViewById(R.id.sign);
sign.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
new SignURLSamples(oss, testBucket, uploadObject, uploadFilePath).presignConstrainedURL();
}
}).start();
}
});
// bucket管理
Button bucket = (Button) findViewById(R.id.bucket);
bucket.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
new ManageBucketSamples(oss, testBucket, uploadFilePath).deleteNotEmptyBucket();
}
}).start();
}
});
}
}
点此查看Android开发笔记的完整目录