1. 介绍
简介
游戏存档是指把玩家的游戏进度存储至华为云空间中,即使用户之前的设备丢失、损毁或换了新设备,只要用户使用相同的华为帐号登录,就可以从华为云空间取得之前的游戏进度继续游戏。
一个存档主要由2部分组成:
- 存档文件:您可以将游戏数据自行写入需要存档的文件,在获取存档文件后也由您自行解析,存档文件大小限制为3MB。
- 存档元数据:存档元数据是指存档本身的属性,您可以向用户展示存档属性,比如存档名称、上次修改时间等,用户可以根据属性信息选择哪个存档继续游戏。存档的主要属性如下表所示。
属性 | 说明 |
存档ID | 存档在该游戏中的唯一标识,在提交存档文件后由华为游戏服务器生成。 |
存档名称 | 存档保存的文件名,在提交存档文件后由华为游戏服务器生成。 |
存档描述 | 存档用于展示给玩家的描述信息,在提交存档文件时由您自行定义,最多1000个字符。 |
上次修改时间 | 服务器生成的时间戳(以毫秒为单位),表示上次更新保存的游戏的时间。 |
游戏时长 | 您在提交存档文件时提供的玩家在该存档上游戏总时间(以毫秒为单位)。 |
游戏进度 | 您在提交存档文件时提供的玩家在该存档上的游戏进度,由您自行定义。比如第几关等,用整数进行识别。 |
封面图片 | 您在提交存档文件时提供的存档封面图像,一般来说为游戏存档时的截图,若未提供,则为系统默认图片,最大200KB,支持JPG或PNG格式,长宽比为16:9。 |
您将建立什么
在本次codelab中,您将建立一个具备简单游戏交互功能的Android应用程序,您的应用程序将包含以下功能:
- 游戏初始化和登录
- 创建游戏存档和展示存档列表
- 点击存档列表,打开指定的存档,查看详细的存档信息
您将会学到什么
- 如何在AppGallery Connect网站上创建应用。
- 如何集成华为HMS Core游戏服务。
- 如何在您的应用中实现游戏初始化、登录、创建游戏存档、展示游戏存档列表和打开游戏存档等功能。
2. 您需要什么
硬件要求
- 开发计算机(台式机或笔记本电脑)。
- EMUI10.0及以上版本的华为设备或者三方安卓设备(带USB线),用于真机调试。
软件要求
- Android Studio 3.6.1及以上
- JDK 1.8 及以上
- miniSdkVersion 19及以上
- targetSdkVersion 31(推荐)
- compileSdkVersion 31 (推荐)
- Gradle 5.4.1及以上
- gradle插件3.5.4以上
需要的知识点
- Java基础语法
- Android基础开发能力
- 参考帐号注册认证注册成为开发者
3. 能力接入准备
集成HUAWEI HMS Core能力,需要完成以下准备工作:
- 在AppGallery Connect创建项目,在项目下创建应用,创建的应用类型必须是"游戏"。
- 创建Android Studio工程。
- 生成签名证书。
- 获取签名证书指纹。
- AppGallery Connect网站配置签名证书指纹。
- 添加配置文件。
具体操作,请按照《HMS Core集成准备》中详细说明来完成。
4. 服务开通
- 在AppGallery Connect首页,点击"我的项目"->"选择您创建的项目"->"项目设置"->"API 管理",进入服务管理菜单。

- 点击 "华为帐号 "和"游戏服务"后面的开关,打开帐号和游戏服务。


5. 集成HMS Core SDK
添加当前应用的AppGallery Connect配置文件
- 在AppGallery Connect首页,点击"我的项目"->"选择您项目下的应用"->"项目设置"->"常规"->"应用"下的"agconnect-services.json"按钮,下载配置文件。

- 将"agconnect-services.json"文件拷贝到应用级根目录下。

配置HMS Core SDK的Maven仓地址
Android Studio的代码库配置在Gradle插件7.0以下版本、7.0版本和7.1及以上版本有所不同。请根据您当前的Gradle插件版本,选择对应的配置过程。
7.0以下版本 | 7.0版本 | 7.1及以上版本 |
7.0以下版本
- 打开Android Studio项目级build.gradle文件。

