Sparky


  • 首页

  • 归档

未命名

发表于 2018-02-01

apk更新的技术:

  • 增量更新:
  • 热更新
  • 插件化
  • rn的js脚本修复apk
  • 静默安装

增量更新的流程

  1. APP检测最新版本,把版本号告诉服务器,服务器进行版本校验
  2. 如果有新版本,服务器需要对当前版本和新版本apk进行一次差分,产生patch差分文件,或者在新版本上传到服务器的时候已经差分好
  3. app在后台下载好差分文件,进行md5校验,在本地进行合并(跟/data目录下的apk文件进行合并),合并成新的apk后,提示用户安装。

Retrofit Https踩坑记录

发表于 2018-01-12

Retrofit Https踩坑记录

前言

新司机上路,坑多,本文重点是踩坑,不详细讲retrofit用法,本文不推荐使用信任所有证书的做法。

证书

分为多种格式, bks cer jks等,这里使用的是bks格式证书。

BKS 做法

1.获取BKS证书,将证书放到项目raw目录下

准备.cer文件

点击网站网址栏前的小锁按钮,选择详细信息,选择view certificate。
显示证书之后,点击详细信息,然后一直下一步,直到导出.cer文件。
1
2
3
4
5
6
7

将.cer转换为.bks

在Android应用中使用自定义证书,CER转BKS

做法:1,下载特定版本的JCE Provider包

http://pan.baidu.com/s/1c1ur13y

or

http://www.bouncycastle.org/download/bcprov-jdk15on-146.jar (现在连接失效)

2,命令行输入以下命令

keytool -importcert -v -trustcacerts -alias 位置1 \
-file 位置2 \
-keystore 位置3 -storetype BKS \
-providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath 位置4 -storepass 位置5

位置1:是个随便取的别名
位置2:cer或crt证书的全地址
位置3:生成后bks文件的位置,建议写全地址
位置4:上面下载JCE Provider包的位置
位置5:生成后证书的密码。下边获取sslsocketfactory中会用到密码

以下例子:

keytool -importcert -v -trustcacerts -alias xx -file E:\bks\xx.cer -keystore E:\bks\xx.bks -storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath E:\bks\bcprov-jdk15on-146.jar -storepass xxxxxx

成功之后会在你指定的位置生成bks文件.然后将文件放到项目raw目录下。

2.获取SSLSocketFactory

这里是https证书认证最关键的代码,一定要仔细查看。password和设置keystore的bks类型一定不要搞错。

/**
 * 获取bks文件的sslsocketfactory
 * @param context
 * @return
 */
public static SSLSocketFactory getSSLSocketFactory(Context context) {
    final String CLIENT_TRUST_PASSWORD = "123456";//信任证书密码,该证书默认密码是123456
    final String CLIENT_AGREEMENT = "TLS";//使用协议
    final String CLIENT_TRUST_KEYSTORE = "BKS";
    SSLContext sslContext = null;
    try {
        //取得SSL的SSLContext实例
        sslContext = SSLContext.getInstance(CLIENT_AGREEMENT);
        //取得TrustManagerFactory的X509密钥管理器实例
        TrustManagerFactory trustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        //取得BKS密库实例
        KeyStore tks = KeyStore.getInstance(CLIENT_TRUST_KEYSTORE);
        InputStream is = context.getResources().openRawResource(R.raw.traint);
        try {
            tks.load(is, CLIENT_TRUST_PASSWORD.toCharArray());
        } finally {
            is.close();
        }
        //初始化密钥管理器
        trustManager.init(tks);
        //初始化SSLContext
        sslContext.init(null, trustManager.getTrustManagers(), null);
    } catch (Exception e) {
        e.printStackTrace();
        Log.e("SslContextFactory", e.getMessage());
    }
    return sslContext.getSocketFactory();
}

3.配置retrofit

String baseUrl = "https://skyish-test.yunext.com";
int[] certificates = {R.raw.traint};
    String[] hostUrls = {baseUrl};
    OkHttpClient client = new okhttp3.OkHttpClient.Builder()
            .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
            .sslSocketFactory(HTTPSUtils.getSSLSocketFactory(context))
            //.hostnameVerifier(HTTPSUtils.getHostNameVerifier(hostUrls)) 
            .readTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .build();

    Retrofit retrofit = new Retrofit.Builder().baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .client(client)
            .build();

配置好retrofit之后就可以使用了。

坑1:SSLContext is not initialized

03-08 15:17:26.804 21672-21672/com.qiwo.enumlistdemo E/AndroidRuntime: FATAL EXCEPTION: main
                                                                       Process: com.qiwo.enumlistdemo, PID: 21672
                                                                       java.lang.RuntimeException: Unable to start activity ComponentInfo{com.qiwo.enumlistdemo/com.qiwo.enumlistdemo.RetrofitHttpsDemoActivity}: java.lang.IllegalStateException: SSLContext is not initialized.
                                                                           at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2650)
                                                                           at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2720)
                                                                           at android.app.ActivityThread.-wrap12(ActivityThread.java)
                                                                           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
                                                                           at android.os.Handler.dispatchMessage(Handler.java:111)
                                                                           at android.os.Looper.loop(Looper.java:207)
                                                                           at android.app.ActivityThread.main(ActivityThread.java:5917)
                                                                           at java.lang.reflect.Method.invoke(Native Method)
                                                                           at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
                                                                           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)
                                                                        Caused by: java.lang.IllegalStateException: SSLContext is not initialized.
                                                                           at com.android.org.conscrypt.OpenSSLContextImpl.engineGetSocketFactory(OpenSSLContextImpl.java:107)
                                                                           at javax.net.ssl.SSLContext.getSocketFactory(SSLContext.java:358)
                                                                           at com.qiwo.api.HTTPSUtils.getSSLSocketFactory(HTTPSUtils.java:158)
                                                                           at com.qiwo.api.DemoHttpsApi.<init>(DemoHttpsApi.java:40)
                                                                           at com.qiwo.enumlistdemo.RetrofitHttpsDemoActivity.initViewAndListener(RetrofitHttpsDemoActivity.java:37)
                                                                           at com.doudou.common.base.BaseSwipeBackAppcompatActivity.onCreate(BaseSwipeBackAppcompatActivity.java:68)
                                                                           at android.app.Activity.performCreate(Activity.java:6307)
                                                                           at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1113)
                                                                           at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2603)
                                                                           at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2720) 
                                                                           at android.app.ActivityThread.-wrap12(ActivityThread.java) 
                                                                           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567) 
                                                                           at android.os.Handler.dispatchMessage(Handler.java:111) 
                                                                           at android.os.Looper.loop(Looper.java:207) 
                                                                           at android.app.ActivityThread.main(ActivityThread.java:5917) 
                                                                           at java.lang.reflect.Method.invoke(Native Method) 
                                                                           at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789) 
                                                                           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679) 

