T1 自定义副显程序开发

【提示:在开发之前,请认真阅读文档中的内容,如有疑问可以联系商米技术支持咨询。】

注:插入usb调试线,会导致主副屏断开。

关于T1双屏

 

商米T1有两种双屏配置:

 

1. 主屏14寸,副屏7寸。

注:7寸副屏仅支持浏览,不支持触控,不支持安装app。用户如需要修改副屏界面,请参考内置副显程序对接文档

Ouk69hAZ0gIkhS99

2. 主屏14寸,副屏14寸,副屏可触摸。

2


详细的硬件说明,请查看官网介绍,设备主副屏运行的都是SUNMI OS系统,相互通过商米封装好的接口实现通信。

注:不要把sunmi的有关代码混淆,混淆可能造成组件无法工作

#sunmi

-keep class com.luedongtech.kr3000.sunmi.** {*;}

-keep class sunmi.ds.**{*;}

-keep class com.sunmi.**{*;}

-keep class sunmi.sc.**{*;}

-keep class com.google.gson.**{*;}

通常开发者的业务app运行在主屏,同时需要副屏显示清单信息、宣传图片,视频等内容,有时还需要实现简单的交互操作。开发者有两种方式使用T1副屏显示内容:

 

    1. 使用T1副屏系统内置的显示程序。只要主屏app遵照商米的规范,向副显程序发特定格式的数据就可让副显程序显示内容(开发成本低,推荐)。

   

    2. 开发者自己写副屏显示app(开发成本高,自由度高)。

 

虽然商米封装的副显程序能满足绝大部分的开发者对于副屏显示内容的需求,但是业务是多样的,商米始终秉持开放原则,支持开发者自己写副显程序。下面将对自己写副显程序做详细的说明。

  • 副显app的分发

我们规定,只要在您的主屏业务App的AndroidManifest.xml中添加一行配置(文档最后讲),业务App在通过应用市场安装到主屏的同时,也会在副屏上安装,也就是说,您的主屏App同时也是副屏App,这里需要您在代码中进行首页显示内容的控制 。

进入正题

 

自定义副显程序开发的核心问题是主副屏的通信,实现了主副屏通信后,您可以把副屏当成是普通的平板电脑开发相关的副屏app。这里有必要先了解一下双屏的通信方式,我们通过一张图片向您直观展示T1双屏的通信协议。

 

 

prototype1


T1的通信协议,从下到上依次包括:

 

    1. Driver层,该层是最底层的通信协议,开发者无需关注。

    2. Service层,该层是商米封装的一个通信服务,开发者也无需关注。

    3. SDK层,该层是T1自带的副显程序使用的通信接口,自定义的副显程序调用的也是这层的接口

    4. APP层,指的是开发者自己的业务app和内置的副显程序。

 

  • 如果使用的是T1内置副显程序,数据通信方式如上图所示,开发者的主屏业务App只要调用商米的SDK按照商米规定好的数据格式向副显发数据,剩下的交给系统处理即可。
  • 如果您自己开发副显程序,就要自己处理数据的收发,内容显示等动作,如果您没有强烈的自定义需求,我们建议您使用T1自带的副显程序显示内容,可以节约很多的人力,物力成本。

 

开始开发

我们给开发者准备了一个双屏通信的Demo,您可以下载副显程序开发资源,参照Demo中的代码实现您自己的副显app,请按照下面的流程部署该Demo:

 

    1. 将资源文件中的HCService.apk安装到模拟主副屏通信的两台设备上,资源中包含两个HCService的apk,请先安装release版本,如果安装失败请尝试安装unsigned版本的apk。


    2. 主副屏上分别运行这个程序,在第一台设备上输入自己的IP地址,勾选”作为主屏”,点击确定。


    3. 在另一台设备上输入主屏机器的IP地址,点击确定(不要勾选‘作为主屏’),可以看到屏幕中弹出toast提示“与主/副屏连接”,就代表主副屏能通信了,如果没有弹出提示,请在设置中的应用程序管理中找到HCService程序 (进入设置->应用->点击右上角的更多->点击显示系统进程,才能看到HCService),强制停止主副屏上的HCService服务,再重复上面的步骤试试。


    4. 将MyDSD项目导入到AndroidStudio中,如果报错了请检查您的编译环境的版本是否兼容demo,可以自己做相关的调整。注:我们不建议您使用eclipse开发副显程序,目前没有提供相关的eclipse资


    5. 这里请修改AndroidManifest.xml中的启动页面为MainScreenActivity,然后将项目部署到主屏设备中,运行。


    6. 再修改启动页面为ViceScreenActivity,然后将项目部署到副屏设备中,运行。


    7.在主屏上点击发送文字的按钮,查看副屏显示内容的变更,然后可以发送图片给副屏查看显示情况。


在这个Demo中包含了主副屏相互发送字符数据的方法,发送单个文件的方法和发送多个文件的方法。以下我们将基于Demo讲解主副屏通信的开发流程:

1. 在build.gradle中添加以下依赖:

dependencies {
    compile files('libs/commons-codec-1.9.jar')
    provided files('libs/framework.jar')
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.sunmi:DS_Lib:latest.release'
    compile 'com.google.code.gson:gson:2.6.2'
}
 
 

需要添加的权限:(强调:Sunmi OS是基于Android6.0定制,Android6.0+ 要求部分敏感权限需要动态申请)

 

还有一个广播接收器,注意,这个是固定的写法,不要做修改哦。

"sunmi.ds.MsgReceiver">
    
        "com.sunmi.hcservice" />
        "com.sunmi.hcservice.status" />
    

 

2. 初始化,您可以在Application中初始化SDK的代码,也可以在任何您想要的地方初始化:

private void initSDK() {
    mDSKernel = DSKernel.newInstance();
    mDSKernel.init(this, mConnCallback);
}

以上DSKernel类是SDK中的核心类,几乎所有双屏通信相关的方法都在这个类中。在初始化后需要添加两个监听类:

//添加主副屏连接状态的监听
mDSKernel.addConnCallback(new IConnectionCallback(){
                        @Override
                        public void onConnected(ConnState arg0) {
                        //连接上了回调
                        }

                        @Override
                        public void onDisConnect() {
                        //断开连接的回调
                        }
                        
                });

//添加接收到数据的回调                
mDSKernel.addReceiveCallback(new IReceiveCallback(){

                        @Override
                        public void onReceiveCMD(DSData arg0) {
                          //收到命令      
                        }

                        @Override
                        public void onReceiveData(DSData arg0) {
                           //收到数据
                        }

                        @Override
                        public void onReceiveFile(DSFile arg0) {
                           //收到单个文件
                        }

                        @Override
                        public void onReceiveFiles(DSFiles arg0) {
                           //收到多个文件
                        }
             
                        
                });   



以上我们完成了初始化,通信当然会分信息的发送和接收了,主副屏可以相互发送接收数据。可以看到在MyDSD里面,MainScreenActivity中包含很多发数据的方法,在ViewScreenActiivty中包含很多接收数据的方法。

所有的方法可以在DSKernel类中查看到,详情可以查看 T1双屏通信接口文档 ,以下我们将讲解部分接口的使用:

1.发送字符数据,副屏接收显示

DataPacket packet = UPacketFactory.buildShowText(DSKernel.getDSDPackageName(), "成功接收字符数据!",null);
mDSKernel.sendData(packet);//参数DataPacket是双屏通信数据打包类,请参照Demo生成该数据
 

接收的方法是在IReceiveCallback接口的onReceiveData(DSData data)中:

@Override
public void onReceiveData(DSData data) {
    Log.d(TAG, "获取到数据:" + data.data);//将接收到的数据打印出来
}


2.发送指令的方法:

String json = UPacketFactory.createJson(DataModel.QRCODE, "");
//三个参数:1.接收指令的副屏app的包名;2.要发送的指令,这里可以参照Demo;3.发送结果回调
mDSKernel.sendCMD(DSKernel.getDSDPackageName(), json.toString(), l,null);

 

接收指令的方法:

@Override
public void onReceiveCMD(DSData cmd) {
     Log.d(TAG, "获取到数据:" + cmd.data);//将接收到的数据打印出来
}
 

指令的使用场景举例:图片视频等的显示需要先将文件发送到副屏,副屏会返回一个文件的id给主屏,然后向副屏发送指令显示指定id的图片或视频。

3.发送单个文件的方法:

//该方法有三个参数:1.接收端app的包名;2.文件在本地存储中的路径;3.发送结果回调
mDSKernel.sendFile(PACKAGE_OF_VICE, Environment.getExternalStorageDirectory().toString()+"/"+videoInAsserts,
      new ISendCallback() {
         public void onSendSuccess(long l) {
            Log.i("sunmi", "这里表示发送成功" + l);
            showVideo(l);
         }

      
         public void onSendFail(int i, String s) {
            //tv_show_id.setText("fail:" + s);
            Log.i("sunmi", "---:" + s);
         }

         public void onSendProcess(long l, long l1) {
            //发送进度的方法
         }
      });

 

接收单个文件的方法,IReceiveCallback接口的onReceiveFile(DSFile file) 中通过调用FilesManager.saveFile(DSFile file)将文件保存在本地:

@Override
public void onReceiveFile(DSFile file) {
   Log.i(TAG,"接收到的单个文件的路径:"+file.path);
   Log.i(TAG,"接收到的单个文件的taskId:"+file.taskId);
    mFilesManager.saveFile(file);
}
 

4.发送多个文件的方法