- 添加HUAWEI agcp插件以及Maven代码库。
- 在buildscript > repositories中配置HMS Core SDK的Maven仓地址。
- 在allprojects > repositories中配置HMS Core SDK的Maven仓地址。
- 如果App中添加了agconnect-services.json文件则需要在buildscript > dependencies中增加agcp插件配置。
buildscript{ repositories { google() jcenter() // 配置HMS Core SDK的Maven仓地址 maven {url 'https://developer.huawei.com/repo/'} } dependencies { classpath 'com.android.tools.build:gradle:3.5.4' // 增加agcp插件配置,推荐您使用最新版本的agcp插件。 classpath 'com.huawei.agconnect:agcp:1.6.0.300' } } allprojects { repositories { google() jcenter() // 配置HMS Core SDK的Maven仓地址 maven {url 'https://developer.huawei.com/repo/'} } }
7.0版本
- 打开Android Studio项目级build.gradle文件。
- 添加HUAWEI agcp插件以及Maven代码库。
在buildscript > repositories中配置HMS Core SDK的Maven仓地址。 - 如果App中添加了agconnect-services.json文件则需要在buildscript > dependencies中增加agcp插件配置。
buildscript{ repositories { google() jcenter() // 配置HMS Core SDK的Maven仓地址 maven {url 'https://developer.huawei.com/repo/'} } dependencies { classpath 'com.android.tools.build:gradle:3.5.4' // 增加agcp插[最新版本](https://developer.huawei.com/consumer/cn/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550)推荐您使用最新版本的agcp插件。 classpath 'com.huawei.agconnect:agcp:1.6.0.300' } } - 打开项目级settings.gradle文件,配置HMS Core SDK的Maven仓地址。
dependencyResolutionManagement { ... repositories { google() jcenter() maven {url 'https://developer.huawei.com/repo/'} }
7.1及以上版本
- 打开Android Studio项目级build.gradle文件。
- 如果App中添加了agconnect-services.json文件则需要在buildscript > dependencies中增加agcp插件配置。
buildscript{ dependencies { ... // 增加agcp插[最新版本](https://developer.huawei.com/consumer/cn/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550)推荐您使用最新版本的agcp插件。 classpath 'com.huawei.agconnect:agcp:1.6.0.300' } } - 打开项目级settings.gradle文件,配置HMS Core SDK的Maven仓地址。
pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() // 配置HMS Core SDK的Maven仓地址 maven {url 'https://developer.huawei.com/repo/'} } } dependencyResolutionManagement { ... repositories { google() mavenCentral() // 配置HMS Core SDK的Maven仓地址 maven {url 'https://developer.huawei.com/repo/'} } }
添加编译依赖
- 打开Android Studio应用级build.gradle文件。

- 在"dependencies"中添加如下编译依赖。其中:
- 添加agcp插件配置。请根据实际情况选择:
- 方式一:在文件头部声明的下一行添加如下配置。
apply plugin: 'com.huawei.agconnect' - 方式二:在plugins中添加如下配置。
plugins { id 'com.android.application' // 添加如下配置 id 'com.huawei.agconnect' }
- 方式一:在文件头部声明的下一行添加如下配置。
- 点击界面上的"Sync Now"链接下载编译已完成的配置。

配置混淆脚本
编译APK前需要添加混淆配置,避免功能异常。配置步骤如下:
- 打开Android Studio工程的混淆配置文件proguard-rules.pro。

- 加入混淆配置。
-ignorewarnings -keepattributes *Annotation* -keepattributes Exceptions -keepattributes InnerClasses -keepattributes Signature -keepattributes SourceFile,LineNumberTable -keep class com.hianalytics.android.**{*;} -keep class com.huawei.updatesdk.**{*;} -keep class com.huawei.hms.**{*;} -keep class com.huawei.gamebox.plugin.gameservice.**{*;} -keep interface com.huawei.hms.analytics.type.HAEventType{*;} -keep interface com.huawei.hms.analytics.type.HAParamType{*;} -keep class com.huawei.hms.analytics.HiAnalyticsInstance{*;} -keep class com.huawei.hms.analytics.HiAnalyticsInstance{*;} -keep class com.huawei.hms.analytics.HiAnalytics{*;} - 当您启用R8资源缩减(项目级"build.gradle"文件中"shrinkResources"属性为"true")和严格引用检查("res/raw/keep.xml"文件中的"shrinkMode"为"strict")时,请您配置"keep.xml"文件手动保留layout资源,确保应用正常通过华为应用市场上架审核。
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/hms_download_progress,@drawable/screen_off,@layout/upsdk*,@drawable/c_buoycircle*,@drawable/hms_game*,@layout/c_buoycircle*,@layout/hms_game*,@strings/hms_game*,@strings/c_buoycircle*" tools:shrinkMode="strict" />
6. 界面设计
本次Codelab中您可以在Android Studio工程中创建一个主页面,布局参照下图进行UI设计,新增四个Button。"init"点击后调用初始化接口,初始化接口调用成功后自动调用登录接口,"createArchive"点击后调用跳转到创建存档页面,"displayByAppAssistant"点击后跳转到华为应用助手展示存档列表页面, "displayUserSelf"点击后跳转到自定义展示存档列表页面,点击列表条目时,可以实现打开游戏存档的功能,具体页面展示和布局代码在10 开发展示游戏存档列表功能时再详细介绍,此处不展示。
// 布局代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_init"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="init"/>
<Button
android:id="@+id/btn_createArchive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="createArchive"/>
<Button
android:id="@+id/btn_displayByAppAssistant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="displayByAppAssistant"/>
<Button
android:id="@+id/btn_displayUserSelf"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="displayUserSelf"/>
</LinearLayout>
7. 开发游戏初始化和登录功能
游戏初始化
- 在Application的onCreate方法中添加注册Activity的回调监听。
public class MyApplication extends Application { @Override public void onCreate(){ super.onCreate(); HuaweiMobileServicesUtil.setApplication(this); } } - 在启动游戏的第一个Activity中,调用JosAppsClient的init方法初始化游戏SDK。以发布中国大陆,含游戏防沉迷回调的代码为例。
private void init(){ AccountAuthParams params = AccountAuthParams.DEFAULT_AUTH_REQUEST_PARAM_GAME; JosAppsClient appsClient = JosApps.getJosAppsClient(this); // 设置防沉迷提示语的context ResourceLoaderUtil.setmContext(this); Task<Void> initTask = appsClient.init(new AppParams(params, new AntiAddictionCallback() { @Override public void onExit() { // System.exit(0); // 该回调会在如下两种情况下返回: // 1.未成年人实名帐号在白天登录游戏,华为会弹框提示玩家不允许游戏,玩家点击"确定",华为返回回调 // 2.未成年实名帐号在国家允许的时间登录游戏,到晚上9点,华为会弹框提示玩家已到时间,玩家点击"知道了",华为返回回调 // 您可在此处实现游戏防沉迷功能,如保存游戏、调用帐号退出接口或直接游戏进程退出(如System.exit(0)) }})); initTask.addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { showLog("init success"); hasInit = true; // 一定要在init成功之后,才可以调用登录接口 signIn(); } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { if (e instanceof ApiException) { ApiException apiException = (ApiException) e; int statusCode = apiException.getStatusCode(); if (statusCode == JosStatusCodes.JOS_PRIVACY_PROTOCOL_REJECTED) { // 错误码为7401时表示用户未同意华为联运隐私协议 showLog("has reject the protocol"); // 此处您需禁止玩家进入游戏 } else if (statusCode == GamesStatusCodes.GAME_STATE_NETWORK_ERROR) { // 错误码7002表示网络异常 showLog("network error"); // 此处您可提示玩家检查网络,请不要重复调用init接口,否则断网情况下可能会造成手机高耗电。 } else if (statusCode == 907135003) { // 907135003表示玩家取消HMS Core升级或组件升级 showLog("init statusCode=" + statusCode); init(); } else { // 在此处实现其他错误码的处理 } } } }); }
游戏登录
- 游戏登录接口必须在init回调成功之后调用。
- 调用AccountAuthManager的getService方法初始化AccountAuthService对象,并调用AccountAuthService的silentSignIn方法实现静默登录。如果静默登录失败,当返回错误码为2002时,调用AccountAuthService的getSignInIntent方法拉起华为帐号登录授权页面,登录结果在onActivityResult里回调。需要注意的是:使用存档相关的功能,在登录时需要调用AccountAuthParamsHelper.setScopeList申请DRIVE_APP_DATA的Scope。
/** * 登录,返回已登录此应用的华为帐号登录信息(或者错误信息) */ List<Scope> scopes = new ArrayList<>(); scopes.add(GameScopes.DRIVE_APP_DATA); AccountAuthParams mAuthParams = new AccountAuthParamsHelper(AccountAuthParams.DEFAULT_AUTH_REQUEST_PARAM_GAME).setScopeList(scopes).createParams(); private void signIn(){ showLog("begin login and current hasInit=" + hasInit); // 一定要在init成功后,才可以调用登录接口 Task<AuthAccount> authAccountTask = AccountAuthManager.getService(this, mAuthParams).silentSignIn(); authAccountTask.addOnSuccessListener(new OnSuccessListener<AuthAccount>() { @Override public void onSuccess(AuthAccount authAccount) { showLog("signIn success"); } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { if (e instanceof ApiException) { ApiException apiException = (ApiException) e; int statusCode = apiException.getStatusCode(); if (2002 == statusCode){ showLog("start getSignInIntent"); signInNewWay(); }else { showLog("signIn failed:" + statusCode); } } } }); } /** * 获取到华为帐号登录授权页面的Intent,并通过调用startActivityForResult(Intent, int)打开华为帐号登录授权页面。 */ private void signInNewWay() { Intent intent = AccountAuthManager.getService(MainActivity.this, mAuthParams).getSignInIntent(); startActivityForResult(intent, SIGN_IN_INTENT); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (data == null){ return; } if (SIGN_IN_INTENT == requestCode) { handleSignInResult(data); }else { showLog("unknown requestCode in onActivityResult"); } } /** * 登录授权的结果响应处理方法 * @param data Data */ private void handleSignInResult(Intent data) { String jsonSignInResult = data.getStringExtra("HUAWEIID_SIGNIN_RESULT"); if (TextUtils.isEmpty(jsonSignInResult)) { showLog("SignIn result is empty"); return; } try { AccountAuthResult signInResult = new AccountAuthResult().fromJson(jsonSignInResult); if (0 == signInResult.getStatus().getStatusCode()) { showLog("Sign in success."); } else { showLog("Sign in failed: " + signInResult.getStatus().getStatusCode()); } } catch (JSONException var7) { showLog("Failed to convert json from signInResult."); } } private static final String TAG = "MainActivity"; /** * 打印日志 * @param msg */ private void showLog(String msg){ Log.d(TAG,msg); }
8. 初始化ArchivesClient实例
在使用游戏存档接口的能力前都需要先调用Games.getArchiveClient获取ArchivesClient实例。示例代码如下:
ArchivesClient client = Games.getArchiveClient(this);
9. 开发新建游戏存档功能
- 新建游戏存档时需要把游戏数据写入存档文件中,开发过程中根据实际情况获取,在本次codelab中,设计了一个简单的页面,数据手动输入,布局显示如下:

字段名称 | 字段描述 |
description | 存档描述。 |
playedTime | 游戏时长。 |
progress | 游戏进度。 |
isCache | 是否支持本地缓存。选中"YES"表示支持本地缓存,选中"NO"表示不支持本地缓存。 |
hasImage | 是否有封面图面,此处显示的是一个固定的图片,可以根据实际业务需求修改。选中"YES"表示有封面图片,选中"NO"表示没有封面图片。 |
submit | 点击此按钮,调用新建存档接口。 |
// 布局代码
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:id="@+id/tv_des"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="description:"/>
<EditText
android:id="@+id/et_des"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/tv_des"
android:layout_toRightOf="@+id/tv_des"
android:hint="Please enter the description."
android:textSize="16sp" />
<TextView
android:id="@+id/tv_playedTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/et_playedTime"
android:text="playedTime:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_playedTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/et_des"
android:layout_alignLeft="@+id/et_des"
android:layout_marginTop="20dp"
android:hint="Please enter the playedTime."
android:textSize="16sp" />
<TextView
android:id="@+id/tv_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/et_progress"
android:textSize="16sp"
android:text="progress:"/>
<EditText
android:id="@+id/et_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/et_playedTime"
android:layout_alignLeft="@+id/et_des"
android:layout_marginTop="20dp"
android:hint="Please enter the progress."
android:textSize="16sp" />
<TextView
android:id="@+id/tv_isSupportCache"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/rg_isCache"
android:textSize="16sp"
android:layout_marginTop="10dp"
android:text="isCache:"/>
<RadioGroup
android:id="@+id/rg_isCache"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/et_des"
android:layout_below="@+id/et_progress"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_cache_yes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="YES"/>
<RadioButton
android:id="@+id/rb_cache_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:text="NO"/>
</RadioGroup>
<TextView
android:id="@+id/tv_hasImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/rg_hasImage"
android:textSize="16sp"
android:layout_marginTop="10dp"
android:text="hasImage:"/>
<RadioGroup
android:id="@+id/rg_hasImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/et_des"
android:layout_below="@+id/rg_isCache"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_hasImage_yes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="YES"/>
<RadioButton
android:id="@+id/rb_hasImage_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:text="NO"/>
</RadioGroup>
<ImageView
android:id="@+id/iv_cover_pic"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_below="@+id/rg_hasImage"
android:layout_centerHorizontal="true"
android:src="@mipmap/cover_picture" />
<Button
android:id="@+id/submit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_cover_pic"
android:layout_marginTop="20dp"
android:textAllCaps="false"
android:text="submit"/>
</RelativeLayout>
</ScrollView>
- 应用需要使用ArchiveDetails.set方法将玩家当前的游戏存档文件写入ArchiveDetails对象。
String description = etDes.getText().toString(); long playedTime = Long.parseLong(TextUtils.isEmpty(etPlayedTime.getText().toString()) ? "0": etPlayedTime.getText().toString()); long progress = Long.parseLong(TextUtils.isEmpty(etProgress.getText().toString()) ? "0" : etProgress.getText().toString()); ArchiveDetails archiveDetails = new ArchiveDetails.Builder().build(); archiveDetails.set((progress + description + playedTime).getBytes()); - 游戏将存档元数据(如存档描述、游戏进度、封面图片等)写入ArchiveSummaryUpdate对象。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.cover_picture); ArchiveSummaryUpdate.Builder builder = new ArchiveSummaryUpdate.Builder(); builder.setActiveTime(playedTime) .setCurrentProgress(progress) .setDescInfo(description); ArchiveSummaryUpdate archiveSummaryUpdate; if (hasImage){ archiveSummaryUpdate = builder.setThumbnail(bitmap) .setThumbnailMimeType("png").build(); }else { archiveSummaryUpdate = builder.build(); } - 调用ArchivesClient.addArchive(ArchiveDetails details, ArchiveSummaryUpdate update, boolean isSupportCache)方法以异步方式提交存档,存档提交成功后HMS Core SDK将以ArchiveSummary对象方式向应用返回存档成功后的存档元数据。
Task<ArchiveSummary> archiveSummaryTask = client.addArchive(archiveDetails, archiveSummaryUpdate, isCache);
archiveSummaryTask.addOnSuccessListener(new OnSuccessListener<ArchiveSummary>() {
@Override
public void onSuccess(ArchiveSummary archiveSummary) {
if (archiveSummary != null){
String fileName = archiveSummary.getFileName();
String archiveId = archiveSummary.getId();
String descInfo = archiveSummary.getDescInfo();
showLog("create archive success descInfo:" + descInfo + ",fileName:" + fileName+ ",archiveId:" + archiveId);
finish();
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof ApiException){
ApiException apiException = (ApiException) e;
int statusCode = apiException.getStatusCode();
if(statusCode == 7216){
Log.e(TAG,"Check whether the format of the archive cover image is correct.");
}else if(statusCode == 7218){
Log.e(TAG,"The game service is disabled.");
}else if(statusCode == 7013){
// 未登录华为帐号 You have not logged in to your Huawei ID.
signIn();
}else if(statusCode == 7213){
//存档个数达到上限 The number of archived files has reached the upper limit.
Log.e(TAG,"The number of archived files has reached the upper limit.");
}else{
Log.e(TAG,"statusCode:"+ statusCode);
}
}
}
});
private static final String TAG = "CreateArchiveActivity";
/**
* Print logs
* 打印日志
* @param msg
*/
private void showLog(String msg){
Log.d(TAG,msg);
}
10. 开发展示游戏存档列表功能
玩家可以在存档入口选择浏览存档记录,此时游戏需要展示所有的存档记录供用户选择。存档记录选择界面的展示方法分为2种:
- 直接展示应用助手的存档页面:游戏需要获取应用助手存档界面的Intent对象,然后通过startActivityForResult方法拉起存档选择页面,您不需要自己开发存档展示页面。
- 自行展示存档列表:获取存档数据,自行开发存档记录页面。
直接展示应用助手的存档列表
- 当玩家进入存档入口时,应用调用ArchivesClient.getShowArchiveListIntent方法获取应用助手存档页面对应的Intent对象。当取得存档列表界面对应的Intent对象后,应用可以调用startActivityForResult方法直接拉起华为应用助手的存档列表页面。
Task<Intent> showArchiveListIntentTask = mClient.getShowArchiveListIntent("游戏存档", true, true, -1); showArchiveListIntentTask.addOnSuccessListener(new OnSuccessListener<Intent>() { @Override public void onSuccess(Intent intent) { if (intent != null){ startActivityForResult(intent,1); } } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { if (e instanceof ApiException){ ApiException apiException = (ApiException) e; Log.e(TAG, "statusCode:" + apiException.getStatusCode()); } } }); - 如果用户在应用助手存档列表中选择一个存档查看,游戏需要调用ArchivesClient.parseSummary获取选择的存档元数据,Bundle对象使用Intent对象的getParcelableExtra方法获取。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (data == null){ return; } if (1 == requestCode){ if (data.hasExtra(ArchiveConstants.ARCHIVE_SELECT)){ Bundle bundle = data.getParcelableExtra(ArchiveConstants.ARCHIVE_SELECT); Task<ArchiveSummary> archiveSummaryTask = mClient.parseSummary(bundle); archiveSummaryTask.addOnSuccessListener(new OnSuccessListener<ArchiveSummary>() { @Override public void onSuccess(ArchiveSummary archiveSummary) { if (archiveSummary != null){ String archiveId = archiveSummary.getId(); String descInfo = archiveSummary.getDescInfo(); String fileName = archiveSummary.getFileName(); showLog("get archive info success archiveId:" + archiveId + ",fileName:" + fileName+ ",descInfo:" + descInfo); } } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { if (e instanceof ApiException){ ApiException apiException = (ApiException) e; Log.e(TAG, "statusCode:" + apiException.getStatusCode()); } } }); }else if (data.hasExtra(ArchiveConstants.ARCHIVE_ADD)){ // 调用addArchive添加存档 } }else { showLog("unknown requestCode in onActivityResult"); } } private static final String TAG = "MainActivity"; /** * Print logs * 打印日志 * @param msg */ private void showLog(String msg){ Log.d(TAG,msg); }
自行展示存档列表
- 自行展示存档列表页面是由开发者自行设计,这里以展示存档封面图片和存档描述为例,页面展示效果如下:

// 主界面布局代码 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rcv_archive" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout> // 列表中每个条目的布局代码 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingHorizontal="15dp" android:paddingVertical="10dp" android:gravity="center_vertical"> <ImageView android:id="@+id/iv_archive_icon" android:layout_width="50dp" android:layout_height="50dp" android:src="@mipmap/ic_launcher"/> <TextView android:id="@+id/tv_archive_desc" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" android:textColor="@color/black" android:layout_marginLeft="15dp"/> </LinearLayout> - 玩家进入存档页面入口时,游戏可以调用ArchivesClient.getArchiveSummaryList方法获取所有存档列表,支持从本地缓存或华为游戏服务器获取。获取到数据之后,通过自定义的RecyclerView列表适配器,把数据显示到页面上。
Task<List<ArchiveSummary>> archiveSummaryListTask = mArchivesClient.getArchiveSummaryList(false); archiveSummaryListTask.addOnSuccessListener(new OnSuccessListener<List<ArchiveSummary>>() { @Override public void onSuccess(List<ArchiveSummary> archiveSummaries) { if (archiveSummaries != null){ mSummaries = archiveSummaries; Log.i(TAG, " archiveSummaries size" + archiveSummaries.size()); mArchiveAdapter = new ArchiveAdapter(archiveSummaries,DisplayArchiveUserselfActivity.this,DisplayArchiveUserselfActivity.this); rcv_archive.setAdapter(mArchiveAdapter); } } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { if (e instanceof ApiException){ ApiException apiException = (ApiException) e; Log.e(TAG, "statusCode:" + apiException.getStatusCode()); } } }); // 适配器代码截取数据处理部分 ArchiveSummary archiveSummary = summaries.get(position); holder.tv_archive_desc.setText(archiveSummary.getDescInfo()); if (archiveSummary.hasThumbnail()){ Task<Bitmap> thumbnailTask = mClient.getThumbnail(archiveSummary.getId()); thumbnailTask.addOnSuccessListener(new OnSuccessListener<Bitmap>() { @Override public void onSuccess(Bitmap bitmap) { holder.iv_archive_icon.setImageBitmap(bitmap); } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { if (e instanceof ApiException){ ApiException apiException = (ApiException) e; Log.e("ArchiveAdapter","statusCode:" + apiException.getStatusCode()); } } }); }
11. 开发打开游戏存档功能
打开游戏存档是指玩家进入游戏后加载之前提交的存档继续进行游戏。如果您打开华为应用助手展示的存档,华为应用助手会自动加载打开游戏存档功能;如果您打开自行展示的存档,请参考本章节完成打开游戏存档功能。建议调用ArchivesClient.getArchiveSummaryList方法获取的存档,不要打开本地缓存的存档。
在游戏存档时可能存在同一个帐号在多个设备中同时存档的情况,如果设备因网络问题存档延时,则可能会发生数据冲突。当更新存档或打开存档时发现两份存档的存档属性对比存在不一致时,华为游戏服务器会返回对应的冲突结果给您,并允许您选择使用其中某份存档作为最终使用的存档,或者也允许您自主混合存档数据作为最终使用的存档。
- 玩家选择一个存档加载时,应用调用ArchivesClient的loadArchiveDetails方法加载存档文件,有以下几种方式选择。
- 不处理数据冲突:即在发生数据冲突时华为服务器不处理数据冲突,需要您自行处理。此方式使用loadArchiveDetails(ArchiveSummary summary)方法或loadArchiveDetails(String archiveId)方法。
- 指定冲突处理的策略:可以指定游戏时长、进度、修改时间作为冲突处理策略,如果发生冲突,华为服务器使用指定的策略处理数据冲突。此方式使用loadArchiveDetails(ArchiveSummary summary, int diffStrategy)方法或loadArchiveDetails(String archiveId, int diffStrategy)方法。
这里以loadArchiveDetails(ArchiveSummary summary, int diffStrategy)方法为例,由华为服务器使用指定的策略处理冲突。"diffStrategy"取值为ArchiveConstants.STRATEGY_TOTAL_PROGRESS 表示加载存档数据发生冲突时,使用进度较高的存档处理冲突。
- 如果结果中的public boolean isDifference()返回true,表示加载存档时发生数据冲突,您需要进行数据冲突处理,详细步骤参见冲突处理,本codelab暂不做详细说明。
- 当存档加载成功后,应用可以使用OperationResult.getArchive获取包含存档元数据和存档文件内容的Archive对象,再使用Archive.getDetails方法获取包含存档文件内容的ArchiveDetails对象。
点击存档列表条目时,加载游戏存档数据。示例代码如下:mArchiveAdapter.setOnItemClickListener(new ArchiveAdapter.OnItemClickListener() { @Override public void onItemClick(View view, int position) { ArchiveSummary archiveSummary = mSummaries.get(position); Task<OperationResult> operationResultTask = mArchivesClient.loadArchiveDetails(archiveSummary, ArchiveConstants.STRATEGY_TOTAL_PROGRESS); operationResultTask.addOnSuccessListener(new OnSuccessListener<OperationResult>() { @Override public void onSuccess(OperationResult operationResult) { Log.i(TAG, "isDifference:" + ((operationResult == null) ? "" : operationResult.isDifference())); if (operationResult != null && !operationResult.isDifference()) { Archive archive = operationResult.getArchive(); if (archive != null && archive.getSummary() != null) { ArchiveSummary summary = archive.getSummary(); String archiveId = summary.getId(); String descInfo = summary.getDescInfo(); String fileName = summary.getFileName(); Log.i(TAG, "get archive info success archiveId:" + archiveId + ",fileName:" + fileName+ ",descInfo:" + descInfo); } } else { //处理冲突 Log.i(TAG, "Handling Conflicts"); } } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { if (e instanceof ApiException) { ApiException apiException = (ApiException) e; Log.e(TAG, "statusCode:" + apiException.getStatusCode()); } } }); } }); - 获取存档文件内容后,应用可以使用ArchiveDetails.get方法读取存档文件中的二进制游戏数据,读取完成后将游戏数据加载给用户。
archive.getDetails().get();
12. 打包测试
- 启动应用或点击主页面中的init,若日志中输出MainActivity: init success表示初始化成功,日志中输出MainActivity: signIn success或MainActivity: Sign in success.表示登录成功。若无这些信息表示初始化和登录接口调用失败,根据具体错误码处理。
- 在新建游戏存档页面的输入栏输入存档描述、游戏时长和游戏进度,isCache选择YES,hasImage选择YES,点击submit,日志中输出CreateArchiveActivity: create archive success descInfo:archive1,fileName:noname1******6653800f,archiveId:BrD******DAakl3zX3表示游戏存档创建成功。descInfo即为页面中输入的存档描述,fileName和archiveId都是华为游戏服务器生成的,表示保存的存档文件名和存档唯一标识。
- 点击主页面displayByAppAssistant按钮后出现下图所示页面,表示应用助手加载存档列表功能正常。

- 点击主界面displayUserSelfstrong>按钮后,显示如下页面,表示自行展示存档列表功能正常。点击列表中的每个条目,日志中输出DaUserselfActivity: get archive info success archiveId:BrD************l3zX3,fileName:nonam**********36653800f,descInfo:archive1strong>表示打开游戏存档功能正常。

- 在应用助手页面选择一个存档查看,日志中输出MainActivity: get archive info success archiveId:BrD******DAakl3zX3,fileName:noname1******6653800f,descInfo:archive1strong>表示通过应用助手展示的存档页面查看存档功能正常。
13. 恭喜您
恭喜您,您已经成功完成了Codelab并学到了:
- 如何在AppGallery Connect创建项目及应用。
- 如何使用华为HMS Core提供的游戏服务。
- 如何使用华为游戏服务开发游戏存档相关功能
14. 参考
您可以阅读下面链接,了解更多相关的信息。
本Codelab中所用API请参考游戏服务的存档相关API介绍
游戏存档官方文档介绍,请参考存档
相关错误码说明,请参考错误码
源码下载地址:源码下载
剩余