原因:

  1. 证书和证书密码不匹配。
  2. 使用了错误的证书。证书类型不对。应该使用bks类型证书加载的确实cer类型的

解决方法:

CLIENT_TRUST_PASSWORD是证书的密码,必须与生成证书步骤里的设置的证书密码一致。如下:

public static SSLSocketFactory getSSLSocketFactory(Context context) {
    final String CLIENT_TRUST_PASSWORD = "123456";//信任证书密码,该证书默认密码是changeit
    final String CLIENT_AGREEMENT = "TLS";//使用协议
    final String CLIENT_TRUST_KEYSTORE = "BKS";
    SSLContext sslContext = null;
    // ...
}

如果是cer类型证书,需要使用生成bks方法重新生成bsk类型证书。

坑2:java.io.IOException: Hostname ‘xx.com’ was not verified

原因:

服务器主机名认证失败

解决方法:

1. 如果okhttpclient中有hostnameverify的配置,加上一个自定义的HostNameVerify,如下

((HttpsURLConnection) urlConnection).setHostnameVerifier(new HostnameVerifier() {
  @Override
  public boolean verify(String hostname, SSLSession session) {
    return true;
  }
});

2. 如果不需要HostNameVerify直接不设置就可以。

//.hostnameVerifier(HTTPSUtils.getHostNameVerifier(hostUrls)) 注释掉这句代码

坑3:javax.net.ssl.SSLPeerUnverifiedException

原因:

SSL链接时主机名验证失败

解决方法:

//.hostnameVerifier(HTTPSUtils.getHostNameVerifier(hostUrls)) 注释掉这句代码

坑4:javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for cert

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for cert
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:333)
at okhttp3.internal.io.RealConnection.connectTls(RealConnection.java:239)
at okhttp3.internal.io.RealConnection.establishProtocol(RealConnection.java:196)
at okhttp3.internal.io.RealConnection.buildConnection(RealConnection.java:171)
at okhttp3.internal.io.RealConnection.connect(RealConnection.java:111)
at okhttp3.internal.http.StreamAllocation.findConnection(StreamAllocation.java:187)
at okhttp3.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:123)
at okhttp3.internal.http.StreamAllocation.newStream(StreamAllocation.java:93)
at okhttp3.internal.http.HttpEngine.connect(HttpEngine.java:296)
at okhttp3.internal.http.HttpEngine.sendRequest(HttpEngine.java:248)
at okhttp3.RealCall.getResponse(RealCall.java:243)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:201)
at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:212)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:190)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:163)
at okhttp3.RealCall.execute(RealCall.java:57)
at retrofit2.OkHttpCall.execute(OkHttpCall.java:174)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.
at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80)
at rx.Subscriber.setProducer(Subscriber.java:211)
at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76)
at rx.Subscriber.setProducer(Subscriber.java:205)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.ja
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.ja
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
at rx.Observable.unsafeSubscribe(Observable.java:8666)
at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94)
at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecut
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust 
at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:324)
at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:225)
at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:115)
at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:571)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:329)
... 35 more
Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

原因:

使用了错误的证书。证书验证失败。

解决

重新生成证书

后记

之前我是看的Tamic的做法,不能走通,不推荐使用它的那种做法。如果是使用它的那种做法,出现错误,请按照本文的做法,使用HTTPS。

参考

http://www.cnblogs.com/lancer-ryn/p/5869696.html

http://www.jianshu.com/p/9a6c204616d2

Android webview和H5交互

发表于 2018-01-12

Android webview和H5交互

1. WebView加载页面

webview可以加载本地和网络页面,根据html的文件位置,有不同的写法.

mWebView.loadUrl("www.baidu.com");
mWebView.loadUrl("file:///android_res/test.html");

通常情况下,webview会重新设置webchromeclient,以便在本应用内实现页面跳转.

mWebView.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            mWebView.loadUrl(url);
            return true;
        }
    });

另外,要实现本文的与h5交互必须允许使用js接口,在实际开发中,一般都要加上这一句.

WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);

2. WebView调用js方法

调用js方法有两种情况: 如果调用js的无返回值方法, 可以直接使用load方法

mWebView.loadUrl("javascript:do()");

如果要调用有返回值方法,需要调用evaluatjavascript方法

mWebView.evaluateJavascript("sum(1,2)", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            Toast.makeText(ViewPagerGalleryDemoActivity.this, "value = " + value, Toast.LENGTH_SHORT).show();
        }
    });

js代码如下:

 <script type="text/javascript">
    function sum(a,b){
    return a+b;
    }
    function do(){
    document.getElementById("p").innerHTML="hello world";
    }
</script>

3. js调用Android方法

需要在Android中定义javascriptinterface接口和声明

  1. 首先要添加一个类和方法,并且用javascriptinterface

    public class TestJavaScriptInterface {

    @JavascriptInterface
    private String testJavaScriptInterface () {
        return "hello javascript"
    }
    

    }

  2. 打开javascriptinterface给h5的开关

    mWebView.addJavascriptInterface(new TestJavaScriptInterface(), "android");
    
  3. js代码如下:

<script type="text/javascript">
function s(){
    //调用Java的back()方法
    var result =window.android.testJavaScriptInterface();
    document.getElementById("p").innerHTML=result;
}
</script>

ADB总结

发表于 2018-01-02

adb总结

什么是adb,c/s架构,adb命令分为3种

adb命令是程序自带的一些命令,adb shell是调用Android系统中的命令,这些系统是放在system/bin目录下

adb命令

adb devices
adb get-state:device,offline,unknown
adb kill-server,adb start-srevre
adb bugreport 打印dumpsys,dumpstate,logcat的输出,分析错误,可以和logcat一样重定向到文件中
adb pull,adb push
adb install, adb uninstall
adb root, adb remount 获取 root 权限,并挂载系统文件系统为可读写状态
adb reboot bootloader , 重启设备,进入 fastboot 模式,同 adb reboot-bootloader 命令
recovery , 重启设备,进入 recovery 模式,经常刷机的同学比较熟悉这个模式
adb forward 将 宿主机上的某个端口重定向到设备的某个端口
adb connect 连接远程Android设备