mDSKernel.sendFiles(PACKAGE_OF_VICE, json.toString(), paths, new ISendFilesCallback(){
   @Override
   public void onAllSendSuccess(long l) {
     //所有文件发送成功
   }
   @Override
   public void onSendFaile(int arg0, String arg1) {}
   @Override
   public void onSendFileFaile(String arg0, int arg1, String arg2) {}
   @Override
   public void onSendProcess(String arg0, long arg1, long arg2) {}
   @Override
   public void onSendSuccess(String arg0, long arg1) {
   }
   
});

 

多个文件的接收方法:

@Override
public void onReceiveFiles(DSFiles dsFiles) {
            mFilesManager.saveFiles(dsFiles);
        }

 

5.启动指定应用的方法

 

参照Demo生成数据,向副屏发指令,该代码需要DSD程序的支持,可以在下载的资源文件中找到该apk,安装在副屏。

DataPacket dsPacket   = UPacketFactory.buildOpenApp(PACKAGE_OF_VICE, null);
//PACKAGE_OF_VICE为要启动的副屏指定应用的包名,如:com.sunmi.mydsd
mDSKernel.sendCMD(dsPacket);

 

以上介绍了一些基本接口的使用,更多的接口请参看DSKernel中的代码注释。

 

 

应用的发布

主副屏的应用都开发好了,当然是通过商米应用市场将该应用大规模分发出去了,上面我们讲了,只要在应用的AndroidManifest.xml中添加一行配置,该应用通过应用市场在主屏安装的时候,只要双屏通信畅通,就会在副屏上同时安装该应用(必须通过应用市场安装才能生效)。主屏和副屏程序是写在一个应用里的,所以需要您在入口类中添加相关的判断逻辑,这就是运行Demo时您需要修改启动页的原因,具体判断主副屏的代码可以参考Demo。




"sunmi_dual" android:value="open"/>

 

钱箱驱动

关于商米设备操作钱箱

商米部分设备如T1可以连接外部钱箱,App可以通过以下两种方式打开钱箱。

一、通过AIDL文件中封装的方法打开钱箱。

二、通过指令打开钱箱。

一、通过AIDL文件中的方法打开钱箱

1.下载相关 资源文件 ,在项目中新建如下层级的package,将源文件中的AIDL文件放入package中。

1

2.操作钱箱的只有打开钱箱和获取钱箱打开次数这两个方法如下图所示,两个方法在IWoyouService.aidl文件中,开发者可以在自己的代码中调用。

2

二、通过ES/POS指令的方式

开发者可以通过两种方式向服务发送ES/POS指令

1.通过虚拟蓝牙与服务建立连接,发送ES/POS指令,可参照 打印机驱动 文档中的通过蓝牙连接调用发送指令打开钱箱,打开钱箱的ES/POS指令如下:

byte[] aa = new byte[5];
aa[0] = 0x10;
aa[1] = 0x14;
aa[2] = 0x00;
aa[3] = 0x00;
aa[4] = 0x00;

2.通过AIDL方法中封装的sendRAWData(bytes [] ,callback )方法发送指令,同样需要用到方式一中的资源文件,可参照 打印机驱动文档中的AIDL打印方式:

byte[] aa = new byte[5];

aa[0] = 0x10;
aa[1] = 0x14;
aa[2] = 0x00;
aa[3] = 0x00;
aa[4] = 0x00;

 try {
  woyouService.sendRAWData(aa, callback);
  } catch (RemoteException e1) {
     e1.printStackTrace();
   }

音量键自定义

什么是音量键自定义

2.1.0版本后的SUNMIUI加入了音量键自定义功能。用户可以在系统设置中设置单按上、下音量键快捷操作指定应用的特定功能,设置流程如下所示。

1

开发者怎么使用

目前可以支持的快捷操作包括:打开应用,打开扫码页,打开显示二维码页面。

 

其中设置打开应用不需要应用开发者配合修改代码即可实现,扫码和显示二维码需要应用本身有这些功能,然后开发者按照商米的规则在AndroidManifest.xml中该功能页配置中添加配置信息,用户才能在自定义音量键的时候指定按音量键操作该应用的这些功能。

所有的配置都是在项目的AndroidManifest.xml中添加,下面以支持快速启动扫码为例,介绍需要在自己App种添加的配置信息。

1.在扫码页的Activity配置中设置启动模式为singleInstance。

2.在intent-filter配置中添加catetory,固定写法android.intent.category.DEFAULT。

3.在intent-filter中添加action,action的规则为:开发者的应用包名+商米规定的业务功能后缀。如您的应用包名com.example.test,业务功能名为scan,则actioname为com.example.test.scan,如下图所示。

10175015530457499

显示二维码的配置方式类似,只是action配置项不同,以下列出所有action动作的列表。

3

注:以上为目前支持的操作,更多的操作动作在后续添加。

商米扫码

为什么要使用商米封装的扫码SDK

商米提供了适配自己设备的扫码SDK,相对于目前使用的开源方案,商米的扫码SDK有以下5个优势

  1. 识别率高,经过大量模拟真实场景的测试,商米扫码SDK相对普遍使用的基于ZXing开源项目的扫码方案在扫码识别率上平均提高了74%。
  2. 比ZXing方案在一维码扫码速度上快了100%以上。
  3. 使用方式更简单,5行代码就能在自己的项目中添加扫码功能。
  4. 支持扫描多达15种码,后续还将添加更多的码种。
  5. 与商米的设备完美适配,软硬件结合可以保证功能的高效稳定。

怎么使用商米的扫码SDK

开发者有两种方式使用商米的扫码SDK

  1. 开发者的应用调用SUNMIUI系统集成的扫码模块完成扫码,获取返回值,该方法简单易用。
  2. 自己写相机界面,调用商米的封装的扫码SDK完成图片的解析,该方式相对复杂,但提供了更高的自由度。

第一种使用方式:

为了降低开发难度,商米在最新的SUNMI OS(V1固件版本187,M1固件版本37)系统中内置了一个扫码的模块,开发者在项目需要调用扫码的地方通过startActivityForResult()调用商米的扫码模块,然后在onActivityResult()方法中接受扫码结果返回值。

/**

	* 外部应用在自己的业务代码需要启动扫码的地方使用下面的方式创建Intent,

	* 然后使用startActivityForResult()调用起商米的扫码模块;

	*/

	Intent intent = new Intent("com.summi.scan");

	intent.setPackage("com.sunmi.sunmiqrcodescanner");

	        

	/**

	* 使用该方式也可以调用扫码模块

	*Intent intent = new Intent("com.summi.scan");

	*intent.setClassName("com.sunmi.sunmiqrcodescanner", 

	"com.sunmi.sunmiqrcodescanner.activity.ScanActivity");

	*/

	/**

	//扫码模块有一些功能选项,开发者可以通过传递参数控制这些参数,

	//所有参数都有一个默认值,开发者只要在需要的时候添加这些配置就可以。

	intent.putExtra("CURRENT_PPI", 0X0003);//当前分辨率 

	//M1和V1的最佳是800*480,PPI_1920_1080 = 0X0001;PPI_1280_720 = 

	//0X0002;PPI_BEST = 0X0003;

	intent.putExtra("PLAY_SOUND", true);// 扫描完成声音提示  默认true

	intent.putExtra("PLAY_VIBRATE", false);

	//扫描完成震动,默认false,目前M1硬件支持震动可用该配置,V1不支持

	intent.putExtra("IDENTIFY_INVERSE_QR_CODE", true);// 识别反色二维码,默认true

	intent.putExtra("IDENTIFY_MORE_CODE", false);// 识别画面中多个二维码,默认false        

	intent.putExtra("IS_SHOW_SETTING", true);// 是否显示右上角设置按钮,默认true

	intent.putExtra("IS_SHOW_ALBUM", true);// 是否显示从相册选择图片按钮,默认true

	*/
	startActivityForResult(intent, START_SCAN);           

