一、介绍
Duration: 3:00
总览
通过构建本次的SplitBill应用,您可以更好地了解华为生态的组成部分,包括认证服务、云存储和云数据库等Serverless服务。此外您还可以了解如何使用近距离数据通信服务的Nearby Connection功能分享文件。无需使用现金,SplitBill应用能够实现用户与其他任意用户共同支付账单。
您将建立什么
在本次的Codelab中,您将建立一款SplitBill应用,并使用到华为认证服务、Network Kit、近距离数据通信服务、云数据库和云存储的接口。该应用为用户提供端到端的群收款服务。使用该应用,多位用户可以组成一个群,共同完成账单支付。
您将学到什么
-
使用认证服务登录应用(华为账号和手机号认证方式)
-
创建群。
-
添加成员到群。
-
添加账单。
-
上传用户头像至云存储和在云数据库中更新头像。
-
使用近距离通信服务分享账单文件。
二、您需要什么
Duration: 2:00
开发环境
-
一台装有Windows10操作系统的台式或笔记本电脑。
-
搭载HMC Core(APK)5.0.0.300或以上版本的华为手机。
-
通过验证的华为账号。
设备要求
一部用于测试的安卓手机或模拟器。
三、集成准备
Duration: 10:00
在集成SDK之前,您需要完成以下准备:
-
在AppGallery Connect上创建应用。
-
创建一个安卓项目。
-
生成签名证书。
-
生成签名证书指纹。
-
配置签名证书指纹。
-
添加应用包名,保存配置文件。
-
在项目级build.gradle文件中添加AppGallery Connect插件和Maven仓。
-
在Android Studio中配置签名证书。
详情请见HUWEI HMS Core集成准备。
说明:在上述准备工作前您需要注册成为一名开发者。
四、开通服务
Duration: 4:00
首先,您需要在AppGallery Connect上启用HMS Core的相关服务。启用前,请完成以下准备工作:
1、登录AppGallery Connect,点击“项目设置”中“API管理”页签,开通如下服务的API权限。
-
认证服务(华为账号认证方式)
-
云数据库
-
云存储
-
近距离通信服务
-
Network Kit
说明:以上API权限默认已开通。如未开通,请手动开通。
2、在弹出的页面设置数据处理位置。
五、集成SDK
Duration: 4:00
您需要将云数据库SDK集成到您的Android Studio项目中。
1、登录AppGallery Connect,点击“我的项目”。
2、点击您的项目,在顶部的应用下拉列表中选择应用。
3、点击“项目设置”>“常规”。在“应用”区域,下载agconnect-services.json文件。
4、将agconnect-services.json文件复制到您的项目中。
5、在Android Studio中,打开项目级build.gradle文件,前往allprojects > repositories,在buildscript的repositories部分配置Maven仓地址。
6、在buildscript的dependencies部分配置AppGallery Connect插件。
buildscript {
dependencies {
classpath 'com.huawei.agconnect:agcp:<version>'
}
}
7、在应用级build.gradle文件中添加AppGallery Connect插件。
apply plugin: 'com.huawei.agconnect'
8、(可选)在Application类中的onCreate方法里添加初始化代码。
if (AGConnectInstance.getInstance() == null) {
AGConnectInstance.initialize(getApplicationContext());
}
9、在应用级build.gradle文件里的dependencies部分添加所需依赖路径。
implementation 'com.huawei.agconnect:agconnect-auth:<version>'
implementation 'com.huawei.hms:hwid:<version>'
implementation 'com.huawei.hms:nearby: <version>'
implementation"com.huawei.agconnect:agconnect-storage:<version>"
implementation'com.huawei.agconnect:agconnect-cloud-database:<version>'
六、设计UI
Duration: 5:00
创建以下界面:登录、建群、群列表、添加账单、账单和收支标签页、账单详情、账单分享详情和收到的账单文件列表。
用户登录界面
群列表和建群界面
七、前提准备
Duration: 5:00
认证服务
认证服务SDK能够让您快速便捷地在您的应用上实现用户注册和登录功能。
-
登录AppGallery Connect,点击“我的项目”。
-
点击您的项目。
-
选择“构建”,单击“认证服务”。若您首次使用认证服务,点击“立即开通”。
-
单击“认证方式”,在“操作”栏中启用华为账号认证方式。
Cloud DB
使用云数据库服务,您需要启用云数据库,创建存储区和云数据库对象所需字段。
1、登录AppGallery Connect,点击“我的项目”。
2、点击您的项目。
3、选择“构建”,点击“云数据库”。若您首次使用云数据库,点击“立即开通”。
4、在弹出页面设置数据处理位置。
5、单击“新增”,进入创建对象类型页面。
6、设置对象类型,单击“下一步”。
7、单击“+新增字段”添加字段,单击“下一步”。
8、添加所需索引。
9、设置各角色权限。
10、单击“确定”。返回对象类型列表,查看已创建的对象类型。
11、单击“导出”。
12、选择文件格式,此处选择文件类型为Java,文件格式为Android。输入包名,点击“确定”。对象类型文件将被作为zip文件下载
13、提取zip中的文件至项目的model包里。
1)点击“存储区”页签。
2)单击“新增”,进入创建存储区页面。
云存储
使用云存储服务,您需要启用云存储,并在开发前完成下述操作。
1、启用云存储后,创建存储实例,单击“下一步”。
2、创建安全策略,控制是否允许未经认证的用户访问存储。单击“完成”。
3、完成上述操作后,您即可使用云存储服务。
八、实现功能
Duration: 15:00
完成准备工作后,集成认证服务、云数据库、云存储、Network Kit和近距离通信服务到您的应用中。
在已设计好的登录页面添加如下代码,实现华为账号登录按钮。
activityAuthBinding.tvGetcode.setOnClickListener(this);
activityAuthBinding.btnHuaweiId.setOnClickListener(this);
HuaweiIdAuthParamsHelper huaweiIdAuthParamsHelper = new HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM);
scopeList = new ArrayList<>();
scopeList.add(new Scope(HwIDConstant.SCOPE.ACCOUNT_BASEPROFILE));
scopeList.add(new Scope(HwIDConstant.SCOPE.SCOPE_ACCOUNT_EMAIL));
scopeList.add(new Scope(HwIDConstant.SCOPE.SCOPE_MOBILE_NUMBER));
scopeList.add(new Scope(HwIDConstant.SCOPE.SCOPE_ACCOUNT_PROFILE));
huaweiIdAuthParamsHelper.setScopeList(scopeList);
HuaweiIdAuthParams authParams = huaweiIdAuthParamsHelper.setAccessToken().setMobileNumber().createParams();
service = HuaweiIdAuthManager.getService(this, authParams);
Button Click:
loginViewModel.signInWithHuaweiId(AuthActivity.this, service).observe(AuthActivity.this, new Observer<SignInResult>() {
@Override
public void onChanged(SignInResult user) {
activityAuthBinding.authProgressBar.setVisibility(View.VISIBLE);
progress();
loginSuccess (user);
}
});
实现OnActivityResult。
huaweiSignIn.launch(service.getSignInIntent());
ActivityResultLauncher<Intent> huaweiSignIn = AuthActivity.authActivity.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
Task<AuthAccount> authAccountTask = AccountAuthManager.parseAuthResultFromIntent(result.getData());
if (authAccountTask.isSuccessful()) {
AuthAccount authAccount = authAccountTask.getResult();
AGConnectAuthCredential credential = HwIdAuthProvider.credentialWithToken(authAccount.getAccessToken());
AGConnectAuth.getInstance().signIn(credential).addOnSuccessListener(new OnSuccessListener<SignInResult>() {
@Override
public void onSuccess(SignInResult signInResult) {
// onSuccess
Common.showToast("log in", AuthActivity.authActivity);
authenticatedUserMutableLiveData.setValue(signInResult);
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Common.showToast("fail", AuthActivity.authActivity);
}
});
}
}
}
});
创建wrapper类,用于初始化云数据库存储区,插入新用户和验证已知用户等数据库操作。
public class CloudDBZoneWrapper { private static final String TAG = "CloudDBZoneWrapper"; private static final String DB_NAME = "SplitBillSampleApp"; private AGConnectCloudDB mCloudDB; private CloudDBZone mCloudDBZone; private CloudDBZoneConfig mConfig; /**
SplitBillApplication
* Get instance of Cloud DB zone wrapper to initiate cloud DB * * @return mCloudDBZoneWrapper */ public CloudDBZoneWrapper getCloudDBZoneWrapper() { if (mCloudDBZoneWrapper != null) { return mCloudDBZoneWrapper; } mCloudDBZoneWrapper = new CloudDBZoneWrapper(); return mCloudDBZoneWrapper; }
/** * Get CloudDB task to open AGConnectCloudDB */ public Task<CloudDBZone> openCloudDBZoneV2() { mConfig = new CloudDBZoneConfig(DB_NAME, CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE, CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC); mConfig.setPersistenceEnabled(true); Task<CloudDBZone> openDBZoneTask = mCloudDB.openCloudDBZone2(mConfig, true); return openDBZoneTask; }
/**
* 调用AGConnectCloudDB.closeCloudDBZone
*/
public void closeCloudDBZone() {
try {
mRegister.remove();
mCloudDB.closeCloudDBZone(mCloudDBZone);
} catch (AGConnectCloudDBException e) {
Log.w(TAG, "CloudDBZone: " + e.getMessage());
}
}
/**
* 云数据库中插入好友数据
*/
public MutableLiveData<Boolean> upsertExpenseData(Friends friends) {
if (mCloudDBZone == null) {
Log.w(TAG, "CloudDBZone is null, try re-open it");
}
Task<Integer> upsertTask = mCloudDBZone.executeUpsert(friends);
upsertTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
@Override
public void onSuccess(Integer cloudDBZoneResult) {
friendsUpdateSuccess.postValue(true);
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
friendsUpdateSuccess.postValue(false);
}
});
return friendsUpdateSuccess;
}
/**
* 云数据库中获取好友列表
*/
public MutableLiveData<Integer> getFriendsIdLiveData() {
CloudDBZoneQuery<Friends> snapshotQuery = CloudDBZoneQuery.where(Friends.class);
Task<Long> countQueryTask = mCloudDBZone.executeCountQuery(snapshotQuery, FriendsEditFields.CONTACT_ID,
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY);
countQueryTask.addOnSuccessListener(new OnSuccessListener<Long>() {
@Override
public void onSuccess(Long aLong) {
Log.i(TAG, "The total number of groups is " + aLong);
friendsIdLiveData.postValue((int) (aLong + 1));
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.w(TAG, "Count query is failed: " + Log.getStackTraceString(e));
}
});
return friendsIdLiveData;
}
/**
* 云数据库中获取好友列表
* 从数据库中添加,获取群或账单数据使用相似方法。
*/
private void insertGroupDataInDB(Group group){
groupViewModel.upsertGroupData(group).observe(getActivity(), new Observer<Boolean>() {
@Override
public void onChanged(Boolean aBoolean) {
if (aBoolean.equals(true)) {
progressStats = true;
Toast.makeText(getContext(), getString(R.string.add_group_success) + " " + group.getName(), Toast.LENGTH_LONG).show();
Navigation.findNavController(fragmentCreateGroupBinding.getRoot()).navigate(R.id.navigation_activity);
} else {
Toast.makeText(getContext(), getString(R.string.add_group_failed), Toast.LENGTH_LONG).show();
progressStats = false;
}
}
});
}
添加账单数据:
private void addExpenseData() {
Expense expense = new Expense();
String strExpenseName = expenseName.getText().toString();
String strPaidBy = spinner.getSelectedItem().toString();
String strExpenseAmount = expenseDesc.getText().toString();
expense.setId(0);
expense.setName(strExpenseName);
expense.setAmount(Float.parseFloat((strExpenseAmount) + ".00"));
expense.setPaid_user_id(0);
expense.setStatus(1);
expenseViewModel.upsertExpenseData(expense).observe(getActivity(), new Observer<Boolean>() {
@Override
public void onChanged(Boolean aBoolean) {
if (aBoolean.equals(true)) {
Toast.makeText(getContext(), getString(R.string.add_expense_success) + " " + strExpenseName, Toast.LENGTH_LONG).show();
//TODO : back key
} else {
Toast.makeText(getContext(), getString(R.string.add_expense_failed), Toast.LENGTH_LONG).show();
}
}
});
}
数据插入完成后,接着使用近距离通信服务提供的类。
NearbyAgent:
/*
* 华为技术有限公司版权所有 保留一切权利,
授权于Apache License 2.0版本(以下简称“许可证”)。
使用此许可证时,须遵循其规定。
您可以通过以下途径获取许可证的副本:
http://www.apache.org/licenses/LICENSE-2.0
除非得到适用法律或书面同意,
许可证许可范围内所提供的软件均按“现状”提供,
而不做任何明示或暗示的保证。
关于许可证中特定语言的权限和限制,
请参见许可证。
*/
package com.huawei.codelabs.splitbill.ui.main.helper;
import android.Manifest;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.pdf.PdfDocument;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import androidx.core.app.ActivityCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.huawei.codelabs.splitbill.R;
import com.huawei.codelabs.splitbill.databinding.FragmentAccountBinding;
import com.huawei.codelabs.splitbill.databinding.FragmentFileDetailsBinding;
import com.huawei.codelabs.splitbill.databinding.FragmentSendExpenseDetailsBinding;
import com.huawei.codelabs.splitbill.ui.main.activities.MainActivity;
import com.huawei.codelabs.splitbill.ui.main.adapter.DeviceAdapter;
import com.huawei.codelabs.splitbill.ui.main.adapter.FilesAdapter;
import com.huawei.codelabs.splitbill.ui.main.adapter.FriendsListAdapter;
import com.huawei.codelabs.splitbill.ui.main.models.Device;
import com.huawei.codelabs.splitbill.ui.main.models.Files;
import com.huawei.hms.hmsscankit.ScanUtil;
import com.huawei.hms.hmsscankit.WriterException;
import com.huawei.hms.ml.scan.HmsBuildBitmapOption;
import com.huawei.hms.ml.scan.HmsScan;
import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions;
import com.huawei.hms.nearby.Nearby;
import com.huawei.hms.nearby.StatusCode;
import com.huawei.hms.nearby.discovery.BroadcastOption;
import com.huawei.hms.nearby.discovery.ConnectCallback;
import com.huawei.hms.nearby.discovery.ConnectInfo;
import com.huawei.hms.nearby.discovery.ConnectResult;
import com.huawei.hms.nearby.discovery.DiscoveryEngine;
import com.huawei.hms.nearby.discovery.Policy;
import com.huawei.hms.nearby.discovery.ScanEndpointCallback;
import com.huawei.hms.nearby.discovery.ScanEndpointInfo;
import com.huawei.hms.nearby.discovery.ScanOption;
import com.huawei.hms.nearby.transfer.Data;
import com.huawei.hms.nearby.transfer.DataCallback;
import com.huawei.hms.nearby.transfer.TransferEngine;
import com.huawei.hms.nearby.transfer.TransferStateUpdate;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8;
public class NearbyAgent {
public static final int REQUEST_CODE_SCAN_ONE = 0X01;
private static final String[] REQUIRED_PERMISSIONS =
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA};
private static final int REQUEST_CODE_REQUIRED_PERMISSIONS = 1;
private final String TAG = "Nearby_Agent";
private final String mFileServiceId = "NearbyAgentFileService";
private final String mEndpointName = android.os.Build.DEVICE;
int lineYAxis = 350;
FragmentAccountBinding fragmentAccountBinding;
FragmentSendExpenseDetailsBinding fragmentSendExpenseDetailsBinding;
FragmentFileDetailsBinding fragmentFileDetailsBinding;
ArrayList<Files> filesArrayList;
FilesAdapter groupAdapter;
private MainActivity mContext = null;
private TransferEngine mTransferEngine = null;
private DiscoveryEngine mDiscoveryEngine = null;
private List<File> mFiles = new ArrayList<>();
private String mRemoteEndpointId;
private String mRemoteEndpointName;
private String mScanInfo;
private final ScanEndpointCallback mDiscCb =
new ScanEndpointCallback() {
@Override
public void onFound(String endpointId, ScanEndpointInfo discoveryEndpointInfo) {
if (discoveryEndpointInfo.getName().equals(mScanInfo)) {
Log.d(TAG, "Found endpoint:" + discoveryEndpointInfo.getName() + ". Connecting.");
mDiscoveryEngine.requestConnect(mEndpointName, endpointId, mConnCbRcver);
}
}
@Override
public void onLost(String endpointId) {
Log.d(TAG, "Lost endpoint.");
}
};
private String mRcvedFilename = null;
private Bitmap mResultImage;
private String mFileName;
private long mStartTime = 0;
private float mSpeed = 60;
private String mSpeedStr = "60";
private boolean isTransfer = false;
private final DataCallback mDataCbSender =
new DataCallback() {
@Override
public void onReceived(String endpointId, Data data) {
if (data.getType() == Data.Type.BYTES) {
String msg = new String(data.asBytes(), UTF_8);
if (msg.equals("Receive Success")) {
Log.d(TAG, "Received ACK. Send next.");
sendOneFile();
}
}
}
@Override
public void onTransferUpdate(String string, TransferStateUpdate update) {
if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_SUCCESS) {
} else if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_IN_PROGRESS) {
showProgressSpeedSender(update);
} else if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_FAILURE) {
Log.d(TAG, "Transfer failed.");
} else {
Log.d(TAG, "Transfer cancelled.");
}
}
};
private final ConnectCallback mConnCbRcver =
new ConnectCallback() {
@Override
public void onEstablish(String endpointId, ConnectInfo connectionInfo) {
Log.d(TAG, "Accept connection.");
mRemoteEndpointName = connectionInfo.getEndpointName();
mRemoteEndpointId = endpointId;
mDiscoveryEngine.acceptConnect(endpointId, mDataCbRcver);
}
@Override
public void onResult(String endpointId, ConnectResult result) {
if (result.getStatus().getStatusCode() == StatusCode.STATUS_SUCCESS) {
Log.d(TAG, "Connection Established. Stop Discovery.");
mDiscoveryEngine.stopBroadcasting();
mDiscoveryEngine.stopScan();
fragmentFileDetailsBinding.tvMainDesc.setText("Connected.");
}
}
@Override
public void onDisconnected(String endpointId) {
Log.d(TAG, "Disconnected.");
if (isTransfer == true) {
fragmentSendExpenseDetailsBinding.tvMainDesc.setVisibility(View.GONE);
fragmentSendExpenseDetailsBinding.tvMainDesc.setText("Connection lost.");
}
}
};
private Data incomingFile = null;
private final DataCallback mDataCbRcver =
new DataCallback() {
@Override
public void onReceived(String endpointId, Data data) {
if (data.getType() == Data.Type.BYTES) {
String msg = new String(data.asBytes(), UTF_8);
mRcvedFilename = msg;
Log.d(TAG, "received filename: " + mRcvedFilename);
isTransfer = true;
fragmentFileDetailsBinding.tvMainDesc.setText(new StringBuilder("Receiving file ").append(mRcvedFilename).append(" from ").append(mRemoteEndpointName + ".").toString());
fragmentFileDetailsBinding.pbMainDownload.setVisibility(View.VISIBLE);
} else if (data.getType() == Data.Type.FILE) {
incomingFile = data;
} else {
Log.d(TAG, "received stream. ");
}
}
public File getLastModified(String directoryFilePath)
{
File directory = new File(directoryFilePath);
File[] files = directory.listFiles(File::isFile);
long lastModifiedTime = Long.MIN_VALUE;
File chosenFile = null;
if (files != null)
{
for (File file : files)
{
if (file.lastModified() > lastModifiedTime)
{
chosenFile = file;
lastModifiedTime = file.lastModified();
}
}
}
return chosenFile;
}
@Override
public void onTransferUpdate(String string, TransferStateUpdate update) {
if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_SUCCESS) {
} else if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_IN_PROGRESS) {
showProgressSpeedReceiver(update);
if (update.getBytesTransferred() == update.getTotalBytes()) {
Log.d(TAG, "File transfer done. Rename File.");
renameFile();
Log.d(TAG, "Send Ack.");
fragmentFileDetailsBinding.tvMainDesc.setText(new StringBuilder("Transfer success. Speed: ").append(mSpeedStr).append("MB/s. \nView the File at /Sdcard/Download/Nearby"));
mTransferEngine.sendData(mRemoteEndpointId, Data.fromBytes("Receive Success".getBytes(StandardCharsets.UTF_8)));
isTransfer = false;
Files files = new Files();
File file= getLastModified(Environment.getExternalStorageDirectory().getPath() + Constants.DOWNLOAD_PATH);
files.setFileName(file.getName());
files.setFilePath(new File(file.getAbsolutePath()));
filesArrayList.add(files);
groupAdapter.notifyDataSetChanged();
}
} else if (update.getStatus() == TransferStateUpdate.Status.TRANSFER_STATE_FAILURE) {
Log.d(TAG, "Transfer failed.");
} else {
Log.d(TAG, "Transfer cancelled.");
}
}
};
private ArrayList<Device> deviceList;
private final ConnectCallback mConnCbSender =
new ConnectCallback() {
@Override
public void onEstablish(String endpointId, ConnectInfo connectionInfo) {
Log.d(TAG, "Accept connection.");
mDiscoveryEngine.acceptConnect(endpointId, mDataCbSender);
fragmentSendExpenseDetailsBinding.rcDevice.setHasFixedSize(true);
fragmentSendExpenseDetailsBinding.rcDevice.setLayoutManager(new LinearLayoutManager(mContext));
Device device = new Device();
device.setDeviceName(connectionInfo.getEndpointName());
deviceList.add(device);
DeviceAdapter deviceAdapter = new DeviceAdapter(deviceList);
fragmentSendExpenseDetailsBinding.rcDevice.setAdapter(deviceAdapter);
deviceAdapter.notifyDataSetChanged();
mRemoteEndpointName = connectionInfo.getEndpointName();
mRemoteEndpointId = endpointId;
}
@Override
public void onResult(String endpointId, ConnectResult result) {
if (result.getStatus().getStatusCode() == StatusCode.STATUS_SUCCESS) {
Log.d(TAG, "Connection Established. Stop discovery. Start to send file.");
mDiscoveryEngine.stopScan();
mDiscoveryEngine.stopBroadcasting();
sendOneFile();
fragmentSendExpenseDetailsBinding.barcodeImage.setVisibility(View.GONE);
fragmentSendExpenseDetailsBinding.tvMainDesc.setText(new StringBuilder("MB/s. \nView the File at /Sdcard/Download/Nearby").append(mFileName).append(" to ").append(mRemoteEndpointName).append("."));
fragmentSendExpenseDetailsBinding.pbMainDownload.setVisibility(View.GONE);
}
}
@Override
public void onDisconnected(String endpointId) {
Log.d(TAG, "Disconnected.");
if (isTransfer == true) {
fragmentSendExpenseDetailsBinding.pbMainDownload.setVisibility(View.GONE);
fragmentSendExpenseDetailsBinding.tvMainDesc.setText("Connection lost.");
}
}
};
public NearbyAgent(MainActivity context) {
mContext = context;
mDiscoveryEngine = Nearby.getDiscoveryEngine(context);
deviceList = new ArrayList<>();
mTransferEngine = Nearby.getTransferEngine(context);
if (context instanceof MainActivity) {
ActivityCompat.requestPermissions(context, REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
}
}
public NearbyAgent(MainActivity context, FragmentAccountBinding fragmentAccountBinding) {
this.fragmentAccountBinding = fragmentAccountBinding;
this.mContext = context;
mDiscoveryEngine = Nearby.getDiscoveryEngine(context);
mTransferEngine = Nearby.getTransferEngine(context);
if (context instanceof MainActivity) {
ActivityCompat.requestPermissions(context, REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
}
}
public static String getFileRealNameFromUri(Context context, Uri fileUri) {
if (context == null || fileUri == null) {
return Constants.UnknownFile;
}
DocumentFile documentFile = DocumentFile.fromSingleUri(context, fileUri);
if (documentFile == null) {
return Constants.UnknownFile;
}
return documentFile.getName();
}
private void showProgressSpeedSender(TransferStateUpdate update) {
long transferredBytes = update.getBytesTransferred();
long totalBytes = update.getTotalBytes();
long curTime = System.currentTimeMillis();
Log.d(TAG, "Transfer in progress. Transferred Bytes: "
+ transferredBytes + " Total Bytes: " + totalBytes);
fragmentSendExpenseDetailsBinding.pbMainDownload.setProgress((int) (transferredBytes * 100 / totalBytes));
if (mStartTime == 0) {
mStartTime = curTime;
}
if (curTime != mStartTime) {
mSpeed = ((float) transferredBytes) / ((float) (curTime - mStartTime)) / 1000;
java.text.DecimalFormat myformat = new java.text.DecimalFormat("0.00");
mSpeedStr = myformat.format(mSpeed);
fragmentSendExpenseDetailsBinding.tvMainDesc.setText(new StringBuilder("Transfer in Progress. Speed: ").append(mSpeedStr).append("MB/s."));
}
if (transferredBytes == totalBytes) {
mStartTime = 0;
}
}
public void loadScanCode(Bitmap mResultImage) {
fragmentSendExpenseDetailsBinding.barcodeImage.setVisibility(View.VISIBLE);
fragmentSendExpenseDetailsBinding.barcodeImage.setImageBitmap(mResultImage);
}
public File createPdf(List<FriendsListAdapter.FriendsUI> friendsUIList, Bitmap scaledImageBitmap, FragmentActivity activity) {
//创建一个新的document。
PdfDocument document = new PdfDocument();
//创建页面描述。
PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(Constants.PAGEWIDTH, Constants.PAGEHEIGHT, 1).create();
//开启一个页面。
PdfDocument.Page page = document.startPage(pageInfo);
Canvas canvas = page.getCanvas();
Paint paint = new Paint();
canvas.drawBitmap(scaledImageBitmap, 0, 0, paint);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(50);
paint.setColor(activity.getResources().getColor(android.R.color.black));
paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL));
canvas.drawText("Invoice" + System.currentTimeMillis(), Constants.PAGEWIDTH / 2, 260, paint);
paint.setStrokeWidth(2f);
canvas.drawLine(Constants.PAGEWIDTH / 2, 300, 550, 300, paint);
canvas.drawText("Amount" + " " + "Participants", Constants.PAGEWIDTH / 2, 400, paint);
lineYAxis = 450;
for (FriendsListAdapter.FriendsUI friendsUI : friendsUIList) {
canvas.drawText(friendsUI.getFriendsName() + " " + friendsUI.getAmount() + "", Constants.PAGEWIDTH / 2, Constants.LINEYAXIS + 50, paint);
lineYAxis = lineYAxis + 50;
}
document.finishPage(page);
pageInfo = new PdfDocument.PageInfo.Builder(Constants.PAGEWIDTH, Constants.PAGEHEIGHT, 2).create();
page = document.startPage(pageInfo);
document.finishPage(page);
//写入document内容。
String directory_path = Environment.getExternalStorageDirectory().getPath() + Constants.CREATEINVOICE;
File file = new File(directory_path);
if (!file.exists()) {
file.mkdirs();
}
String targetPdf = new StringBuilder().append(directory_path).append("invoice").append(System.currentTimeMillis()).append(".pdf").toString();
File filePath = new File(targetPdf);
try {
document.writeTo(new FileOutputStream(filePath));
} catch (IOException e) {
Log.e("main", "error " + e.toString());
}
//关闭document。
document.close();
return filePath;
}
public void sendFile(File file, FragmentSendExpenseDetailsBinding fragmentSendExpenseDetailsBinding) {
this.fragmentSendExpenseDetailsBinding = fragmentSendExpenseDetailsBinding;
init();
mFiles.add(file);
sendFilesInner();
}
public void sendFiles(List<File> files) {
init();
mFiles = files;
sendFilesInner();
}
public void sendFolder(File folder) {
init();
File[] subFile = folder.listFiles();
for (int i = 0; i < subFile.length; i++) {
if (!subFile[i].isDirectory()) {
mFiles.add(subFile[i]);
Log.d(TAG, "Travel folder: " + subFile[i].getName());
}
}
sendFilesInner();
}
private void sendFilesInner() {
/* 生成bitmap */
try {
//生成barcode。
HmsBuildBitmapOption options = new HmsBuildBitmapOption.Creator().setBitmapMargin(1).setBitmapColor(Color.BLACK).setBitmapBackgroundColor(Color.WHITE).create();
mResultImage = ScanUtil.buildBitmap(mEndpointName, HmsScan.QRCODE_SCAN_TYPE, Constants.BARCODE_SIZE, Constants.BARCODE_SIZE, options);
loadScanCode(mResultImage);
} catch (WriterException e) {
Log.e(TAG, e.toString());
}
/* 开始广播 */
BroadcastOption.Builder advBuilder = new BroadcastOption.Builder();
advBuilder.setPolicy(Policy.POLICY_P2P);
mDiscoveryEngine.startBroadcasting(mEndpointName, mFileServiceId, mConnCbSender, advBuilder.build());
Log.d(TAG, "Start Broadcasting.");
}
public void receiveFile(FragmentFileDetailsBinding fragmentFileDetailsBinding, ArrayList<Files> filesArrayList, FilesAdapter groupAdapter) {
/* 扫描bitmap */
this.fragmentFileDetailsBinding = fragmentFileDetailsBinding;
this.filesArrayList=filesArrayList;
this.groupAdapter=groupAdapter;
Log.d("TAG", "start");
init();
HmsScanAnalyzerOptions options = new HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE, HmsScan.DATAMATRIX_SCAN_TYPE).create();
ScanUtil.startScan(mContext, REQUEST_CODE_SCAN_ONE, options);
Log.d("TAG", "Sent");
}
public void onScanResult(Intent data) {
if (data == null) {
Log.d("TAG", "fail");
fragmentFileDetailsBinding.tvMainDesc.setText("Scan Failed.");
return;
}
/* 保存设备名称*/
HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT);
mScanInfo = obj.getOriginalValue();
/* 开始扫描*/
ScanOption.Builder scanBuilder = new ScanOption.Builder();
scanBuilder.setPolicy(Policy.POLICY_P2P);
mDiscoveryEngine.startScan(mFileServiceId, mDiscCb, scanBuilder.build());
Log.d(TAG, "Start Scan.");
fragmentFileDetailsBinding.tvMainDesc.setText(new StringBuilder().append("Connecting to ").append(mScanInfo).append("..."));
}
private void sendOneFile() {
Data filenameMsg = null;
Data filePayload = null;
isTransfer = true;
Log.d(TAG, "Left " + mFiles.size() + " Files to send.");
if (mFiles.isEmpty()) {
Log.d(TAG, "All Files Done. Disconnect");
fragmentSendExpenseDetailsBinding.tvMainDesc.setText(R.string.all_files_sent);
fragmentSendExpenseDetailsBinding.pbMainDownload.setVisibility(View.GONE);
fragmentSendExpenseDetailsBinding.tvHeading.setVisibility(View.GONE);
mDiscoveryEngine.disconnectAll();
isTransfer = false;
return;
}
try {
mFileName = mFiles.get(0).getName();
filePayload = Data.fromFile(mFiles.get(0));
mFiles.remove(0);
} catch (FileNotFoundException e) {
Log.e(TAG, "File not found", e);
return;
}
filenameMsg = Data.fromBytes(mFileName.getBytes(StandardCharsets.UTF_8));
Log.d(TAG, "Send filename: " + mFileName);
mTransferEngine.sendData(mRemoteEndpointId, filenameMsg);
Log.d(TAG, "Send Payload.");
mTransferEngine.sendData(mRemoteEndpointId, filePayload);
}
private void renameFile() {
if (incomingFile == null) {
Log.d(TAG, "incomingFile is null");
return;
}
File rawFile = incomingFile.asFile().asJavaFile();
Log.d(TAG, "raw file: " + rawFile.getAbsolutePath());
File targetFileName = new File(rawFile.getParentFile(), mRcvedFilename);
Log.d(TAG, "rename to : " + targetFileName.getAbsolutePath());
Uri uri = incomingFile.asFile().asUri();
if (uri == null) {
boolean result = rawFile.renameTo(targetFileName);
if (!result) {
Log.e(TAG, "rename failed");
} else {
Log.e(TAG, "rename Succeeded ");
}
} else {
try {
openStream(uri, targetFileName);
} catch (IOException e) {
Log.e(TAG, e.toString());
} finally {
delFile(uri, rawFile);
}
}
}
private void openStream(Uri uri, File targetFileName) throws IOException {
InputStream in = mContext.getContentResolver().openInputStream(uri);
Log.e(TAG, "open input stream successfuly");
try {
copyStream(in, new FileOutputStream(targetFileName));
Log.e(TAG, "copyStream successfuly");
} catch (IOException e) {
Log.e(TAG, e.toString());
} finally {
in.close();
}
}
private void copyStream(InputStream in, OutputStream out) throws IOException {
try {
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
out.flush();
} finally {
out.close();
}
}
private void delFile(Uri uri, File payloadfile) {
//删除源文件。
mContext.getContentResolver().delete(uri, null, null);
if (!payloadfile.exists()) {
Log.e(TAG, "delete original file by uri successfully");
} else {
Log.e(TAG, "delete original file by uri failed and try to delete it by File delete");
payloadfile.delete();
if (payloadfile.exists()) {
Log.e(TAG, "fail to delete original file");
} else {
Log.e(TAG, "delete original file successfully");
}
}
}
private void showProgressSpeedReceiver(TransferStateUpdate update) {
long transferredBytes = update.getBytesTransferred();
long totalBytes = update.getTotalBytes();
long curTime = System.currentTimeMillis();
Log.d(TAG, "Transfer in progress. Transferred Bytes: "
+ transferredBytes + " Total Bytes: " + totalBytes);
fragmentFileDetailsBinding.pbMainDownload.setProgress((int) (transferredBytes * 100 / totalBytes));
if (mStartTime == 0) {
mStartTime = curTime;
}
if (curTime != mStartTime) {
mSpeed = ((float) transferredBytes) / ((float) (curTime - mStartTime)) / 1000;
java.text.DecimalFormat myformat = new java.text.DecimalFormat("0.00");
mSpeedStr = myformat.format(mSpeed);
fragmentFileDetailsBinding.tvMainDesc.setText(new StringBuilder().append("Transfer in Progress. Speed: ").append(mSpeedStr).append("MB/s."));
}
if (transferredBytes == totalBytes) {
mStartTime = 0;
}
}
private void init() {
if (fragmentSendExpenseDetailsBinding != null) {
fragmentSendExpenseDetailsBinding.pbMainDownload.setProgress(0);
fragmentSendExpenseDetailsBinding.pbMainDownload.setVisibility(View.GONE);
fragmentSendExpenseDetailsBinding.tvMainDesc.setText("");
fragmentSendExpenseDetailsBinding.barcodeImage.setVisibility(View.GONE);
}
mDiscoveryEngine.disconnectAll();
mDiscoveryEngine.stopScan();
mDiscoveryEngine.stopBroadcasting();
mFiles.clear();
}
}
初始化用于MainActivity的NearByAgent类:
nearbyAgent = new NearbyAgent(this);
在SendExpenseDetailsFragment中发送文件码:
((MainActivity) getActivity()).nearbyAgent.sendFile(new File(getArguments().getString("mValues")), fragmentSendExpenseDetailsBinding);
在FileDetailsFragment中接收文件:
((MainActivity) getActivity()).nearbyAgent.receiveFile(fragmentFileDetailsBinding, filesArrayList, groupAdapter);
在MainActivity的onActivityResult中获取结果。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case NearbyAgent.REQUEST_CODE_SCAN_ONE:
Log.d("data:", "1");
nearbyAgent.onScanResult(data);
break;
default:
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
使用近距离数据通信服务下载数据:
/**
AccountRepository类
* 初始化Network Kit
*/
@Override
protected void initManager() {
//下载manager。
downloadManager = new DownloadManager.Builder("downloadManager")
.build(context);
callback = new FileRequestCallback() {
@Override
public GetRequest onStart(GetRequest request) {
return request;
}
@Override
public void onProgress(GetRequest request, Progress progress) {
Log.i(TAG, "onProgress:" + progress);
}
@Override
public void onSuccess(Response<GetRequest, File, Closeable> response) {
String filePath = "";
if (response.getContent() != null) {
filePath = response.getContent().getAbsolutePath();
}
Log.i(TAG, "onSuccess" + " for " + filePath);
}
@Override
public void onException(GetRequest getRequest, NetworkException e, Response<GetRequest, File, Closeable> response) {
if (e instanceof Exception) {
String errorMsg = "download exception for paused or canceled";
Log.w(TAG, errorMsg);
} else {
String errorMsg = "download exception for request:" + getRequest.getId() +
"\n\ndetail : " + e.getMessage();
if (e.getCause() != null) {
errorMsg += " , cause : " +
e.getCause().getMessage();
}
Log.e(TAG, errorMsg);
}
}
};
}
@Override
public void download() {
imageDownload(context);
}
private void imageDownload(Context context) {
if (downloadManager == null) {
Log.e(TAG, "can not download without init");
return;
}
String downloadFilePath = context.getObbDir().getPath() + File.separator + "acc111.jpg";
getRequest = DownloadManager.newGetRequestBuilder()
.filePath(downloadFilePath)
.url(Common.getUrlRequest())
.build();
Result result = downloadManager.start(getRequest, callback);
checkResult(result);
}
九、打包和测试
1、启动Android Studio,点击运行按钮,在手机或模拟器上运行您的应用。点击登录按钮登录您的应用。
2、登录成功后,展示群界面。点击“+“图标新建群。
3、群在创建完成后将被插入到云数据库中,用户进入主界面,显示最新群列表。
4、在列表中点击一个群,打开群详情界面。
5、点击任意一条列表中的账单或收支数据,进入详情界面。
6、点击分享按钮,将账单详情以文件形式发送给附近的好友。该操作使用近距离数据通信服务,无需使用到手机流量和wifi。
7、查看收到的账单文件。
8、点击接收按钮接收文件。
十、恭喜您
祝贺您,您已成功构建一款SplitBill应用并学会了:
-
在AppGallery Connect中配置云数据库和云存储。
-
在Android Studio中集成多个HMS Core服务并构建一款SplitBill应用。
十一、参考
参考如下文档获取更多信息:
-
Auth Service
-
Cloud DB
-
Cloud Storage
-
Network Kit
-
Nearby Service
点击此处下载源码。
声明:本codelab实现多个HMS Core服务在单个项目中的集成,供您参考。您需要验证确保相关开源代码的安全合法合规。
欲了解更多更全技术文章,欢迎访问https://developer.huawei.com/consumer/cn/forum/?ha_source=zzh