// 根据TAG和级别过滤日志输出

adb logcat

adb logcat [-s] [ClassName:[PREVISOUS]] [*:[PREVIOUS]]

adb logcat // 直接输出的终端
adb logcat > c:/log.txt // 保存到文件
adb logcat ActivityManager:I PowerManager:D *:S
adb logcat *:W // 显示所有优先级大于等于“warning”的日志
 adb logcat -s ActivityManager

logcat命令列表:

-d 将日志显示到控制台后退出
-c 清理已经存在的目录
-f <filename> 将日志输出到文件
-v <format> 设置日志输出格式控制输出字段,格式如下,默认是brief格式
    brief--显示优先级/标记和原始进程PID
    process--仅显示进程PID
    tag--仅显示优先级/标记
    thread--仅显示进程:线程和优先级/标记
    raw--显示原始的日志信息,调用时间,优先级/标记,PID
    time--显示日期,调用时间,优先级标记/pid
    long--显示所有的元数据并用空行分割消息内容
-b <buffer> 加载一个可使用的日志缓冲区供查看,默认是main
    radio--查看包含在无线/电话相关的缓冲区信息
    events--查看事件相关的消息
    main--查看主缓冲区

adb logcat -f c:/log.txt
adb logcat -v thread // 使用thread输出格式
adblogcat -b radio

adb shell命令

/system/bin下或者sdk sources/android-20/com/android/commands

[pm]
adb shell pm list package [-s|-3|-f|-i] FILTER
adb shell pm list package -i
adb shell pm path PATH
adb shell pm path com.tencent.mobileqq
adb shell pm list instrumentation
adb shell pm dump
adb shell pm dump com.tencent.mobileqq
adb shell pm install/uninstall
adb shell pm clear
adb shell pm set-install-location/get-install-location
[am]
adb shell am start [-n|-S|-W|-a] PACKAGENAME
adb shell am start -n com.android.camera/.Camera
adb shell am force-stop [PACKAGENAME]
adb shell am startservice [CLASSNAME]
adb shell am broadcast [CLASSNAME]
adb shell am monitor
adb shell am instrument
[input]
adb shell input text [TEXT]
adb shell input keyevent [KEYCODE]
adb shell input tap [X Y]
adb shell input swipe [X0 Y0 X1 Y1]
[screencap|screenrecord]
adb shell screencap -p [PATH]
adb shell screenrecord [PATH]
[UI automator]
adb shell uiautomator [runtest|dump]
[ime]
adb shell ime list -s
adb shell ime set [INPUTMETHOD]
[wm]
adb shell wm size
[monkey]

Android Monkey的用法

[settings]

修改系统设置
探究下 Android4.2 中新增的 settings 命令

[dumpsys]

参考1

参考2

命令 功能
package 包查询
activity 所有activity信息
connectivity 网络连接
netpolicy 网络策略
netstats 网络状态
wifi wifi信息
network_manager 网络管理
account 账号信息
alarm 闹钟信息
meminfo 内存信息
cpuinfo cpu使用情况
gfxinfo 帧率信息
display 显示
power 电源
batterystats 电池状态
battery 电池
batteryinfo $package_name 电量信息及CPU使用时长
diskstats 磁盘相关信息
usagestats 每个界面的启动时间
statusbar 状态栏
alarm 闹钟
location 位置
window 窗口
[log]
adb shell log [-p PREVIOUS] [-t TAG] [MESSAGE]
[getprop]

查看Android设备的参数信息

adb shell getprop [key]

linux命令

// 常用命令
cat,cd,chmod,cp,data,df,du,grep,kill,ln,ls,lsof,netstat,ping,ps,rm,rmdir,top,touch,>,>>,|

EPUB

发表于 2017-12-27

EPUB

什么是EPUB?

e-pub的意思是电子出版,是IDPF制定的一个标准,这个标准是基于xml格式的电子书或者其它数字出版物。

.epub文件其实就是一个简单的zip文件可以使用winrar或者linux使用归档管理器打开来看目录结构和基本文档。内容都是xml格式的。

epub文件的目录结构和基本文档

1. mimetype文件

这个文件十分简单,里面只有

application/epub+zip

但是这个文件必须作为zip文件中的第一个文件,并且不能被压缩,必须在根目录下。同时这个文件不能有回车和换行。

2. META-INFO

META-INFO文件夹跟mimetype一样,也是必须存在的文件夹,也必须在根目录下,在文件夹下有一个container.xml文件.epub阅读系统会首先查看该文件,他指向文件图书的源数字文件的位置。这个container.xml格式如下

<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf"
media-type="application/oebps-package+xml" />
</rootfiles>
</container>

mimetype 和 container 是 EPUB 档案中仅有的两个需要严格限制位置的文件。建议将其他文件保存到 EPUB 的子目录下(通常被称为OEBPS但不是必须的)。

3. 元数据文件(.opf packaging format)

META-INFO文件夹下的container.xml所指定的rootfile的文件。该文件文件名没有特殊要求,扩展名为.opf。他指定了图书中所有内容的位置,如文本和多媒体目录。另外他还会给出一个元数据文件,它是Navigation Center eXtended (NCX) 表,有的称其为逻辑目录,opf内容示例如下所示:

<?xml version='1.0' encoding='utf-8'?>
<package xmlns="http://www.idpf.org/2007/opf"
xmlns:dc="http://purl.org/dc/elements/1.1/"
unique-identifier="bookid" version="2.0">
<metadata>
<dc:title>Hello World: My First EPUB</dc:title>
<dc:creator>My Name</dc:creator>
<dc:identifier id="bookid">urn:uuid:12345</dc:identifier>
<meta name="cover" content="cover-image" />
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="text/xml"/>
<item id="cover" href="title.html" media-type="application/xhtml+xml"/>
<item id="content" href="content.html" media-type="application/xhtml+xml"/>
<item id="cover-image" href="images/cover.png" media-type="image/png"/>
<item id="css" href="stylesheet.css" media-type="text/css"/>
</manifest>
<spine toc="ncx">
<itemref idref="cover" linear="no"/>
<itemref idref="content"/>
</spine>
<guide>
<reference href="cover.html" type="cover" title="Cover"/>
</guide>
</package>
3.1 命名空间