在onActivityResult方法中接收返回的扫码结果参数,参考如下代码: @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 1 && data != null) { Bundle bundle = data.getExtras(); ArrayList> result = (ArrayList>) bundle .getSerializable("data"); Iterator> it = result.iterator(); while (it.hasNext()) { HashMap hashMap = it.next(); Log.i("sunmi", hashMap.get("TYPE"));//这个是扫码的类型 Log.i("sunmi", hashMap.get("VALUE"));//这个是扫码的结果 } } super.onActivityResult(requestCode, resultCode, data); }

 @Override

protected void onActivityResult(int requestCode, int resultCode, Intent data) {

if (requestCode == 1 && data != null) {

Bundle bundle = data.getExtras();

ArrayList> result = (ArrayList>)

bundle .getSerializable(“data”);

Iterator> it = result.iterator();

while (it.hasNext()) {

HashMap hashMap = it.next();

Log.i(“sunmi”, hashMap.get(“TYPE”));//这个是扫码的类型

Log.i(“sunmi”, hashMap.get(“VALUE”));//这个是扫码的结果

}

}

super.onActivityResult(requestCode, resultCode, data);

}

第二种方式:

1. 在项目的libs目录中按以下层级添加libiconv.so,libscaninit.so,libsunmiscan.so和sunmiscan.jar四个库文件。

project

2. 在处理业务的代码中引入头文件和解码库,可以参照DEMO。

        import com.sunmi.scan.Config;

	import com.sunmi.scan.Image; 

	import com.sunmi.scan.ImageScanner; 

	import com.sunmi.scan.Symbol;

	import com.sunmi.scan.SymbolSet; 

        private ImageScanner scanner;//声明扫描器 

	scanner = new ImageScanner();//创建扫描器

	scanner.setConfig(0, Config.X_DENSITY, 2);//行扫描间隔

	scanner.setConfig(0, Config.Y_DENSITY, 2);//列扫描间隔

	scanner.setConfig(0, Config.ENABLE_MULTILESYMS, 0);

	//是否开启同一幅图一次解多个条码,0表示只解一个,1为多个

	scanner.setConfig(0, Config.ENABLE_INVERSE, 0);//是否解反色的条码  

        scanner.setConfig( Symbol.QRCODE,Config.ENABLE, 1);//允许识读QR码,默认1:允许

        scanner.setConfig( Symbol.PDF417,Config.ENABLE, 1);//允许识读PDF417码,默认0:禁止

        scanner.setConfig(Symbol.DataMatrix, Config.ENABLE, 1);//允许识读DataMatrix码,默认0:禁止

        scanner.setConfig(Symbol.AZTEC, Config.ENABLE, 1);//允许识读AZTEC码,默认0:禁止

4.传入图像数据和解码,以下的代码可以写在PreviewCallback.onPreviewFrame(byte[] data, Camera camera)方法中。

  /**
    *创建解码图像,width, height 分别为摄像头预览分辨率的宽度和高度,一般来说,分辨率越高图
    *像越清晰,但解码速度越慢。由于解码算法需要处理的是原始灰度数据,而预览图像的默认格式为 
    *YCbCr_420_SP,需要转换格式才能处理, 参数"Y800"表示待转换的图像格式。
    */
    Image source = new Image(width, height, "Y800");

    /**
    *设置扫描区域范围,为了较好的识读较长的一维码,扫描框的宽度不应过小,由于预览的图像为横屏, 
    *注意扫描区域需要转换为竖屏对应的位置 
    */
    Rect cropRect = finder_view.getScanImageRect(size.height, size.width);
    //finder_view为DEMO中自定义的扫码区域控件。
    source.setCrop(cropRect.top,cropRect.left,cropRect.height(),cropRect.width());

    /*填充图像数据,data 为摄像头原始数据*/
    source.setData(data); 

    int result = scanner.scanImage(source); 

5.获取解码结果和条码类型。

 {
         
      SymbolSet syms = scanner.getResults();
          for (Symbol sym : syms) {   
            Log.i("sunmi", "码型:"+sym.getSymbolName());//条码类型,如“EAN-8”
            Log.i("sunmi","结果:"+sym.getResult())//获取解码结果字符串,这里就是要获取的结果
            
            }

  }       

so更新记录

历史资源文件

版本更新内容
1.3.0 1.去掉libiconv.so,即解码库只有一个so文件libsunmiscan.so(包括32,64位平台)
2.解决内存泄漏问题
3.增加点阵打印QR码识读,开启方式scanner.setConfig(Symbol.QRCODE, Config.ENABLE_DPM,1);//允许识读点阵打印QR码,默认0:禁止
1.2.9 1.增加64位平台(arm64-v8a)解码库
2.去掉libscaninit.so,同时jar包去掉libscaninit.so库版本查询接口
3.jar包版本更新为1.1.1
4.修复若干由于增加64位解码库发现的兼容性问题
1.2.8 1.优化code39解析能力
2.针对银联扫码能力测试要求的部分修改,该版本已通过银联扫码能力测试和最佳实践测试
3. 增加ISBN10 ISBN13输出使能控制
1.2.6 1.增加解码结果字节模式输出接口
2.解决倾斜角度扫一维码偶现程序异常的问题
1.2.5 1. 增加解码结果字节模式输出接口
2.优化一维码定位算法和解码时间
1.2.4 1.解决码字符中有不可以显示字符导致的解码结果字符解析异常的问题
2.其他优化
1.2.2 1. 优化打开相册读取高分辨率的图片解码慢的问题
2.解决使用Android7.0后硬件管家的扫码成功后没有结果返回的问题,
1.2.1 1.新增支持的二维码DataMatrix和Aztec(默认关闭,需要设置开启识读)
2.支持识读缺损一个定位符的QR码的识读
3.解决因开启多码同时识读时的bug
1.1.0 1.一维码可360度识读,之前的版本只在接近水平垂直方向识读
2.QR码识别速度提升一倍
3.解决QR码混合编码(中英文等)译码出现的bug
4.解决PDF417测试出现的潜在内存异常风险(如除0,内存溢出等)
1.0.1 1.增加了二维码PDF417的解码算法(默认关闭,需要设置开启识读)
2.减少了部分一维码的误码率
3.对于比较长的一维码可以竖屏全屏读取
4.对于上下并排的多个一维码,优先读取中间的条码
5.一维码和QR码支持反色和镜像解码,PDF417支持镜像解码
6.增加了解码库版本查询功能
1.0.0基础版本, 支持识读二维码QR和10多种主流一维码

更多说明

商米扫码SDK目前支持的扫码类型包括如下:

  • 一维码: EAN-8, EAN-13, UPC-A, UPC-E, Codabar, Code39, Code93, Code128, ISBN10, ISBN13, DataBar, DataBar Expanded, Interleaved 2 of 5
  • 二维码: QR Code , PDF417,DataMatrix,AZTEC

商米打印机驱动

简介

商米的V1、P1、T1内置了热敏打印机,允许App直接打印热敏小票出来。商米产品的内置打印机一共有2种规格:

  • 80mm宽,带切刀。兼容58mm,H10、T1搭载了这种打印机
  • 58mm宽,不带切刀。V1、V2搭载了这种打印机

App如何调用内置热敏打印机

App开发者可以使用3种方式调用内置热敏打印机:

1.通过蓝牙连接打印机
2.通过AIDL连接打印机
3.通过JS桥调用打印机

下面以V1设备为例,介绍两种调用方式,该方案也适用于P1、T1设备。

一、通过蓝牙连接调用

在V1的蓝牙设备列表中可以看到一个已经配对,且永远存在的蓝牙设备“ InnerPrinter”,这是由操作系统虚拟出来的打印机设备,实际并不存在,但它的通信方式与外置的蓝牙打印机几乎完全相同,是用来方便已经熟悉ESC/POS指令的开发者适配V1,这样他们就无需修改代码、快速迁移打印代码到V1了。

因此,如果开发者已经熟悉ESC/POS指令,甚至拥有的App已经针对ESC/POS指令完成了开发,或期望打印方法保持通用性,不要高度依赖商米设备的特征,则建议使用蓝牙连接方式。

如果还不熟悉ESC/POS指令,可以 点击这里 下载《ESC/POS指令说明》了解V1支持的指令集。

通过连接InnerPrinter虚拟打印机来完成打印的原理是:

1.与该蓝牙设备建立连接
2.将指令和文本内容拼接转码为Bytes
3.发送给InnerPrinter
4.底层打印服务驱动打印设备完成打印

关于如何连接“InnerPrinter”,开发者可以在网上搜索蓝牙连接的相关基础教程,为方便阅读,以下简要地说明了蓝牙设备连接的开发代码,便于快速上手。

以下效果为蓝牙打印Demo App打印的效果,点击这里下载Demo Apk及源码

1

其中包含5个关键操作:

1.在App的项目中添加蓝牙权限声明

 2.获取蓝牙设备BluetoothAdapter

  BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null && !mBluetoothAdapter.isEnabled()) {
       Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);  
       startActivityForResult(intent, REQUEST_ENABLE_BT);
    }

 3.获取虚拟蓝牙设备

  String innerprinter_address = "00:11:22:33:44:55";
    BluetoothDevice innerprinter_device = null;
    Set devices = mBluetoothAdapter.getBondedDevices();
    for(BluetoothDevice device : devices){
        if(device.getAddress().equals(innerprinter_address)){
              innerprinter_device = device;
         }
     }

 4.获取蓝牙套接字 BluetoothSocket

 UUID PRINTER_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
   BluetoothSocket mSocket = 
   innerprinter_device.createRfcommSocketToServiceRecord(PRINTER_UUID);

 5 .将已经拼接完的打印指令Bytes,通过sendData(byte [] bytes)方法发送给InnerPrinter

  OutputStream mOut = mSocket.getOutputStream();
   private void sendData(byte[] bytes) {
          if (mOut != null) {
              try {
                  mOut.write(bytes, 0, bytes.length);
                  mOut.flush();
              } catch (IOException e) {
                  Log.e("TAG", e.getMessage());
              } finally {
                 try {
                      mOut.close();
                  } catch (IOException e) { }
              }
          }
    }

 第5个关键操作中的byte[] bytes参数为待打印的小票内容,是通过“GBK”编码将文字转为Byte与指令Byte拼接在一起的内容。

 58mm宽打印机的每行含384个像素点,每个中文字符默认宽度为24像素,每个英文字符默认宽度为12像素,所以一行最多可以打印出16个中文字符或32个英文字符,每行打印后会自动换行,当然,你也可以通过打印“ ”字符强制换行。

 每个行的默认高度是30像素,你可以通过指令设置倍高倍宽来控制字的大小,通过对齐指令来控制每行的文字位置。

 请参考《ESC/POS指令说明》了解更多的控制指令细节。

 注意:可能会出现多个应用同时发起打印请求的问题,我们建议您如果通过虚拟蓝牙打印,打印数据最好一次性发送。

二、通过AIDL调用打印机

 背景

