先前学习安卓偏向于界面的绘制,对于其他方面的知识比较薄弱,在逐步查缺补漏(就是填大坑,自己真的太菜了。)本学期开设了Android课程,其中一章为远程服务,自觉收获颇多,特写此文。本文为课程实验的学习与整理,若有错误请烦请指正。
一、知识点
(知识点有空再补全)
二、问题描述
假设一个APP“支付宝宝”提供支付的服务(Service),另一个APP“迪迪打车”可以调用该服务。要求
(1) 为“支付宝宝”设计远程服务接口IPayService,该接口包含:
- (a)方法pay,用于模拟支付。id_payer是付款人“支付宝宝”的账号。id_payee 是收款人的账号。amount_to_pay是要付款的数额。requester_app是发起支付的APP名称,本题指“迪迪打车”。
“支付宝宝”余额以及支付历史等信息,要求用SharedPreferences保存。
boolean pay (int id_payer, int id_payee, float amount_to_pay, String requester_app) - (b)方法getPayHistory,用于返回某APP发起的支付记录。PayHistory是自定义的数据类型,用来存放支付历史。
PayHistory getPayHistory(int id_payer, String requester_app)
(2) 为“迪迪打车”设计支付页面,调用“支付宝宝”IPayService接口中的pay方法进行支付,并将返回的支付结果显示。
(3) 为“迪迪打车”设计“查询支付历史”的功能,调用“支付宝宝”IPayService接口中的getPayHistory方法,得到“迪迪打车”的支付历史,并显示在界面上。
三、支付宝宝(ALiBaby)
(一) 需求分析
- 登录,因本次实验重点在于远程服务以及数据的简单存储与访问,故登录过程不做验证,仅作为余额的唯一id标识。
- 余额的显示与修改,使用TextView显示账号和余额,并提供修改余额的按钮。
- 远程服务接口IPayService,提供两个功能:
A. 支付功能
B. 历史查询功能
(根据题目所提供的函数格式,同一id仅保存一个最新的历史记录)
(二)界面绘制
- 登录
- 信息显示
(三)关键代码
- 登录(LoginActivity.java)
//获取用户输入的账号
String account = accountET.getText().toString();
//将账号存入Intent,传入MainActivity中,以获取相应账号的余额信息
Intent intent = new Intent(LoginActivity.this,MainActivity.class);
intent.putExtra("ACCOUNT",account);
startActivity(intent);
finish();
- 信息显示与修改(MainActivity.java)
//信息显示
float money = MoneyData.loadBalance(MainActivity.this,account);
mTextView.setText(account+",余额:"+money+" 元");
//信息修改
//获取想要修改成的金额
String money = mEditText.getText().toString();
//判断是否输入了金额,若输入则保存本地SharedPreferences,否则提示用户输入相关信息
if (money.equals("")){
Toast.makeText(MainActivity.this,"请输入修改金额!",Toast.LENGTH_SHORT).show();
return;
}else {
MoneyData.saveBalance(MainActivity.this,Float.parseFloat(money),account);
float m = MoneyData.loadBalance(MainActivity.this,account);
mTextView.setText(account+",余额:"+m+" 元");
}
- 历史信息类(PayHistory.java)
/**
* 支付历史信息类
* 对于自定义的数据类型,用户需要实现Parcelable接口
*/
public class PayHistory implements Parcelable {
private int id_payer;//付款人id
private int id_payee;//收款人id
private String requester_app;//收款方APP
private float amount_to_pay;//金额
private int is_success; //0 失败 1 成功
//满参构造函数
public PayHistory(int id_payer, int id_payee, String requester_app, float amount_to_pay,int is_success) {
this.id_payer = id_payer;
this.id_payee = id_payee;
this.requester_app = requester_app;
this.amount_to_pay = amount_to_pay;
this.is_success = is_success;
}
//以Parcel对象为输入的构造函数
public PayHistory(Parcel parcel) {
this.id_payer = parcel.readInt();
this.id_payee = parcel.readInt();
this.requester_app = parcel.readString();
this.amount_to_pay = parcel.readFloat();
this.is_success = parcel.readInt();
}
@Override
public int describeContents() {
return 0;
}
//重载打包函数,将本类内部的数据,按照特定的顺序写入Parcel对象
//写入的顺序必须与构造函数的读取顺序一致
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id_payer);
dest.writeInt(this.id_payee);
dest.writeString(this.requester_app);
dest.writeFloat(this.amount_to_pay);
dest.writeInt(this.is_success);
}
//实现静态公共字段CREATOR,用来使用Parcel对象构造PayHistory对象
public static final Creator<PayHistory> CREATOR = new Creator<PayHistory>(){
@Override
public PayHistory createFromParcel(Parcel parcel){
return new PayHistory(parcel);
}
@Override
public PayHistory[] newArray(int size) {
return new PayHistory[size];
}
};
//get&&set函数
...
}
- 信息的读取与保存(MoneyData.java)
public class MoneyData {
//基础名+id名(+app名)以此确定唯一id对应的唯一SharedPreferences
public static final String PREFERENCE_BALANCE="Balance";
public static final String PREFERENCE_PAYHISTORY="PayHistory";
public static int MODE = Context.MODE_PRIVATE;
//加载余额
public static float loadBalance(Context context,String account){
String name = PREFERENCE_BALANCE + account;
SharedPreferences sharedPreferences = context.getSharedPreferences(name,MODE);
float money = sharedPreferences.getFloat("Money",0);
return money;
}
//保存余额
public static void saveBalance(Context context,float money,String account){
String name = PREFERENCE_BALANCE + account;
SharedPreferences sharedPreferences = context.getSharedPreferences(name,MODE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putFloat("Money",money);
editor.commit();
}
//读取支付历史信息
public static PayHistory loadPayHistory(Context context,int payer,String app){
String name = PREFERENCE_PAYHISTORY+payer+app;
SharedPreferences sharedPreferences = context.getSharedPreferences(name,MODE);
int id_payer = sharedPreferences.getInt("ID_PAYER",0);
int id_payee = sharedPreferences.getInt("ID_PAYEE",0);
String requester_app = sharedPreferences.getString("REQUESTER_APP","DiDiTaxi");
float amount_to_pay = sharedPreferences.getFloat("AMOUNT_TO_PAY",0);
int is_success = sharedPreferences.getInt("IS_SUCCESS",0);
PayHistory payHistory = new PayHistory(id_payer,id_payee,requester_app,amount_to_pay,is_success);
return payHistory;
}
//保存支付历史
public static void savePayHistory(Context context,PayHistory payHistory,int payer,String app){
String name = PREFERENCE_PAYHISTORY+payer+app;
SharedPreferences sharedPreferences = context.getSharedPreferences(name,MODE);
SharedPreferences.Editor editor = sharedPreferences.edit();
int id_payer = payHistory.getId_payer();
editor.putInt("ID_PAYER",id_payer);
int id_payee = payHistory.getId_payee();
editor.putInt("ID_PAYEE",id_payee);
String requester_app = payHistory.getRequester_app();
editor.putString("REQUESTER_APP",requester_app);
float amount_to_pay = payHistory.getAmount_to_pay();
editor.putFloat("AMOUNT_TO_PAY",amount_to_pay);
int is_success = payHistory.getIs_success();
editor.putInt("IS_SUCCESS",is_success);
editor.commit();
}
}
- 远程服务函数(PayService.java)
//通过使用系统生成的IPayService.java内部的Stub类实现IBinder对象的建立
//逐一实现在IPayService.aidl接口文件定义的函数
private final IPayService.Stub mBinder = new IPayService.Stub() {
//支付函数
@Override
public boolean pay(int id_payer, int id_payee, float amount_to_pay, String requester_app) throws RemoteException{
//获取当前账户的余额
float balance = MoneyData.loadBalance(PayService.this,id_payer+"");
//根据参数构造支付历史
PayHistory payHistory = new PayHistory(id_payer,id_payee,requester_app,amount_to_pay,1);
//若余额大于等于所要求支付的金额,则扣款,将is_success置1并保存新余额和支付历史,返回true表示支付成功
//若小于,则将is_success置0,并保存支付历史,返回false表示支付失败,余额不变
if (balance >= amount_to_pay){
float newBalance = balance - amount_to_pay;
payHistory.setIs_success(1);
MoneyData.saveBalance(PayService.this,newBalance,id_payer+"");
MoneyData.savePayHistory(PayService.this,payHistory,id_payer,requester_app);
return true;
}else {
payHistory.setIs_success(0);
MoneyData.savePayHistory(PayService.this,payHistory,id_payer,requester_app);
return false;
}
}
//读取支付历史函数
@Override
public PayHistory getPayHistory(int id_payer, String requester_app) throws RemoteException{
PayHistory payHistory = MoneyData.loadPayHistory(PayService.this,id_payer,requester_app);
return payHistory;
}
};
四、迪迪打车(DiDiTaxi)
(一)需求分析
- 提供“查询支付历史”和“支付租车费用”两个功能的按钮
- 在查询支付历史功能中可以根据输入的付款方id查询并显示它最近一次的支付历史记录(包括付款方id,收款方id,收款方APP名称,付款金额以及付款状态)
- 在支付租车费用中可以根据输入的付款方id,收款方id以及付款金额调用ALiBaby提供的远程服务接口完成支付功能,并用Toast显示支付是否成功。
(二)界面绘制
- 主界面
- 查询支付历史
- 支付租车费用
(三)关键代码
- 绑定服务(PayActivity.java & QueryActivity.java)
private IPayService mPayService;
private boolean isBind = false;
//服务链接
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mPayService = IPayService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mPayService = null;
}
};
//服务绑定
Intent serviceIntent = new Intent("com.hzf.nicholas.alibaby.PayService");
Intent serviceIntent2 = createExplicitFromImplicitIntent(QueryActivity.this,serviceIntent);
bindService(serviceIntent2,mServiceConnection, Context.BIND_AUTO_CREATE);
isBind = true;
//将隐式启动转化为显示启动
public Intent createExplicitFromImplicitIntent(Context context, Intent implicitIntent) {
// Retrieve all services that can match the given intent
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0);
// Make sure only one match was found
if (resolveInfo == null || resolveInfo.size() != 1) {
return null;
}
// Get component info and create ComponentName
ResolveInfo serviceInfo = resolveInfo.get(0);
String packageName = serviceInfo.serviceInfo.packageName;
String className = serviceInfo.serviceInfo.name;
ComponentName component = new ComponentName(packageName, className);
// Create a new intent. Use the old one for extras and such reuse
Intent explicitIntent = new Intent(implicitIntent);
// Set the component to be explicit
explicitIntent.setComponent(component);
return explicitIntent;
}
- 支付租车费用
int payer = Integer.parseInt(payerET.getText().toString());
int payee = Integer.parseInt(payeeET.getText().toString());
float money = Float.parseFloat(amountET.getText().toString());
if (isBind){
try {
boolean flag = mPayService.pay(payer,payee,money,getString(R.string.app_name));
if (flag){
Toast.makeText(PayActivity.this,getString(R.string.s_activity_pay_success),Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(PayActivity.this,getString(R.string.s_activity_pay_failure),Toast.LENGTH_SHORT).show();
}
} catch (RemoteException e) {
e.printStackTrace();
}
}else {
Toast.makeText(PayActivity.this,getString(R.string.s_activity_start_service_first),Toast.LENGTH_SHORT).show();
}
- 查询支付历史
int payer = Integer.parseInt(mEditText.getText().toString());
if (isBind){
try {
PayHistory payHistory = mPayService.getPayHistory(payer,getString(R.string.app_name));
int id_payer = payHistory.getId_payer();
int id_payee = payHistory.getId_payee();
String name = payHistory.getRequester_app();
float money = payHistory.getAmount_to_pay();
int is_success = payHistory.getIs_success();
String data = getString(R.string.s_activity_pay_payer)+id_payer+'\n';
data += getString(R.string.s_activity_pay_payee)+id_payee+'\n';
data += getString(R.string.s_activity_query_get_app_name)+name+'\n';
data += getString(R.string.s_activity_pay_amount_to_pay)+money+'\n';
if (is_success == 0){
data += getString(R.string.s_activity_query_pay_state)+getString(R.string.s_activity_pay_failure);
}else {
data += getString(R.string.s_activity_query_pay_state)+getString(R.string.s_activity_pay_success);
}
mTextView.setText(data);
mTextView.setVisibility(View.VISIBLE);
} catch (RemoteException e) {
e.printStackTrace();
}
}else {
Toast.makeText(QueryActivity.this,getString(R.string.s_activity_start_service_first),Toast.LENGTH_SHORT).show();
}
五、遇到的BUG
(一)java.lang.SecurityException: MODE_WORLD_READABLE no longer supported
- SharedPreferences的三种访问模式中的MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE在高版本的Android中因为安全性无法保证已经被废弃,需要使用MODE_PRIVATE。
(二)Android AIDL 编译错误,finished with non-zero exit value 1
- 包名写错了。
- 使用的自定义数据类型还没实现Parcelable接口。
(三)ServiceIntent must be explicit
在Android5.0之后,为了确保应用的安全性,启动 Service 时,需要使用显式 Intent。解决方案是:
- A.intent.setAction(service的action名称);intent.setPackage(service包名);
- B.使用网上的方法,将隐式启动的intent转化为显式启动的intent。
(四)app:compileDebugAidl : aidl is missing
- 在创建好aidl后修改了包名,导致包名不一致。需要保持aidl的包名与对应Java文件本身一致。
(五)远程绑定服务后,无法调用相关函数,报空指针错误。
- 在绑定完服务之后不能立即调用服务中的方法,否则会出现空指针错误。
解决BUG过程中看到的一个博客,帮助我解决了一些问题。
关于安卓不能远程绑定服务的问题