文档本身必须使用命名空间 xmlns=”http://www.idpf.org/2007/opf“,
metadata也可以在这里指定命名空间,元数据一般使用 Dublin Core Metadata Initiative (DCMI)
xmlns=”http://purl.org/dc/elements/1.1/“

3.2 源数据

Dublin Core Metadata Initiative (DCMI)定义了一组常见的元数据,可以用来描述各种不同的数字资料,下边是元数据摘要:

...
<metadata>
<dc:title>Hello World: My First EPUB</dc:title>
<dc:creator>My Name</dc:creator>
<dc:identifier id="bookid">urn:uuid:12345</dc:identifier>
<meta name="cover" content="cover-image" />
</metadata>
...

有两个术语是必须的,分别是title和identifier。这个identifier必须是唯一的,对于图书出版商来说这个字段一般包括ISBN编号,可以使用URL或者很大的随机生成的uuid。

属性unique-identifier的值必须和dc:identifier元素的id属性匹配

epub规范没有要求包含name的属性值为cover的meta元素,但是为了增加封面和图像的可移植性,一般建议加上。meta元素的content属性的值对应在这个opf文件manifest元素的id号,

3.3 清单数据

opf文件中的manifest节点列出了epub的所有资源,通常是组成电子书的一组xhtml文件和相关的媒体文件。epub鼓励使用css设定图书内容的样式,因此manifest中也包含css。进入数字图书的所有内容必须在manifest中列出。
下边是manifest示例:

...
<manifest>
<item id="ncx" href="toc.ncx" media-type="text/xml"/>
<item id="cover" href="title.html" media-type="application/xhtml+xml"/>
<item id="content" href="content.html" media-type="application/xhtml+xml"/>
<item id="cover-image" href="images/cover.png" media-type="image/png"/>
<item id="css" href="stylesheet.css" media-type="text/css"/>
</manifest>

所有的想必须有对应的media-type

epub支持三种核心图像文件类型:JPEG,PNG,GIF和Scalable Vector Graphics(SVG矢量图)

3.4 spine

spine的意思是脊柱,作用是线性阅读顺序,可以将OPF spine看做书中的页面的顺序,按照文档顺序从上到下一次读取spine,下边是spine示例:

... 
<spine toc="ncx"> 
<itemref idref="cover" linear="no"/> 
<itemref idref="content"/> 
</spine> 
... 

每个itemref元素都需要一个idref属性,并且和manifest中的id想匹配。toc属性也是必须的,他引用manifest中表示ncx表文件名的id。

spine中的linear属性表明该项作为线性阅读顺序的一部分,还是和先后顺序无关。建议将封面定义为no。符合epub规范的阅读系统将首次打开spine中设置linear=no的第一项。

3.5 guide

这个是可选的,最好保留,可以为epub阅读系统提供语义功能。下边是示例:

...
<guide>
<reference href="cover.html" type="cover" title="Cover"/>
</guide>
...

上边就是opf文件中的所有内容,manifest文件定义epub所有物理资源,spine定义这些资源的顺序信息,guide负责解释这些部分的含义。

4. NCX(Navigation Control file for XML applications)

ncx定义了数字图书的目录表,在复杂的图书中,目录表通常采用层次结构,包括嵌套的内容,章节等信息。
下边是简单的toc.ncx文件内容:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN"
"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
<meta name="dtb:uid" content="urn:uuid:12345"/>
<meta name="dtb:depth" content="1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle>
<text>Hello World: My First EPUB</text>
</docTitle>
<navMap>
<navPoint id="navpoint-1" playOrder="1">
<navLabel>
<text>Book cover</text>
</navLabel>
<content src="title.html"/>
</navPoint>
<navPoint id="navpoint-2" playOrder="2">
<navLabel>
<text>Contents</text>
</navLabel>
<content src="content.html"/>
</navPoint>
</navMap>
</ncx>

EPUB使用的DAISY的NCX DTD的命名空间
xmlns=”http://www.daisy.org/z3986/2005/ncx/“

4.1 head

DTD要求ncx文件的head元素包括四个meta元素:

  • dtd:uid // 数字图书的唯一id,和opf文件中的dc:identifier对应
  • dtd:depth // 目录中层次的深度
  • dtd:totalPageCount // 仅用于纸质图书,保留0即可
  • dtd:maxPageNumber // 仅用于纸质图书,保留0即可
4.2 docTitle

是图书的标题,和opf文件中的dc:title对应

4.3 navMap

navMap是ncx文件中最重要的部分,定义了电子书的目录。navmap中包括一个或者多个navPoint元素,每个navPoint都要包括下列元素:

  • playOrder属性 // 说明文档的阅读顺序,和opf文件中spine中itemref元素的顺序相同
  • navLabel/text元素 // 给出章节的标题通常是章的标题或者数字,如”第一章”
  • content元素 // 他的src属性指向这些内容的物理资源。
  • 还可以有一个或者多个navPoint元素 // 表示层次结构

5. 问题

5.1 NCX和OPF文件元数据交叉

由于 NCX 源自其他标准,使用 NCX 编码的信息和 OPF 内容之间存在重复。如果通过程序生成 EPUB,这算不上什么问题,因为同样的代码可输出到两个文件中。两个位置的信息要一致,不同的 EPUB 读者可能使用不同位置的值。(可如果手工去编写呢?)

5.2 NCX和OPF spine有什么不同

opf spine主要用来描述电子书章节的顺序,比如第一章后第二章再后第三章 …

ncx文件描述电子书的目录结构。

一条法则就是ncx包括的节点比spine多。实际上spine的所有项都会出现在ncx中,但是ncx可能更详细。

Hencode学习笔记-1.3

发表于 2017-11-29

文字的绘制

1. Canvas绘制文字

canvas绘制文字有三种方式:1. drawText() 2. drawTextRun() 3. drawTextOnPath()

1.1 drawText(String text, float x, float y, Paint paint)

这个坐标可以认为是在文字左下角。是比较接近的位置,但是如果要精确还是以下边所说的基线为准。

1.2 drawTextRun()

API 23新加入的方法,比drawText有两个新的参数上下文和文字方向, 主要用于文字结构比较特殊的文字的绘制。

1.3 drawTextOnPath()

沿着一条线来绘制文字

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPaint.setColor(Color.BLUE);