小票打印机的发明者是EPSON,ESC/POS指令就是由EPSON设计、定义的,在早在Android之前就已经出现,在互联网普及之前就已经广泛应用于餐饮、零售行业,至今有15年以上的历史了。

 因此指令中的数据处理方式比较底层,一些经历过传统软件开发的开发者可能在这些指令集上有自己封装和积累,但对于新兴的互联网开发者来说,无论是打印文字、图片、调整格式都会显得非常繁琐,需要投入一定的成本周期来适应。

 因此,在V1的开发者还可以通过使用Android标准的AIDL连接内置打印服务,调用打印机。

 AIDL是Android Interface Definition language的缩写,它是一种Android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信接口。关于AIDL的使用方法,请开发者查看网上相关的文档查看相关说明。

 当然开发者也可以暂时跳过AIDL的原理,通过下文简短的说明及示例代码,快速了解调用方式。

 教程

首先需要下载AIDL文件及Demo,压缩文档中含有2部分内容:


1.Demo是我们准备的打印示例工程源码,以下效果为通过Demo打印的小票,T1。
2.AIDL文件是所有打印服务暴露的接口方法清单,需要整合到自己App源码项目中。

 以下为Demo App中具体的调用关键操作解读:

 1.在自己项目中添加资源文件中附带的AIDL文件:
      (1)在src下建立woyou.aidlservice.jiuiv5包

      (2)加入IWoyouService.aidl、ICallback.aidl2个文件。

 2.在控制打印的代码类中实现ServiceConnection。

  private ServiceConnection connService = new ServiceConnection() {
        @Override
        public void onServiceDisconnected(ComponentName name) {
                woyouService = null;
            }
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
                woyouService = IWoyouService.Stub.asInterface(service);
                setButtonEnable(true);
           }
        };  

 3.调用ApplicationContext.bindService(),并在ServiceConnection实现中进行传递。

 注意:bindservice是非阻塞调用,意味着调用完成后并没有立即绑定成功,必须以serviceConnected为准。

 Intent intent=new Intent(); 
 intent.setPackage("woyou.aidlservice.jiuiv5"); 
 intent.setAction("woyou.aidlservice.jiuiv5.IWoyouService"); 
 startService(intent); bindService(intent, connService, 
 Context.BIND_AUTO_CREATE);

 4.在ServiceConnection.onServiceConnected()实现中,你会接收一个IBinder实例(被调用的Service). 调用 IWoyouService.Stub.asInterface(service)将参数转换为IWoyouService类型。

 5.现在就可以调用IWoyouService接口中定义的各种方法进行打印了。这一步你总要检测到RemoteException异常,该异常在连接断开时被抛出,这里需要做处理。它只会被远程方法抛出。接口方法中ICallback可以传递null。断开连接时调用接口实例中的 ApplicationContext.unbindService() 。

 IWoyouService中封装了打印很多种数据的方法和控制格式的方法如:

 woyouService.printText("this is a text",callback); 
 woyouService.printTextWithFont("this is a text with font","",36,callback); 
 woyouService.setFontSize(24,callback); 
 woyouservice.printColumnsText("print a line of a table!",12,0,callback);

 该接口同时也包含打印ESC/POS拼装的数据的方法:

 woyouService.sendRAWData(bytes,callback);

 注意:可能会出现多个应用同时发起打印请求的问题,您如果通过AIDL打印,我们建议您使用事物打印。

三、通过HTML中的JS调用打印机

通过JS调用打印机本质上是通过JS桥在HTML中操作android原生的代码实现打印,通过蓝牙打印或者AIDL的方式实现打印。我们给开发者准备了一个 Demo,在Demo中,通过点击HTML中的一个按钮,调用在java中写好的一个方法,让后通过AIDL的方式调用打印机打印一个自检小票。

以下为具体的调用流程:

 1.在HTML中定义android中要使用的方法。

  document.getElementsByTagName('a')[0].addEventListener('click', function(){
          
          var a = "wellcome to sunmi";
                  javascript:lee.funAndroid(a);   
          return false;
      }, false);      

 2.初始化WebView.

    WebView mWebView = (WebView) findViewById(R.id.wv_view);
    // 设置编码
    mWebView.getSettings().setDefaultTextEncodingName("utf-8");
    // 支持js
    mWebView.getSettings().setJavaScriptEnabled(true);
    mWebView.setWebChromeClient(new WebChromeClient());

 初始化打印服务

Intent intent = new Intent();
intent.setPackage("woyou.aidlservice.jiuiv5");
intent.setAction("woyou.aidlservice.jiuiv5.IWoyouService");
startService(intent);//Start printer service
bindService(intent, connService, Context.BIND_AUTO_CREATE);

 3.给WebView添加监听,在onPageFinished 回调方法中调用WebView.addJavascriptInterface(new JsObject(), ‘lee’);在JsObject类中通过 @JavascriptInterface定义在HTML中指定要操作的方法,在方法中通过调用AIDL打印方式打印一张小票。

 mWebView.setWebViewClient(new WebViewClientDemo());//添加一个页面相应监听类 class WebViewClientDemo extends WebViewClient {      @Override      public boolean shouldOverrideUrlLoading(WebView view, String url) {         // 当打开新链接时,使用当前的 WebView,不会使用系统其他浏览器          view.loadUrl(url);          return true;        }       @Override       public void onPageFinished(WebView view, String url) {       super.onPageFinished(view, url);          /**           * 注册JavascriptInterface,其中"lee"的名字随便取,如果你用"lee",那么在html中只要用  lee.方法名()           * 即可调用MyJavascriptInterface里的同名方法,参数也要一致           */            mWebView.addJavascriptInterface(new JsObject(), "lee");                 }         }                           class JsObject {                 @JavascriptInterface                 public void funAndroid(final String i) {                         Toast.makeText(getApplicationContext(), "calling the local method funAndroid by JS. " + i,                         Toast.LENGTH_SHORT).show();                                                          try {                                 woyouService.printerSelfChecking(callback);//Using AIDL to print something.                          } catch (RemoteException e) {                                 e.printStackTrace();                         }                 }         } 

 4.加载HTML文件,在点击HTML中的按钮的时候就会打印一张小票了

// 载入包含js的html
mWebView.loadData("", "text/html", null);
mWebView.loadUrl("file:///android_asset/test.html");//这里是您业务html所在的网页

 关于通过jar包调用打印机

 由于我们打印服务的升级,这个方式的调用在打印服务版本2.0.0以后都不再兼容,如果您已经使用了该方式进行打印 ,我们建议通过获取打印机版本号判断是否大于或等于’2.0.0’来兼容不同版本的打印服务,您可能会用到如下类似的代码:

    ... //绑定打印服务成功后得到一个IWoyouService类型的变量woyouService
    String version=woyouService.getServiceVersion(); //获取到打印服务的版本号
    if(version.compareTo("2.0.0")>=0){  //判断是否是新的打印服务
      ... //AIDL的方式调用打印服务
    }else{
      ... //依旧是通过jar包来调用打印服务
    }

T1副屏内置副显程序对接

 

T1 接入副屏客显程序说明

更新内容

2017-12-28 (适用版本:主屏(T1-host): V1.14.22、7寸副屏(sub7): V1.11.3、14寸副屏(sub14): V1.9.11)

  • 新增14寸副屏支持轮播视频;
  • 新增14寸副屏支持视频续播;
  • 新增7寸副屏支持幻灯片;
  • 新增7寸副屏支持单个视频;
  • 新增7寸副屏支持视频轮播;
  • 新增7寸副屏支持视频续播;
  • 新增判断副屏尺寸的方法;
  • 新增管理副屏缓存文件的方法;
  • DS_Lib库更新至1.0.16;

2018-01-15

  • 更新下载资源;

                                                                                                                                                                                                                                       

一、简介

T1双屏机器有三种组合:主机、主机+7寸副屏、主机+14寸

初始化配置

副屏。主副屏都是运行SUNMI OS定制系统,通过商米已封装好的接口实现通信。

主屏主要用来运行业务APP,例如:收银系统。副屏主要面向顾客显示结算、广告内容。开发者有两种方式实现副屏显示:

  • T1副屏系统内置了默认副屏显示APP,内置多个常用模板。开发者仅需参照文中模板的实现代码,即可实现副屏内容显示;
  • 开发自己副屏显示APP,需要自行处理数据的收发、内容显示等动作;

如果您没有强烈的自定义需求,商米建议您使用默认副屏显示APP,可以节省很多的研发成本。

本文将对如何接入内置副屏客显程序进行说明。

二、如何调试应用

由于主副屏是通过USB通信的,当主/副屏插入USB线时,主副屏的连接会断开,导致无法调试设备。

商米提供的解决方案是:将开发电脑与将要调试的T1主机处于同一个局域网络环境下,通过网络对设备进行调试。

如何调试:

1、开启USB调试,及调试权限;

    操作说明

2、通过网络对设备进行ADB调试;

    操作说明

三、如何接入副屏客显程序

(强调:Sunmi OS是基于Android6.0定制,Android6.0+ 要求部分敏感权限需要动态申请)

商米默认副屏APP提供了多个显示模板,开发者仅需要在自己的主屏业务APP实现控制副屏输出的代码,向默认副屏APP发送正确格式的数据即可实现副屏显示。

1、初始化配置

对接7寸或14寸默认副屏APP的初始化流程一样。

步骤1:

    下载DemoApp资源文件。

步骤2:

    参照DemoApp源码,直接在Android Studio的app module下的build.gradle文件中声明以下代码:

 dependencies {
    compile 'com.sunmi:DS_Lib:1.0.15'   //商米提供的lib库,包含已封装好的接口
    compile 'compile 'com.alibaba:fastjson:1.1.67.android''  //fastjson任意版本
}

步骤3:

    在清单文件AndroidMainfest.xml的节点下配置以下声明:


  ....
  "sunmi.ds.MsgReceiver">  //接收数据的广播
     
         "com.sunmi.hcservice">
         "com.sunmi.hcservice.status">
      
  

步骤4:

    在适当的位置初始化SDK代码,可以参考DSC_Demo中的实现代码。

DSKernel mDSKernel =DSKernel.newInstance();
mDSKernel.init(context, mConnCallback);    //绑定服务的回调
mDSKernel.addReceiveCallback(mReceiveCallback);  //双屏通信接收数据回调

接下来可以参考DemoApp源码,实现套用默认副屏APP内置模板的内容显示。

混淆ds_lib:

-keep interface sunmi.ds.**{ *; }

-keep class sunmi.ds.**{ *; }

除此之外,还需要混淆greendao以及fastjson,具体混淆规则以greendao和fastjson官方提供为准

2、副屏客显程序内置模板

7寸与14寸副屏支持的模板对比:

 模板7寸 14寸 
 文字 √ √
 文字+二维码 √ √
 单张图片 √ √
 幻灯片 √ √
 单个视频 √ √
 视频轮播 √ √
 清单 × √
 清单+单张图片 × √
 清单+幻灯片 × √
 清单+单个视频  × √
 清单+视频轮播 × √

2.1、两行文字

png

实现代码:

JSONObject json = new JSONObject();
json.put("title", title);//title为上面一行的标题内容
json.put("content", content);//content为下面一行的内容
String jsonStr = json.toString();
//构建DataPacket类
DataPacket packet = UPacketFactory.buildShowText(DSKernel.getDSDPackageName(), jsonStr, callback);//第一个参数是接收数据的副显应用的包名,这里参照Demo就可以,第二个参数是要显示的内容字符串,第三个参数为结果回调。

mDSKernel.sendData(packet);//调用sendData方法发送文本

2.2、二维码+文字

2

实现代码:

JSONObject json = new JSONObject();
try {
    json.put("title", "微信支付");
    json.put("content", "10.00");
} catch (JSONException e) {
    e.printStackTrace();
}

mDSKernel.sendFile(DSKernel.getDSDPackageName(), json.toString(), Environment.getExternalStorageDirectory().getPath() + "/qrcode.png", new ISendCallback() {
    @Override
    public void onSendSuccess(long l) {
        //显示图片
        try {
            JSONObject json = new JSONObject();
            json.put("dataModel", "QRCODE");
            json.put("data", "default");
            mDSKernel.sendCMD(SF.DSD_PACKNAME, json.toString(), l, null);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onSendFail(int i, String s) {      
    }

    @Override
    public void onSendProcess(long l, long l1) {
    }
});

2.3、单张图片

3

实现代码:

mDSKernel.sendFile(DSKernel.getDSDPackageName(), Environment.getExternalStorageDirectory().getPath() + "/img_01.png", new ISendCallback() {

        @Override
        public void onSendSuccess(long taskId) {
            showPicture(taskId);
        }

        @Override
        public void onSendFail(int errorId, String errorInfo) {
        }

        @Override
        public void onSendProcess(long totle, long sended) {
        }
    });
}

/**
 * 展示单张图片
 *
 * @param taskId
 */

private void showPicture(long taskId) {
    //显示图片
    try {
        JSONObject json = new JSONObject();
        json.put("dataModel", "SHOW_IMG_WELCOME");
        json.put("data", "default");
        mDSKernel.sendCMD(DSKernel.getDSDPackageName(), json.toString(), taskId, null);
    } catch (JSONException e) {
        e.printStackTrace();
    }
}

2.4、幻灯片

4

实现代码:

JSONObject json = new JSONObject();
json.put("rotation_time",5000); //幻灯片的切换时间,用毫秒计算,如果不传默认是10000毫秒
List pathList = new ArrayList<>();
pathList.add("/sdcard/img1.png");
pathList.add("/sdcard/img2.png");
...
mDSKernel.sendFiles(DSKernel.getDSDPackageName(), json.toString(), 
pathList, new ISendFilesCallback() {
    public void onAllSendSuccess(long fileId) {
        show(fileId);
    }
    public void onSendSuccess(final String s,final long l) {}
    public void onSendFaile(int errorId, String errorInfo) {}
    public void onSendFileFaile(String path, int errorId, String errorInfo){}
    public void onSendProcess(String path, long total, long sended) {}
});

private void show(long fileId) {
    String json = UPacketFactory.createJson(DataModel.IMAGES,"");
    mDSKernel.sendCMD(DSKernel.getDSDPackageName(),json,fileId,null);
}

2.5、单个视频

5

实现代码:

mDSKernel.sendFile(DSKernel.getDSDPackageName(), Environment.getExternalStorageDirectory().getPath() + "/video_01.mp4", new sunmi.ds.callback.ISendCallback() {
        @Override
        public void onSendSuccess(long l) {         
            playvideo(l);         
        }

        @Override
        public void onSendFail(int i, String s) {
            Log.d("highsixty", "发送单个文件视频文件失败 ------------>" + s);          
        }

        @Override
        public void onSendProcess(final long l, final long l1) {
        }
    });
}

private void playvideo(long taskID) {  
    String json = UPacketFactory.createJson(DataModel.VIDEO, "true"); //"true"视频续播;false视频从头播放
    mDSKernel.sendCMD(DSKernel.getDSDPackageName(), json, taskID, null);
}

2.6、视频轮播

6

实现代码:

/**
 * 发送多视频
 */

private void sendVideos() {
    //请对文件是否存在做判断
    List files = new ArrayList<>();
    files.add(Environment.getExternalStorageDirectory().getPath() + "/video_01.mp4");
    files.add(Environment.getExternalStorageDirectory().getPath() + "/video_02.mp4");
    files.add(Environment.getExternalStorageDirectory().getPath() + "/video_03.mp4");
    mDSKernel.sendFiles(DSKernel.getDSDPackageName(), "", files, new ISendFilesCallback() {

        @Override
        public void onAllSendSuccess(long fileid) {
            playvideos(fileid);
        }

        @Override
        public void onSendSuccess(String path, long taskId) {           
        }

        @Override
        public void onSendFaile(int errorId, String errorInfo) {           
        }

        @Override
        public void onSendFileFaile(String path, int errorId, String errorInfo) {            
        }

        @Override
        public void onSendProcess(String path, long totle, long sended) {
        }
    });
}

private void playvideos(long taskID) {
    Log.d(TAG, "playvideos: ------------>" + taskID);
    String json = UPacketFactory.createJson(DataModel.VIDEOS, "true"); //第二个参数true:视频续播 false:视频从头播放
    mDSKernel.sendCMD(DSKernel.getDSDPackageName(), json, taskID, null);
}

2.7、清单

7

实现代码:

{
        "title": "三米奶茶店欢迎您", 
        "head": {
            "param1": "序列号",
            "param2": "商品名",
            "param3": "单价",
            "param4": "数量",
            "param5": "小结"
        },
        "list": [
            {
              "param1": "1",
              "param2": "华夫饼",
              "param3": "10.00",
              "param4": "1",
             "param5":"10.00"
            },
            {
              "param1": "1",
              "param2": "吞拿鱼华夫饼",
              "param3": "12.00",
              "param4": "1",
             "param5":"12.00"
            }
            ... ...//这里是相同格式的数据
        ],
        "KVPList": [
            {
                "name": "收款",
                "value": "¥40.00"
            },
            {
                "name": "优惠",
                "value": "¥3.00"
            },
           {
                "name": "找零",
                "value": "¥3.00"
            },
           {
                "name": "实收",
                "value": "¥37.00"
            }
        ]
}

/**
*JSON数据格式说明:
*1:title字段为标题
*2:head字段是表头字段
*3:list是商品列表,表头和表的字段数量要一致 
*4:KVPList为结算键值对列表
*/

/**
*规则:
*当显示图文混合时:head的params赋值个数最小1个最大4个;list中每个元素的params赋值个数最*小1个最大4个;KVPList的size
*最小1个最大4个。
*/ 

/**
*receiverPackageName 接收数据的副屏显示app的包名DataType.DATA 
*DataType.DATA 
*DataModel.TEXT 
*jsonStr 显示的数据内容,具体格式参见下面的框
*callback 回调
*/
DataPacket pack = buildPack(receiverPackageName, DataType.DATA, DataModel.TEXT, jsonStr, callback);
mDSKernel.sendData(pack);

2.8、清单+单张图片

8

实现代码:

 {
        "title": "三米奶茶店欢迎您", 
        "head": {
            "param1": "序列号",
            "param2": "商品名",
            "param3": "单价",
            "param4": "数量",
            "param5": "小结"
        },
        "list": [
            {
              "param1": "1",
              "param2": "华夫饼",
              "param3": "10.00",
              "param4": "1",
             "param5":"10.00"
            },
            {
              "param1": "1",
              "param2": "吞拿鱼华夫饼",
              "param3": "12.00",
              "param4": "1",
             "param5":"12.00"
            }
            ... ...//这里是相同格式的数据
        ],
        "KVPList": [
            {
                "name": "收款",
                "value": "¥40.00"
            },
            {
                "name": "优惠",
                "value": "¥3.00"
            },
           {
                "name": "找零",
                "value": "¥3.00"
            },
           {
                "name": "实收",
                "value": "¥37.00"
            }
        ]
}

/**
*JSON数据格式说明:
*1:title字段为标题
*2:head字段是表头字段
*3:list是商品列表,表头和表的字段数量要一致 
*4:KVPList为结算键值对列表
*/

/**
*规则:
*当显示图文混合时:head的params赋值个数最小1个最大4个;list中每个元素的params赋值个数最*小1个最大4个;KVPList的size
*最小1个最大4个。
*/ 