Path path = new Path();
path.moveTo(100, 100);
path.lineTo(200, 200);
path.rLineTo(300, 100);
path.rLineTo(400, 200);
canvas.drawPath(path, mPaint);

canvas.drawTextOnPath("Hello HenCode!", path, 0, 0, mPaint);

方法为drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint), hOffset和vOffset分别是文字相对于path水平方向和垂直方向的偏移量

1.4 StaticLayout

canvas.drawText()只能绘制单行的文字,而不能换行。

  • 不能在view边缘自动换行
  • 不能使用\n换行

如果要绘制多行文字,必须把文字切断后使用drawText或者一一使用StaticLayout。

他是android.text.Layout的子类,单纯用来绘制文字的。

它可以设置宽度上限来使文字自动换行,或者使用\n使文字换行。

String text1 = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";  
StaticLayout staticLayout1 = new StaticLayout(text1, paint, 600,  
    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
String text2 = "a\nbc\ndefghi\njklm\nnopqrst\nuvwx\nyz";  
StaticLayout staticLayout2 = new StaticLayout(text2, paint, 600,  
    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);

...

canvas.save();  
canvas.translate(50, 100);  
staticLayout1.draw(canvas);  
canvas.translate(0, 200);  
staticLayout2.draw(canvas);  
canvas.restore(); 

2. Paint对文字绘制的辅助

2.1 设置显示效果

setTextSize() // 设置文字大小

setTextScaleX() // 设置文字缩放

setTextAlign() // 设置对齐方式,三种LEFT,RIGHT,CENTER,默认LEFT

setHinting() // 设置字体微调

现在的 Android 设备大多数都是是用的矢量字体。矢量字体的原理是对每个字体给出一个字形的矢量描述,然后使用这一个矢量来对所有的尺寸的字体来生成对应的字形。由于不必为所有字号都设计它们的字体形状,所以在字号较大的时候,矢量字体也能够保持字体的圆润,这是矢量字体的优势。不过当文字的尺寸过小(比如高度小于 16 像素),有些文字会由于失去过多细节而变得不太好看。 hinting 技术就是为了解决这种问题的:通过向字体中加入 hinting 信息,让矢量字体在尺寸过小的时候得到针对性的修正,从而提高显示效果。效果图盗一张维基百科的:

setTextSkewX() // 设置文字倾斜

setTextLocation() // 设置语言区域

setLetterPadding() // 设置字符间距

setFontFeatureSettings() // 设置字体样式

paint.setFontFeatureSettings("scmp"); // 设置为small cap
canvas.drawText("Hello Hencode", 100, 100, paint);

这个东西不懂,以后补充一下

setTypeFace() // 设置字体

paint.setTypeface(Typeface.DEFAULT);

paint.setTypeface(Typeface.SERIF);

paint.setTypeface(Typeface.createFromAssert(getContext().getAssert(), "xx.ttf"))

setUnderlineText() // 是否加下划线
setFakeBoldText() // 设置伪粗体
setStrikeThruText() // 是否加删除线

setElegantTextHeight(boolean elegant) // 是否开启优雅的高度

  1. 把「大高个」文字的高度恢复为原始高度;
  2. 增大每行文字的上下边界,来容纳被加高了的文字。

setSubpixelText() // 是否开启次像素级抗锯齿,就是增强抗锯齿效果。

setLinearText() // 设置liear text

2.2 测量文字尺寸

2.2.1 getFontSpacing() // 获得推荐的行距,即是相邻的baseline之间的距离,如下图所示
canvas.drawText(texts[0], 100, 150, paint);  
canvas.drawText(texts[1], 100, 150 + paint.getFontSpacing, paint);  
canvas.drawText(texts[2], 100, 150 + paint.getFontSpacing * 2, paint);  

2.2.2 getFontMatrics() // 获得FontMatrix, FontMatrix是一个相对专业的工具类,提供了几个文字排印方面的数值:ascent, descent, top,bottom, leading;

这个不知道什么时候用到

2.2.3 getTextBounds(String text, int start, int end, Rect bounds) // 获得文字的显示范围

text是文字,start,end是指待测的文本的开始位置和结束位置,rect是测量完要赋值的矩形。

paint.setStyle(Paint.Style.FILL);  
canvas.drawText(text, offsetX, offsetY, paint);

paint.getTextBounds(text, 0, text.length(), bounds);  
bounds.left += offsetX;  
bounds.top += offsetY;  
bounds.right += offsetX;  
bounds.bottom += offsetY;  
paint.setStyle(Paint.Style.STROKE);  
canvas.drawRect(bounds, paint);  

2.2.4 measureText(String text) // 测量文字宽度

canvas.drawText(text, offsetX, offsetY, paint);  
float textWidth = paint.measureText(text);  
canvas.drawLine(offsetX, offsetY, offsetX + textWidth, offsetY, paint);  

注意: getTextBounds()和measureText()的区别:

如果你用代码分别使用 getTextBounds() 和 measureText() 来测量文字的宽度,你会发现 measureText() 测出来的宽度总是比 getTextBounds() 大一点点。这是因为这两个方法其实测量的是两个不一样的东西。

  • getTextBounds()测量的是文字的显示范围,这个矩形恰好能够包裹住文字
  • measureText()测量的是文字绘制时所占的宽度,包括了字符间距等

2.2.5 getTextWidths(String text, float[] widths) // 获取每个字符的宽度,并传入width数组中

2.2.6 breakText() // 截取某长度的字符串

google api是这样写的
Measure the text, stopping early if the measured width exceeds maxWidth.意思是测量文本,如果宽度超过给定的宽度就停止测量。其效果就是如果你的字符串的长度大于breakText()中指定的最大长度,drawtext的时候就只显示这个被截取的定长的文字。返回值为截取的字符的个数。

String text = "Hencode is very very very ... nice!";
mPaint.setTextSize(64);
int i = mPaint.breakText(text.toCharArray(), 0, 17, 600, new float[]{});
canvas.drawText(text.toCharArray(), 0, i, 100, 100, mPaint);

2.2.7 光标相关

对于像EditText一样的场景,就会需要绘制光标,在API 23开始引入两个新方法来计算光标。(这跟光标没有毛线关系貌似,主要是测量字符的宽度)

2.2.7.1 getRunAdvance(String text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset) //
params des
text CharSequence: the text to measure. Cannot be null.
start int: the index of the start of the range to measure
end int: the index + 1 of the end of the range to measure
contextStart int: the index of the start of the shaping context
contextEnd int: the index + 1 of the end of the shaping context
isRtl boolean: whether the run is in RTL direction
offset int: index of caret position

return : width measurement between start and offset

即是说 text指的是原文本,start,end指的是要测量的开始和结束的位置,contextStart,contextEnd不知道什么意思, isrtl是否反转字符串,offset指要插入的位置。

返回的是start和offset之间的距离。

mPaint.setTextSize(36);
// 包含特殊符号的绘制(如 emoji 表情)
String text = "Hello HenCoder \uD83C\uDDE8\uD83C\uDDF3"; // "Hello HenCoder 🇨🇳"
int start = 0;
int end = text.length();
int contextStart = 0;
int contextEnd = text.length();
boolean isRtl = false;
int offset = text.length();
float advance = mPaint.getRunAdvance(text, start, end, contextStart, contextEnd, isRtl, offset - 4);
canvas.drawText(text, 100, 100, mPaint);
canvas.drawLine(100 + advance, 60, 100 + advance, 100, mPaint);

2.2.7.2 getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance) // 给出一个位置的像素值,计算出文字中最接近这个位置的字符偏移量
2.2.8 hasGlyph(String string) // 检查指定的字符串中是否是一个单独的字形。