String filePath = "xxx/img.png";//显示的图片路径
mDSKernel.sendFile(DSKernel.getDSDPackageName(), filePath, new ISendCallback() {
    public void onSendSuccess(long fileId) {
        show(fileId, jsonStr);//图片发送成功,显示文字内容
    } 
    public void onSendFail(int errorId, String errorInfo) {}
    public void onSendProcess(long total, long sended) {}
});

void show(long fileId, String jsonStr){
    jsonStr = UPacketFactory.createJson(DataModel.SHOW_IMG_LIST, jsonStr);
    //第一个参数DataModel.SHOW_IMG_LIST为显示布局模式,jsonStr为要显示的内容字符
    mDSKernel.sendCMD(DSKernel.getDSDPackageName(), jsonStr, fileId,null);
}

2.9、清单+幻灯片

8

实现代码:

{
        "title": "三米奶茶店欢迎您", 
        "head": {
            "param1": "序列号",
            "param2": "商品名",
            "param3": "单价",
            "param4": "数量",
            "param5": "小结"
        },
        "list": [
            {
              "param1": "1",
              "param2": "华夫饼",
              "param3": "10.00",
              "param4": "1",
             "param5":"10.00"
            },
            {
              "param1": "1",
              "param2": "吞拿鱼华夫饼",
              "param3": "12.00",
              "param4": "1",
             "param5":"12.00"
            }
            ... ...//这里是相同格式的数据
        ],
        "KVPList": [
            {
                "name": "收款",
                "value": "¥40.00"
            },
            {
                "name": "优惠",
                "value": "¥3.00"
            },
           {
                "name": "找零",
                "value": "¥3.00"
            },
           {
                "name": "实收",
                "value": "¥37.00"
            }
        ]
}

/**
*JSON数据格式说明:
*1:title字段为标题
*2:head字段是表头字段
*3:list是商品列表,表头和表的字段数量要一致 
*4:KVPList为结算键值对列表
*/

/**
*规则:
*当显示图文混合时:head的params赋值个数最小1个最大4个;list中每个元素的params赋值个数最*小1个最大4个;KVPList的size
*最小1个最大4个。
*/ 

List paths = new ArrayList<>();
paths.add(Environment.getExternalStorageDirectory().getPath() + "/sunmi1.png");
paths.add(Environment.getExternalStorageDirectory().getPath() + "/sunmi2.png");
paths.add(Environment.getExternalStorageDirectory().getPath() + "/sunmi3.png");
paths.add(Environment.getExternalStorageDirectory().getPath() + "/sunmi4.png");

mDSKernel.sendFiles(DSKernel.getDSDPackageName(), "", paths, new ISendFilesCallback() {
    @Override
    public void onAllSendSuccess(long fileId) {               
      sendImgsListCMD(fileId,json);                           
    }

    @Override
    public void onSendSuccess(String path, long taskId) {}
    @Override
    public void onSendFaile(int errorId, String errorInfo) {}
    @Override
    public void onSendFileFaile(String path, int errorId, String errorInfo) {}
    @Override
    public void onSendProcess(String path, long totle, long sended) {}
    });
    
    void sendImgsListCMD(long fileId, String jsonStr){   
      jsonStr= UPacketFactory.createJson(DataModel.SHOW_IMGS_LIST, json);
        mDSKernel.sendCMD(DSKernel.getDSDPackageName(), jsonStr, fileId,null);
    }

2.10、清单+单个视频

10

实现代码:

mDSKernel.sendFile(DSKernel.getDSDPackageName(), Environment.getExternalStorageDirectory().getPath() + "/video_01.mp4", new sunmi.ds.callback.ISendCallback() {
    @Override
    public void onSendSuccess(long l) {     
        playVideoMenu(l);      
    }

    @Override
    public void onSendFail(int i, String s) {
        Log.d("highsixty", "发送单个文件视频和清单文件失败 ------------>" + s);      
    }

    @Override
    public void onSendProcess(final long l, final long l1) {
    }
});

private void playVideoMenu(long fileId) {
    try {
        JSONObject data = new JSONObject();
        data.put("title", "商米奶茶店收银");
        JSONObject head = new JSONObject();
        head.put("param1", "序号");
        head.put("param2", "商品名");
        head.put("param3", "单价");
        data.put("head", head);
        data.put("flag", "true");    //设置为true,则视频将接着之前的进度播放。设置为false,则视频将从头播放。
        JSONArray list = new JSONArray();
        for (int i = 1; i < 11; i++) {
            JSONObject listItem = new JSONObject();
            listItem.put("param1", "" + i);
            listItem.put("param2", products.get(i - 1));
            listItem.put("param3", prices.get(i - 1));
            list.put(listItem);
        }

        data.put("list", list);
        JSONArray KVPList = new JSONArray();
        JSONObject KVPListOne = new JSONObject();
        KVPListOne.put("name", "总计 ");
        KVPListOne.put("value", "132.00");
        JSONObject KVPListTwo = new JSONObject();
        KVPListTwo.put("name", "优惠 ");
        KVPListTwo.put("value", "12.00");
        JSONObject KVPListThree = new JSONObject();
        KVPListThree.put("name", "数量 ");
        KVPListThree.put("value", "10");
        JSONObject KVPListFour = new JSONObject();
        KVPListFour.put("name", "应收 ");
        KVPListFour.put("value", "120.00");
        KVPList.put(0, KVPListOne);
        KVPList.put(1, KVPListTwo);
        KVPList.put(2, KVPListThree);
        KVPList.put(3, KVPListFour);
        data.put("KVPList", KVPList);
        Log.d("HHHH", "onClick: ---------->" + data.toString());
        String json = UPacketFactory.createJson(DataModel.SHOW_VIDEO_LIST, data.toString());
        mDSKernel.sendCMD(DSKernel.getDSDPackageName(), json, fileId, null);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2.11、清单+视频轮播

10

实现代码:

/**
 * 左视频右清单文件
 */
private void sendMenuVideos() {
    Log.d(TAG, "sendMenuVideos: ------------->");
    List files = new ArrayList<>();
    files.add(Environment.getExternalStorageDirectory().getPath() + "/video_01.mp4");
    files.add(Environment.getExternalStorageDirectory().getPath() + "/video_02.mp4");
    files.add(Environment.getExternalStorageDirectory().getPath() + "/video_03.mp4");

    mDSKernel.sendFiles(DSKernel.getDSDPackageName(), "", files, new ISendFilesCallback() {

        @Override
        public void onAllSendSuccess(long fileid) {          
            playMenuVideos(fileid);
        }

        @Override
        public void onSendSuccess(String path, long taskId) {            
        }

        @Override
        public void onSendFaile(int errorId, String errorInfo) {           
        }

        @Override
        public void onSendFileFaile(String path, int errorId, String errorInfo) {           
        }

        @Override
        public void onSendProcess(String path, long totle, long sended) {
        }
    });
}

private void playMenuVideos(long taskID) {    
    try {
        JSONObject data = new JSONObject();
        data.put("title", "商米奶茶店收银");
        JSONObject head = new JSONObject();
        head.put("param1", "序号");
        head.put("param2", "商品名");
        head.put("param3", "单价");
        data.put("head", head);
        data.put("flag", "true"); //“true"视频续播;false 从头播放
        JSONArray list = new JSONArray();
        for (int i = 1; i < 11; i++) {
            JSONObject listItem = new JSONObject();
            listItem.put("param1", "" + i);
            listItem.put("param2", products.get(i - 1));
            listItem.put("param3", prices.get(i - 1));
            list.put(listItem);
        }
        data.put("list", list);
        JSONArray KVPList = new JSONArray();
        JSONObject KVPListOne = new JSONObject();
        KVPListOne.put("name", "总计 ");
        KVPListOne.put("value", "132.00");
        JSONObject KVPListTwo = new JSONObject();
        KVPListTwo.put("name", "优惠 ");
        KVPListTwo.put("value", "12.00");
        JSONObject KVPListThree = new JSONObject();
        KVPListThree.put("name", "数量 ");
        KVPListThree.put("value", "10");
        JSONObject KVPListFour = new JSONObject();
        KVPListFour.put("name", "应收 ");
        KVPListFour.put("value", "120.00");
        KVPList.put(0, KVPListOne);
        KVPList.put(1, KVPListTwo);
        KVPList.put(2, KVPListThree);
        KVPList.put(3, KVPListFour);
        data.put("KVPList", KVPList);
        Log.d("HHHH", "onClick: ---------->" + data.toString());
        String json = UPacketFactory.createJson(DataModel.MENUVIDEOS, data.toString());
        mDSKernel.sendCMD(DSKernel.getDSDPackageName(), json, taskID, null);
    } catch (JSONException e) {
        e.printStackTrace();
    }
}

3、判断副屏的尺寸

主屏业务应用可通过获取副屏Model值判断副屏尺寸,实现根据副屏尺寸输出相应的显示内容的业务逻辑。

3.1、从主屏获取副屏Model值

实现代码:

String subModel = Settings.Global.getString(getContentResolver(), "sunmi_sub_model")

//返回值:“t1sub14”对应14寸副屏,“t1sub7”对应7寸副屏。返回其它值表示获取失败。

3.2、从副屏获取副屏Model值

实现代码:

//从副屏获取副屏model
case R.id.btn_get_sub_model:
JSONObject jsonObject2 = new JSONObject();
try {
  jsonObject2.put("dataModel", "GET_MODEL");
  jsonObject2.put("data", "");
} catch (JSONException e) {
  e.printStackTrace();
}

DataPacket p2 = new DataPacket.Builder(DSData.DataType.CMD).recPackName(SF.SUNMI_DSD_PACKNAME).data(jsonObject2.toString())
.addCallback(new ISendCallback() {
  @Override
  public void onSendSuccess(long taskId) {
  }    
  
  @Override                         
  public void onSendFail(int errorId, String errorInfo) {           
  }     
                       
  @Override
  public void onSendProcess(long totle, long sended) {              
  }
}).build();

mDSKernel.sendQuery(p2, new QueryCallback() {                 
  @Override                 
  public void onReceiveData(final DSData data) {                      Log.d("highsixty", "onReceiveData: ------------>" + data.data); runOnUiThread(new Runnable() {                         
      @Override                         
      public void run() {                             
        Toast.makeText(MainActivity.this, "从副屏获取副屏model-->" + data.data, Toast.LENGTH_LONG).show();                         
      }
    });
  }
});             
break;
//返回值:“t1sub14”对应14寸副屏,“t1sub7”对应7寸副屏。返回其它值表示获取失败。

4、清除副屏缓存

4.1、查询副屏指定缓存目录大小

实现代码:

JSONObject jsonObject = new JSONObject();
try {
    jsonObject.put("dataModel", "GETVICECACHEFILESIZE");
    jsonObject.put("data", Environment.getExternalStorageDirectory().getAbsolutePath() + "/HCService/" + getPackageName().replace(".", "_"));
} catch (JSONException e) {
    e.printStackTrace();
}

DataPacket packet = new DataPacket.Builder(DSData.DataType.CMD).recPackName(SF.SUNMI_DSD_PACKNAME).data(jsonObject.toString())
        .addCallback(null).build();
mDSKernel.sendQuery(packet, new QueryCallback() {
    @Override
    public void onReceiveData(final DSData data) {
        Log.d("highsixty", "onReceiveData: ------------>" + data.data);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this, "副屏缓存文件大小字节数为" + data.data, Toast.LENGTH_LONG).show();
            }
        });
    }
});