练习

小结

应用获取系统权限

发表于 2017-11-24

应用获取系统权限

将程序打包成系统应用才能获得系统权限

  1. 添加清单文件

    android:sharedUserId="android.uid.system"
    
  2. build未签名apk

    android studio –> build –> build apk(s)

  3. 找到系统签名秘钥和系统签名工具

    系统密钥为:platform.pk8和platform.x509.pem

    AOSP路径: build\target\product\security

    工具为:signApk.jar

    AOSP路径:/out/host/linux-x86/framework/ signApk.jar

  4. 对未签名apk进行签名

    使用下边的命令进行签名

    java -jar $(signApk.jar的全路径地址) $(platform.x509.pem的全路径地址) $(platform.pk8的全路径地址) $(未签名apk文件的全路径地址) $(要生成的apk文件的全路径地址) 
    
  5. 放入/system/app/文件夹下(需要root权限)

    adb remount
    adb push $(系统签名过的apk全路径地址) /system/app/
    

签名工具和签名秘钥下载地址

【踩坑记录】自定义ViewGroup

发表于 2017-11-24

【踩坑记录】自定义ViewGroup

1.获取自定义viewgroup对象为空

解决方法:

检查自定义viewgroup的构造方法

public SearchHistoryView(Context context) {
    this(context, null);
}

public SearchHistoryView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public SearchHistoryView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

看构造方法调用是否正确,参数是否正确。我出现这个错误是在两个参数的构造方法中把第二个参数写成了null导致这个错误。

原因:

在自定义viewgroup的类中,构造方法中一般会调用两个参数的构造方法,在写构造方法时注意,一般都是最后调用this(arg1, arg2,arg3)这个构造方法,如果出现对象为空的情况,检查构造方法,参数是否正确,是否调用正确。

2.自定义viewgroup,直接在布局中添加子view正常,代码添加view时出现java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams

解决方法:

自定义LayoutParams类,

public class LayoutParams extends MarginLayoutParams {

    public int gravity = -1;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public LayoutParams(int width, int height) {
        super(width, height);
        gravity = Gravity.TOP;
    }

    public LayoutParams(int width, int height, int gravity) {
        super(width, height);
        this.gravity = gravity;
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}

并在自定义viewgroup类中重写如下几个构造方法

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return super.checkLayoutParams(p) && p instanceof LayoutParams;
}

@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p);
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

原因:

我是按照网上的zhy大神的flowlayout教程写下来的,他是在自定义viewgroup中重写了generatelayoutparams()方法,然后在onmeasure中直接讲layoutparams直接强转成marginlayoutparams,做demo的时候因为是直接在xml中把子view添加到viewgroup控件中,这种做法是没有问题的,但是在写项目的时候因为需要动态添加布局,这时候就出现了以上的问题。建议去看一下layoutparams的知识。

3.子view设置了margin,但是在viewgroup中看到log打印margin还是0

解决方法:

因为我使用的是动态添加子view的方式

TextView textView = (TextView) LayoutInflater.from(SearchActivity.this).inflate(R.layout.item_shv_textview, null, false);
           textView.setText(bean);

上边的代码一般都是我写inflat view的代码,3个参数中,arg1代表资源文件,arg2代表父布局,arg3代表是否依赖父布局,一般情况下用到的时候我都是上边的代码,但是在这里用要改为

TextView textView = (TextView) LayoutInflater.from(SearchActivity.this).inflate(R.layout.item_shv_textview, mParent, false);
            textView.setText(bean);

就是说必须指定他所在的viewgroup,margin属性才有值.

原因:

知识体系和碎片化阅读

发表于 2017-11-24

知识体系和碎片化阅读

知乎上关于知识体系的问答

概念及误区

通俗点的解释就是将一些零碎的,分散的,相对独立的知识点或者概念加以整合,使之形成具有一定联系的知识系统。

个人知识体系的两大误区

  1. 以“知识收集”为中心的学习观念
  2. “囫囵吞枣,不求甚解”的学习方法

建立知识体系的一般方法

  1. 明确知识体系的主题和用途

    我的知识体系是什么,我做这个是为了什么。

    只有搞清楚自己知识体系的方向,找到自己最愿意学习的部分,并且能让自己更专业的知识,你才有足够的动力去坚持。

    明确知识体系的主题和用途可以帮助你迅速理清脉络,剔除无效信息,不必再在无关的内容上浪费自己宝贵的时间。

  2. 知识的整理和分类

    碎片阅读如何形成知识体系? - 知乎

    1. 从深度到广度
    2. 从广度到深度

      可以使用有道云,印象笔记等笔记类软件进行整合,建立专门的笔记本,然后把你的知识点按照名称,作为笔记归纳到你的知识体系中。

  3. 知识的输出和运用

    把自己学到的知识写成文章等发出去。

    “每章小结”是很有必要的。不一定要每章,至少一周一复习

  4. 找到获取知识的途径

    1. 网络
    2. 书籍
    3. 课堂
    4. 留心生活中的点滴
  5. 把握知识动态,更新知识体系

    要去经常浏览最新的跟自己知识体系有关的新技术

如何构建自己的知识体系

  1. 确定哪些知识加入知识体系
  2. 知识组块
  3. 及时复习

碎片阅读如何形成知识体系

概念

  1. 并非碎片化就能形成有价值的体系
  2. 最大的问题是知识的获取和整理
  3. 碎片化阅读和知识体系的构建的本质:拿来主义,分门别类

材料选择和基本特点

  1. 阅读的愉悦感

    一个感兴趣的内容能加强你的抗干扰能力,把碎片化阅读的效率极大的提高。

    碎片化信息的第一选择是:具有吸引力的内容

  2. 难度较低的陌生材料或者熟悉的材料

    碎片化阅读并不盲目追求陌生知识,陌生知识是对自己有用,但是陌生知识一般有自己的知识体系。你可以在遇到陌生领域的时候,先去构建该领域知识体系,了解该领域基本知识之后再进行阅读。

养成收集整合,反复阅读的习惯

最好把有价值的内容随手保存到笔记软件里,整合在一起。减少出现“我好像在哪里见过这个问题”“不过我想不起在哪里看到的了”的困扰,原作者推荐有道云笔记

  1. 有目的的阅读

    阅读时可以将有用的片段进行保存,可以保存到云笔记等软件中。

  2. 无目的的阅读和浏览

    有趣的内容也可以浏览,扩大知识体系

归纳

分类是一种重要的科学方法,从碎片到块到系统,归纳可以说是核心的一步。

带着疑问,遇到就保存,清晰的整理归类后,绕开繁琐复杂的信息,大脑的顿悟系统就开始起作用了,你会发现比较复杂的事物变的易于理解和条理清晰起来。

分好类之后,再遇到什么有用内容,我都知道保存归纳到哪里,并且去哪里提取相关的信息。除此之外,我还有很多其他学科类收集与整理,都获益颇多。同理,题主的很多的学习与工作的事都可以通过这个方式提高效率并且对于问题有更好的认识,这也是为建立知识系统进行的必要铺垫。

整理和构建体系

在你归纳整合之后,你就可以对当前知识进行系统的整理,建立一个文件夹,写一些心得等,基本的知识分类框架就已经出来。

总结一下步骤

  1. 碎片阅读的选择
  2. 随手保存
  3. 整理归纳
  4. 在归纳之后建立不同层级的文件夹

Android WIFI开发

发表于 2017-11-24

[Android WIFI开发]

前言

在Android应用层的开发中,使用到wifi相关知识点的地方并不多,所以之前对wifi开发并不熟悉,最近接到的两个硬件项目都有用到wifi并且知识点越来越深入,所以有必要记一下笔记,做一些注释。

基本使用

1.权限

Android中要使用系统功能一般都要申请权限,在6.0上可能还要手动申请权限,这里wifi需要的权限有

<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> // 需要系统权限 [定位权限]

其中在6.0以上设备,定位权限需要主动申请,并且如果要获取扫描wifi列表需要打开系统的定位开关。

2. WiFi相关API

WIFI相关API

ScanResult类用于存放wifi扫描结果信息,包含ssid(wifi名称), bssid(网络接入点地址),capabilities(加密类型),frequency(传输频率),level(信号强度)等。这里的解释并不太标准,但是对应功能很形象,如果你要了解更多,可以去具体的查阅资料。

WifiConfiguration类用于存放wifi的配置信息。包括wifi的密码,加密类型,网络id(用于连接wifi)等。他的几个子类分别对应秘钥加密方式,安全协议等,这些在设置wifi配置的时候会被用到

wifiinfo类用来描述wifi属性和连接状态。暴露了一些方法给开发者调用。getBSSID(), getMacAddress(), getIpAddress(),getSSID()等

WifiManager类是framework层暴露的api,用来管理wifi。通过调用 Context.getSystemService(Context.WIFI_SERVICE)可以得到类的实例。通过他可以得到:1.已经配置的网络列表。2.当前连接的wifi。3.扫描到的wifi。4.以及一些常量表示广播的意图等

3. wifi状态及开关

/**
 * 判断wifi是否打开
 * <p>需添加权限 {@code <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>}</p>
 *
 * @return {@code true}: 是<br>{@code false}: 否
 */
public static boolean getWifiEnabled() {
    @SuppressLint("WifiManagerLeak")
    WifiManager wifiManager = (WifiManager) Utils.getApp().getSystemService(Context.WIFI_SERVICE);
    return wifiManager.isWifiEnabled();
}

/**
 * 打开或关闭wifi
 * <p>需添加权限 {@code <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>}</p>
 *
 * @param enabled {@code true}: 打开<br>{@code false}: 关闭
 */
public static void setWifiEnabled(final boolean enabled) {
    @SuppressLint("WifiManagerLeak")
    WifiManager wifiManager = (WifiManager) Utils.getApp().getSystemService(Context.WIFI_SERVICE);
    if (enabled) {
        if (!wifiManager.isWifiEnabled()) {
            wifiManager.setWifiEnabled(true);
        }
    } else {
        if (wifiManager.isWifiEnabled()) {
            wifiManager.setWifiEnabled(false);
        }
    }
}

4. 扫描wifi

 /**
 *
 * 获取WIFI列表
 * <p>需要权限{@code <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>}</p>
 * <p>注意Android6.0上需要主动申请定位权限,并且打开定位开关</p>
 *
 * @param context 上下文
 * @return wifi列表
 */
public static List<ScanResult> getWifiList(Context context) {
    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    List<ScanResult> scanResults = wm.getScanResults();

    Collections.sort(scanResults, new Comparator<ScanResult>() {
        @Override
        public int compare(ScanResult scanResult1, ScanResult scanResult2) {
            return scanResult2.level - scanResult1.level;
        }
    });
    return scanResultsCopy;
}

 /**
 * 获取当前链接的WiFi信息
 *
 * @param context 上下文
 * @return 当前wifi数据
 */
public static WifiInfo getCurrentWifi (Context context) {
    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    return wm.getConnectionInfo();
}


public static String getWifiEncryptTypeStr (String capabilitie) {
    if (TextUtils.isEmpty(capabilitie)) return null;

    String encryptType;

    if (capabilitie.contains("WPA") && capabilitie.contains("WPA2")) {
        encryptType = "WPA/WPA2 PSK";
    } else if (capabilitie.contains("WPA2")) {
        encryptType = "WPA2 PSK";
    } else if (capabilitie.contains("WPA")) {
        encryptType = "WPA PSK";
    } else if (capabilitie.contains("WEP")) {
        encryptType = "WEP";
    } else {
        encryptType = "NONE";
    }

    return encryptType;
}