4.2、清除副屏指定缓存目录

实现代码:

DataPacket packet2 = UPacketFactory.remove_folders(DSKernel.getDSDPackageName(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/HCService/" + getPackageName().replace(".", "_"), new ISendCallback() {
    @Override
    public void onSendSuccess(long taskId) {
        showToast("清除缓存文件成功");
    }

    @Override
    public void onSendFail(int errorId, String errorInfo) {
        showToast("清除缓存文件失败");
    }

    @Override
    public void onSendProcess(long totle, long sended) {
    }
});

mDSKernel.sendCMD(packet2);

4.3、查询指定缓存文件是否存在

实现代码:

//说明:该方法检查fileId对应的文件在副屏是否存在,
第一个参数为之前发送文件时副屏返回的文件id,该id是所有的文件唯一的,第二个参数为结果回调
mDSKernel.checkFileExist(fileId, new ICheckFileCallback(){
  @Override
  public void onCheckFail() {}
 
  @Override
  public void onResult(boolean arg0) {
     //返回值为true是存在
     //返回值false是不存在  
      }                                    
});

4.4、清除副屏指定缓存文件

实现代码:

mDSKernel.deleteFileExist(FileID, new ICheckFileCallback() {
    @Override
    public void onCheckFail() {
        Log.d(TAG, "onCheckFail: ----------->");
    }

    @Override
    public void onResult(boolean exist) {
        Log.d(TAG, "onResult: ---------->" + exist);
    }
});

5、图片/视频显示规则

图片/视频的缩放规则:

  • 图片/视频默认居中等比例缩放,至少有两条边与显示容器边界接触;

图片/视频的建议分辨率:

  • 7寸:
  • 全屏显示图片:1024*600
  • 全屏显示视频:1024*600
  • 14寸:
  • 全屏显示图片/视频:1920*1080
  • 图片/视频+清单:1186*1080

建议文件大小:

  • 图片:≤1MB/张
  • 视频:≤5MB/个

建议幻灯片文件数量:

  • 图片:≤10张

建议轮播视频文件数量:

  • 视频:≤10个

注:因文件传送至副屏需要时间,显示文件时可能会有延时。故尽量在空闲时先将图片和视频文件缓存至副屏,当需要在副屏显示时,根据文件ID调用。

FAQ

1.App完成调试后如何大批量发布到每一台卖出去的机器?

答:平台合作伙伴通过合作伙伴平台将自己的应用上传到商米的应用库中,在我的应用市场中将该应用的自动安装功能打开即可实现。详情请查看开发者文档中的发布应用

2.如何防止别人改动自己铺出去的机器中的应用? 

答:合作伙伴可以在后台设置:1.只能通过应用市场安装应用到设备。2.需要通过手机号或邮箱在设备端获取调试权限调试设备。

3.什么是商米虚拟蓝牙打印?

答:商米基于物理链接实现的一个虚拟蓝牙链接,所有带打印的商米设备的蓝牙列表中会有一个名为’InnerPrinter’的蓝牙信号,对业务App来说和普通的蓝牙无区别,可以当成普通的蓝牙打印机。

4.你们的设备可以获取root权限吗?

答:不可以。为了设备的安全性,商米限制不能获取到设备的root权限。

5.商米的系统可以定制吗?

答:商米的优势就是,系统可控和可定制。包括系统的开机画面,系统桌面,默认启动App,系统默认安装应用都可以在合作伙伴后台设置。

6.如何认领应用?

答:当您在新建的应用时候发现如下提示,说明有合作伙伴已经上传过您的应用

如此应用确实为贵公司所有,请发邮件认领些应用。

认领邮件格式:

标题:认领应用

邮件正文:认领应用-同名应用(包名)  ,联系人,联系方式。

                  并附件中添加公司营业执照,对应软件著作权证明等

注意:这里的同名应用是指在商米平台上显示的应用名,可能与你的真实应用名不同

 

7.应用上传失败或无法上传?

答:如果你遇到解析失败等无法上传等信息,请确认APK编译打包正常,如有以上情况将会影响应用上架

1、APK编译时没有将ARM库文件包含在包文件中,此情况将导致无法正常安装。

2、APK编译时没有将应用签名打包在包文件中,此情况将导致签名异常。

3、APK编译时没有选择比上一版的版本号(versionCode),此情况将导致无法提交应用

 

 

跳转到应用商店APP详情

注:只适用于应用市场3.5或以上版本的竖屏设备

1.在应用内通过Intent打开:

public void startActivity(){

String packageName = "要打开的App的包名";

String uri = String.format("market://woyou.market/appDetail?packageName=%s",packageName);

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));

        intent.addCategory(Intent.CATEGORY_DEFAULT);

        PackageManager packageManager = getPackageManager();

        List activities = packageManager.queryIntentActivities(intent,

                PackageManager.MATCH_DEFAULT_ONLY);

        boolean isIntentSafe = activities.size() > 0;

        if (isIntentSafe) {

            startActivity(intent);

        }

    }

2.通过使用webview打开:

在xml中使用进入应用市场详情页

如果mWebView.setWebViewClient();设置了此函数,则不能通过a链接自动打开,只能通过拦截链接再使用Intent跳转。

mWebView.setWebViewClient(new WebViewClient(){

            @Override

            public boolean shouldOverrideUrlLoading(WebView view, String url) {

                Log.d(TAG, "shouldOverrideUrlLoading: " + url);

                Intent intent = null;

                try {

                    intent = Intent.parseUri(url,Intent.URI_INTENT_SCHEME);

                } catch (URISyntaxException e) {

                    e.printStackTrace();

                    return false;

                }

                startActivity(intent);

                return true;

            }

        });

3.在浏览器打开:

地址:market://woyou.market/appDetail?packageName=包名

注:只能在终端自带浏览器或者Chrome 浏览器打开,其他浏览器均打开不开

特殊代码说明

一、获取商米设备标识

商米建议通过获取到以下内容来判断是否商米设备:

1.设备的品牌名 brand(如:SUNMI)

    商米的品牌名统一为 SUNMI

2.设备的系统型号 model(如:V1-B18)

    系统型号组成为 产品型号+硬件特性+‘-’+软件特性

    其中以V、M、P、L开头为手持设备,以T、D、S开头为横屏设备(截至2017年12月) 

3.设备的ROM版本号(如:1.1.0)。

4.设备的ROM顺序号(如:128)。

可以下载Demo,仿照Demo在自己项目src下面新建android.os包(固定写法),将SystemProperties.java放入该包下,按以下方法获取指定的值:

获取brand的代码为:

    String brand = SystemProperties.get("ro.product.brand");

获取model的方法为:

    String model = SystemProperties.get("ro.product.model");

获取ROM版本号的代码为:

    String versionname = SystemProperties.get("ro.version.sunmi_versionname");

获取ROM顺序号的方法为:

    String versioncode = SystemProperties.get("ro.version.sunmi_versioncode");

 

二、获取设备的SN号

1.在AndroidManifest.xml中添加如下权限。

2.在需要的地方用以下代码获取商米SN号。

public static String getSN() {
        if (Build.VERSION.SDK_INT ›= Build.VERSION_CODES.O) {
            return Build.getSerial();
        } else
            return Build.SERIAL;
    }

三、隐藏及恢复底部导航栏

(注:商米新开发了Kiosk霸屏模式,APP不用做任何修改,仅通过云端设置即可实现隐藏状态栏、导航栏并无法通过手势唤出。   该功能在T2、K1设备上已实现,其他设备上线进展请联系技术支持400-902-1168(9:00-21:00)。   建议合作伙伴使用商米Kiosk霸屏模式,已获得更好的体验)