/**
 * wifi加密方式有5种
 * 0 - WPA/WPA2 PSK
 * 1 - WPA2 PSK
 * 2 - WPA PSK
 * 3 - WEP
 * 4 - NONE
 * @param capabilitie
 * @return
 */
public static int getWifiEncryptType (String capabilitie) {
    if (TextUtils.isEmpty(capabilitie)) return -1;

    int encryptType;

    if (capabilitie.contains("WPA") && capabilitie.contains("WPA2")) {
        encryptType = 0;
    } else if (capabilitie.contains("WPA2")) {
        encryptType = 1;
    } else if (capabilitie.contains("WPA")) {
        encryptType = 2;
    } else if (capabilitie.contains("WEP")) {
        encryptType = 3;
    } else {
        encryptType = 4;
    }

    return encryptType;
}

5. 连接wifi

 /**
 * @des 连接已经保存过配置的wifi
 * @param context
 * @param ssid
 */
public static void connectWifi (Context context, String ssid) {

    Log.d(TAG, "connectWifi: 去连接wifi: " + ssid);

    if (!getWifiEnabled()) return;

    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    WifiConfiguration wc = new WifiConfiguration();

    wc.SSID = "\"" + ssid + "\"";

    WifiConfiguration configuration = getWifiConfig(context, ssid);
    if (configuration != null) {
        wm.enableNetwork(configuration.networkId, true);
    }

}

/**
 * @des 连接没有配置过的wifi
 * @param context
 * @param ssid
 * @param password
 * @param encryptType
 */
public static void connectWifi (Context context, String ssid, String password, int encryptType) {

    Log.d(TAG, "connectWifi: 去连接wifi: " + ssid);

    if (!getWifiEnabled()) return;

    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    WifiConfiguration wc = new WifiConfiguration();
    wc.allowedAuthAlgorithms.clear();
    wc.allowedGroupCiphers.clear();
    wc.allowedKeyManagement.clear();
    wc.allowedPairwiseCiphers.clear();
    wc.allowedProtocols.clear();

    wc.SSID = "\"" + ssid + "\"";

    WifiConfiguration configuration = getWifiConfig(context, ssid);
    if (configuration != null) {
        wm.removeNetwork(configuration.networkId);
    }

    switch (encryptType) {
        case 4://不加密
            wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
            break;

        case 3://wep加密
            wc.hiddenSSID = true;
            wc.wepKeys[0] = "\"" + password +"\"";
            wc.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
            wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
            wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
            wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
            wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
            wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);

            break;
        case 0: //wpa/wap2加密
        case 1: //wpa2加密
        case 2: //wpa加密

            wc.preSharedKey = "\"" + password + "\"";
            wc.hiddenSSID = true;
            wc.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
            wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
            wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
            wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
            wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
            wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
            wc.status = WifiConfiguration.Status.ENABLED;
            break;
    }

    int network = wm.addNetwork(wc);
    wm.enableNetwork(network, true);
}

public static void disConnectWifi (Context context, int networkId) {
    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    wm.disableNetwork(networkId);
    wm.disconnect();
}

6. 获取和清除WIFI配置

/**
 * @des 清除wifi配置信息
 * @param context
 * @param ssid
 */
public static void clearWifiInfo(Context context, String ssid) {

    Log.d(TAG, "clearWifiInfo: 清除WIFI配置信息: " + ssid);

    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);

    String newSSID = "\"" + ssid + "\"";

    if (!(ssid.startsWith("\"") && ssid.endsWith("\""))) {
        newSSID = "\"" + ssid + "\"";
    } else {
        newSSID = ssid;
    }

    WifiConfiguration configuration = getWifiConfig(context, newSSID);
    configuration.allowedAuthAlgorithms.clear();
    configuration.allowedGroupCiphers.clear();
    configuration.allowedKeyManagement.clear();
    configuration.allowedPairwiseCiphers.clear();
    configuration.allowedProtocols.clear();

    if (configuration != null) {

        wm.removeNetwork(configuration.networkId);
        wm.saveConfiguration();
    }
}

public static WifiConfiguration getWifiConfig (Context context, String ssid) {

    Log.d(TAG, "getWifiConfig: 获取wifi配置信息: " + ssid);

    if (TextUtils.isEmpty(ssid)) return null;

    String newSSID;

    if (!(ssid.startsWith("\"") && ssid.endsWith("\""))) {
        newSSID = "\"" + ssid + "\"";
    } else {
        newSSID = ssid;
    }

    WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    List<WifiConfiguration> configuredNetworks = wm.getConfiguredNetworks();

    for (WifiConfiguration configuration : configuredNetworks) {
        if (newSSID.equalsIgnoreCase(configuration.SSID)) {
            return configuration;
        }
    }

    return null;
}

高级用法

1. 监听wifi状态的变化并刷新wifi列表和连接状态

2. 5Gwfif和2.4Gwifi

踩坑记录

1. wifi的ssid是带有””,所以你如果要判空是不行的,并且在连接wifi的时候要手动给ssid加上””。

2. 前文已经说了Android6.0以上要想扫描到wifi需要开启权限和手机的定位功能,不然得到的列表为空。

3. wifi的秘钥安全模式网上并没有人给出标准的答案,各个手机厂商的wifi系统上也都不一样,对于我们开发者而言,主要有5种方式也可以总结为3中:NONE(无密码模式), WEP(安全性较低), WAP PSK, WAP2 PSK, WPA/WPA2 PSK(这三种安全性较高)。需要注意的是通过capabilitie属性得到的字符串是类似 [WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS] 这样的,因为wifi的加密方式是安全模式+密码加密类型组合的方式。你要通过string.contains()方法解析出其中的安全模式,上文的扫描wifi中有解析的代码。

4. 扫描列表中ssid可能为空或””,一般要剔除掉,而且wifi列表一般是根据信号强度排序。也有可能列表中出现同名的wifi,我之前遇到是因为现在的路由器同时支持5G wifi和2.4G wifi,两个wifi属于不同的频段。

5. wifi是可以设置为隐藏wifi的,设置为隐藏wifi后不能被扫描到,但是可以通过直接输入ssid和秘钥的方式连接上。

后记

1…8910
Sparky

Sparky

我是谁?我在做什么?我为什么要这么做?我想做什么?我要怎么做?

99 日志
© 2019 Sparky
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4