Android系统默认提供了隐藏系统的导航栏的方法,但对于Dialog的支持较差,导致全屏对话框打开时先弹出导航栏再隐藏(闪屏),SunmiOS针对此问题进行了修复(V1系统固件版本252后支持,T1系统固件版本132后支持)

1.Activity的全屏化

——安卓默认支持

public class MainActivity extends AppCompatActivity {
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        SystemUIUtils.setStickFullScreen(getWindow().getDecorView());
    }
}
public class SystemUIUtils {

    public static void setStickFullScreen(View view) {
        int systemUiVisibility = view.getSystemUiVisibility();
        int flags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
        systemUiVisibility |= flags;
        view.setSystemUiVisibility(systemUiVisibility);
    }

}

2.Dialog的全屏化

——原生系统下 AOSP 的Bug会导致全屏对话框打开时先弹出导航栏然后再隐藏导航栏(闪屏)。

public AlertDialog create(boolean fullscreen) {
    LayoutInflater inflater = LayoutInflater.from(context);

    final AlertDialog dialog = new AlertDialog(context,
            R.style.DialogStyle);

    if(fullscreen){
        SystemUIUtils.setStickFullScreen(dialog.getWindow().getDecorView());
    }
}

3.设置隐藏后显示导航栏

3.1.全局底部上划

——上划后底部导航栏显示4s,4s后底部导航栏隐藏

3.2.切换应用至其他APP(如APP内跳转至第三方应用、第三方APP弹窗等)

——切换至其他APP是否显示底部导航栏以第三方应用要求为准,切换至自己APP时底部导航栏消失

四、避免重复申请外设权限

当APP需要通过USB关联外设来实现业务时(比如连接USB打印机打印小票),安卓要求用户手动确认设备使用权限,来保障用户信息安全,防止木马非法入侵USB设备。

1.如何避免USB设备重新插拔同一个外设时,APP反复申请该外设权限

需要用户手动确认时,勾选“默认情况下用于该USB设备”,无法通过代码绕过该安全机制

验证DEMO

2.如何避免设备重启后,已勾选“默认情况下用于该USB设备”的APP仍反复申请同一个外设的使用权限

2.1.在APP的AndroidManifest中指定某个Activity部分中,添加如下代码


                


2.2.在该目录下创建xml文档



    

其中,class的值需要和要连接的外设类型一致,外设class参照表如下


    /**
     * USB class indicating that the class is determined on a per-interface basis.
     */
    public static final int USB_CLASS_PER_INTERFACE = 0;
    /**
     * USB class for audio devices.
     */
    public static final int USB_CLASS_AUDIO = 1;
    /**
     * USB class for communication devices.
     */
    public static final int USB_CLASS_COMM = 2;
    /**
     * USB class for human interface devices (for example, mice and keyboards).
     */
    public static final int USB_CLASS_HID = 3;
    /**
     * USB class for physical devices.
     */
    public static final int USB_CLASS_PHYSICA = 5;
    /**
     * USB class for still image devices (digital cameras).
     */
    public static final int USB_CLASS_STILL_IMAGE = 6;
    /**
     * USB class for printers.
     */
    public static final int USB_CLASS_PRINTER = 7;
    /**
     * USB class for mass storage devices.
     */
    public static final int USB_CLASS_MASS_STORAGE = 8;
    /**
     * USB class for USB hubs.
     */
    public static final int USB_CLASS_HUB = 9;
    /**
     * USB class for CDC devices (communications device class).
     */
    public static final int USB_CLASS_CDC_DATA = 0x0a;
    /**
     * USB class for content smart card devices.
     */
    public static final int USB_CLASS_CSCID = 0x0b;
    /**
     * USB class for content security devices.
     */
    public static final int USB_CLASS_CONTENT_SEC = 0x0d;
    /**
     * USB class for video devices.
     */
    public static final int USB_CLASS_VIDEO = 0x0e;
    /**
     * USB class for wireless controller devices.
     */
    public static final int USB_CLASS_WIRELESS_CONTROLLER = 0xe0;
    /**
     * USB class for wireless miscellaneous devices.
     */
    public static final int USB_CLASS_MISC = 0xef;
    /**
     * Application specific USB class.
     */
    public static final int USB_CLASS_APP_SPEC = 0xfe;
    /**
     * Vendor specific USB class.
     */
    public static final int USB_CLASS_VENDOR_SPEC = 0xff;

3.如何避免前两步后设置后,业务页面会因USB设备插入而刷新

安卓原生逻辑导致用户选择“默认情况下用于该USB设备”会导致USB设备插入时打开某个指定activity。如果要避免页面刷新导致业务中断,可以增加如下代码防止页面刷新。

 

五、如何避免自己的应用数据被清除

应用数据默认是可以通过系统设置删除的,删除后应用将恢复刚安装的状态。但是可以通过配置APP来精细管理应用数据(比如按照业务分类或时间删除数据),也可以避免重要的应用数据被删除。

Screenshot-3.png
Screenshot-2.png

在程序的manifest文件的application中加上manageSpaceActivity属性,并且指定一个Activity,这个Activity就是点击管理空间之后会跳转的那个Activity了。

  

  
    
  

PS 如果要避免数据被删除,可以创建一个自动关闭的Activity。

public class ManageSpaceActivity extends Activity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
  
        finish();  
  
    }// onCreate  
}  

七、如何避免插入usb外设导致app界面闪烁

在APP的AndroidManifest中,添加如下代码

android:configChanges="navigation|keyboardHidden|keyboard"

八、如何避免扫描枪扫出内容与实际不一致

一般情况下,文本框输入的所有内容会提交给输入法进行转换(如拼音、联想等),有的输入法经过转换后会转义部分字词导致输入内容不一致。

除了更换系统输入法为合适的输入法外,还可以利用输入法通常不对密码框进行处理的规则,调整文本框类型为“可见密码”(inputType),并且约定好可输入规范(digits)也可以避免该问题

发布应用

关于商米应用市场

每个商米合作伙伴都可以在商米官网 注册商米合作伙伴帐号,有一个自己的操作后台,原则上每一台商米的设备在卖出去的时候都会和一个合作伙伴账号绑定,商米会以合作伙伴为粒度提供部分功能和权限的控制服务。

SUNMIUI内部有一个应用市场,合作伙伴可以通过应用市场将自己的应用大规模分发到商米的设备上。部分合作伙伴可以有一个自己管理的应用市场,如果合作伙伴没有在后台打开’允许第三方应用安装'(如下图箭头所示),默认旗下用户只能通过商米应用市场为设备安装应用。

1

应用市场规则

商米为部分合作伙伴分配了不同权限的应用市场,默认合作伙伴上传的应用不用商米审核就可以出现在自己的应用市场中。商米可以审核该应用,通过审核后该应用会出现在其他合作伙伴的应用市场中,当然部分合作伙伴可以选择自己的应用不出现在其他人的应用市场,也可以选择自己应用市场不出现其他人的应用,关于权限的授予可以咨询商米客服400-902-1168(每天9:00~21:00,节日除外)

应用分发流程

应用开发完成后,上传到应用市场,用户可以在设备上的应用市场搜索到应用,如果是渠道合作伙伴可以设置应用在旗下的设备自动安装。以下讲解应用首次上传分发的流程。

1.上传应用。

在 合作伙伴后台,进入”我的应用”选项,点击创建应用

2

2.填写相关内容

点击’上传安装包’,从文件夹中选择您要上传的apk,等待上传完成后,商米后台将自动分析安装包,下图绿色箭头指示部分将会自动填写好,开发者手动填写应用介绍,应用适配的设备,应用类型等信息。

  • 上传APK安装包时如果提示包名重复,则表示此APK已经在商米应用市场上架,如果此APK是贵公司所有,请联系商米客服400-902-1168(每天9:00~21:00,节日除外),进入认领。
3

3.提交应用。

合作伙伴将剩余部分内容补充完整后,下图的’提交’按钮将由灰色不可点击变成绿色可点击状态,点击’提交’将会完成应用的上传,

4

关于灰度部署

上图中’提交’按钮上方有一个’是否灰度部署’选项,勾选后提交按钮将变成’灰度部署’项,点击后将进入灰度部署配置项中。

灰度部署是商米针对合作伙伴的实际需求提供的一项功能,每台设备在使用的时候会有一个地理理位置信息,合作伙伴在开发好自己的应用适配商米的设备后,可以根据地理位置或者SN号在部分设备上部署,只有灰度部署范围内的设备的应用市场中才会出现该应用。在小范围的灰度部署后,开发者可以将应用部署到全范围设备上。

5

设置自动安装。

如果希望上传的应用自动在旗下所有设备上安装,可以在’我的应用市场’项中设置该应用为自动安装应用(如下图箭头所示),旗下设备在收到推送通知后会自动下载安装该应用,设置自动安装后还可以设置该应用是否作为默认启动应用(如下图箭头所示),设备开机后将自动启动该应用。

6

更新应用

在应用通过商米审核后,在我的应用中列表项中会有’更新’按钮,开发者可以发布该应用的升级包。点击后进入更新应用页,更新的流程和发布流程类似,请参照上面的发布流程。

  • 上传更新包的时候如果提示签名异常,请检查APK签名是否与原来的安装包不一致。如确认是需要变更签名,则请删除应用后再创建应用
  • 上传更新包的时候如果提示包名不一致,请检查APK包名是否有变动或有没有上传错误。如确认是需要变更包名,则请删除应用后再创建应用
7