如何上架gp ,在被代码关联的情况下

2020的主要工作其实是如何上架gp,背景是公司使用马甲包app上架gp后,被gp大批量下架了,导致在很长一段时间app都上架不上去。有审核时秒挂,有审核几天挂,有上线后几天挂。后来跟一个代上架服务商合作,把代码改成了纯java,结果代上架也上不去,都是下架。这半年来都是要跟gp审核战斗,大大小小包提审了有12+个包。

关于关联性做过的尝试

1、上架的环境处理
—— 用一个远程服务器,不用chrome,用firefox,用不同信用卡去购买google账号,使用到的账号都用一套新的。避免被关联到是同一个app
2、apk的处理
—— 提高混淆等级,换代码根路径,换文件名,换资源,增加无用代码,无用的第三方库,资源混淆,使用混淆字典,string.xml随机添加字符等,降低代码重复率
3、服务器接口的处理
—— 换服务器ip,部署一套独立的环境
4、app 产品流程和ui的处理
—— 换另外一套ui,产品流程稍微调一下不同
使用上面方式目的是为了让app关联性降到最低,得以审核通过。但还是一直审核不过。就差换人写和换办公室写代码了。
最后使用的方式:
1、android端使用360加固
2、android端使用反抓包
3、后端接口更改route和header,避免使用之前被封的域名
4、使用flutter语言去重写。

上面4条结果实施后app就通过了gp审核,得以上架成功。

#>猜想:怀疑使用了反抓包功能后,gp看不到请求的api,加固后反编译也看不到里面文件夹,所以gp分析不了这个app跟之前app的对比。

android反抓包

####一开始以为很简单

Android7.0以下
第一步
https://juejin.im/post/5cb98386e51d456e2c24851d
client = new OkHttpClient.Builder() .proxy(Proxy.NO_PROXY) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build();

第二步
public static boolean isWifiProxy(Context context) {
final boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
String proxyAddress;
int proxyPort;
if (IS_ICS_OR_LATER) {
proxyAddress = System.getProperty(“http.proxyHost”);
String portStr = System.getProperty(“http.proxyPort”);
proxyPort = Integer.parseInt((portStr != null ? portStr : “-1”));
} else {
proxyAddress = android.net.Proxy.getHost(context);
proxyPort = android.net.Proxy.getPort(context);
}
return (!TextUtils.isEmpty(proxyAddress)) && (proxyPort != -1);
}

android7.0以上
https://blog.csdn.net/alcoholdi/article/details/106455192

####然而,抓包却可以使用drony破掉,drony代理了手机所有的流量

第三步:
自带证书走起
从运维得到 xxxxx.pem
然后使用openssl转换成cer证书
openssl x509 -outform der -in xxxxx.pem -out ClientCertificate.cer
打印cer证书得到字符串
keytool -printcert -rfc -file ClientCertificate.cer

链接参考:
android设置反抓包:
https://juejin.im/post/5d8c593ee51d45783544b9ac
https://www.jianshu.com/p/cc7ae2f96b64
转换证书:
https://www.anquanke.com/post/id/201219

目的是让客户端验证服务端的https证书
最后会报网络错误
java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

####最后:

####哈哈,抓包如果还想破也是可以的,需要反编译app,然后找到cer证书,在charles上设置该证书,所以要把cer证书藏得深一点。

####flutter上的做法
charles 只要开了ssl后:

1
2
3
client2.findProxy = (uri) {
return "PROXY 10.10.18.4:8888; PROXY 10.10.21.6:8888;";
};
1
2
[DioErrorType.DEFAULT]: HandshakeException: Handshake error in client (OS Error: 
CERTIFICATE_VERIFY_FAILED: self signed certificate in certificate chain(handshake.cc:354))

加以下的true后,可以抓到包

1
2
3
4
5
6
client2.badCertificateCallback = (X509Certificate cert, String host, int port){
return true;
};
client2.badCertificateCallback = (X509Certificate cert, String host, int port){
return false;
};

[DioErrorType.DEFAULT]: HandshakeException: Handshake error in client (OS Error:
CERTIFICATE_VERIFY_FAILED: self signed certificate in certificate chain(handshake.cc:354))

1
2
3
4
5
6
7
Future<HttpClient> getHttpClient() async {
ByteData data = await rootBundle.load('images/ClientCertificate.pem');
SecurityContext context = SecurityContext.defaultContext;
context.setTrustedCertificatesBytes(data.buffer.asUint8List());
HttpClient httpClient = new HttpClient(context: context);
return httpClient;
}

调用的地方

dio = Dio(options);
HttpClient client2 = await getHttpClient();
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client2.findProxy = (uri) {
return “PROXY 10.10.18.4:8888; PROXY 10.10.21.6:8888;”;
};
client2.badCertificateCallback = (X509Certificate cert, String host, int port){
return false;
};
return client2;
};

完整

####最后验证了,如果不开proxy,怎么设置都可以顺利请求数据

https://book.flutterchina.club/chapter10/http.html
证书校验
Https中为了防止通过伪造证书而发起的中间人攻击,客户端应该对自签名或非CA颁发的证书进行校验。HttpClient对证书校验的逻辑如下:

  1. 如果请求的Https证书是可信CA颁发的,并且访问host包含在证书的domain列表中(或者符合通配规则)并且证书未过期,则验证通过。
  2. 如果第一步验证失败,但在创建HttpClient时,已经通过SecurityContext将证书添加到证书信任链中,那么当服务器返回的证书在信任链中的话,则验证通过。
  3. 如果1、2验证都失败了,如果用户提供了badCertificateCallback回调,则会调用它,如果回调返回true,则允许继续链接,如果返回false,则终止链接。
    综上所述,我们的证书校验其实就是提供一个badCertificateCallback回调,下面通过一个示例来说明。
    上面几句话验证过正确
    其实,如果要防抓包的话直接一句代码就好,正常请求是可以work,只要代理并且尝试用ssl proxying,那么都禁止即可
    client2.badCertificateCallback = (X509Certificate cert, String host, int port){
    return false;
    };

最后保险再加一道
import ‘package:http_certificate_pinning/http_certificate_pinning.dart’;
http_certificate_pinning: ^1.0.3
List allowedSHAFingerprints = [‘24:AE:ED:0F:06:35:A9:D5:77:C4:A9:66:D9:56:33:27:97:87:36:4C:83:DF:96:55:FE:8E:A6:04:26:D4:8C:1E’].toList();
dio.interceptors.add(CertificatePinningInterceptor(allowedSHAFingerprints));
这种方式不好,原因如下

#####最新实验出来的 flutter

#####这种会导致https线上跑的时候卡顿,原因是每个api接口都去请求一次服务器验证,很慢,1s或者2s时间

1
2
3
if (Application.baseUrl.startsWith('https')) {
dio.interceptors.add(CertificatePinningInterceptor([Application.HTTPS_FINGERPRINT].toList()));
}

//防抓包 用dio本身的方式,实际上跟okhttp做法一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getHttpClient().then((value) => {
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
HttpClient httpClient = value;
return httpClient;
}
});
//获取httpClient
Future<HttpClient> getHttpClient() async {
SecurityContext context = SecurityContext();
try {
ByteData data = await getCert();
context.setTrustedCertificatesBytes(data.buffer.asUint8List());
} catch (e) {
}
HttpClient httpClient = HttpClient(context: context);
return httpClient;
}

//获取证书

1
2
3
Future<ByteData> getCert() async {
return await rootBundle.load('images/ClientCertificate.pem');
}

第二种防抓包只要攻击者设置了代理抓包,就会报错CERTIFICATE_VERIFY_FAILED,原因对方没我们的cert

####kotlin版本

1
2
3
4
5
builder.proxy(Proxy.NO_PROXY);

X509TrustManager trustManager = HttpTrustManager.createTrustManager(); builder.sslSocketFactory(HttpTrustManager.createSslSocketFactory(trustManager)
, trustManager);
OkHttpClient client = builder.build();

flutter 如何监控一个变量的变化,如js的watch

####背景:

需要在一个页面里面加ga,统计页面里面banner的访问量,banner有一定的条件显示和隐藏。

####架构:

一个页面主widget,一个子widget banner,通过子widget的 show字段控制banner的显示隐藏。双向绑定。
在主widget只需要改show的值就可以控制banner。但要加访问量ga,没有想android里面有setShow的方法可以加。

####思路:

监控show变量值的变化,然后当show变成true的时候统计ga访问量。

####block:

flutter里面没有watch关键词,最后找到一种类似watch的字段,叫ValueNotifier和ValueListenableBuilder

####解决思路:

根据show创建一个ValueNotifier,

1
2
final bool show;
_showNotifier = ValueNotifier<bool>(show);

看到ValueNotifier有方法addListener,removeListener
以为可以直接监听,从而变量用ValueNotifier包装后直接监听,很方便

但事实上当变量show变化的时候,是监听不到任何回调的,后来发现addListener是需要动作触发的,即使用_showNotifier.value(true),可以监听到回调。这并不是我想要的。

所以继续找,发现一种可以收到回调,需要在widget树上使用ValueListenableBuilder,通过变量到widget树到监听回调,才能走完闭环。

1
2
3
4
5
6
7
8
9
ValueListenableBuilder(
builder: (BuildContext context, bool value, Widget child) {
if (this.widget.show) {
GaUtil.sendEvent(name: ‘’);
}
return buildRootWidget();
},
valueListenable: this.widget._showNotifier,
);

gradle 闭包实现动态赋值

背景:需要在某个闭包里面写对应的productFlavor的对应实现闭包。动态赋值,
猜想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
//最终目标,但实现不了,原因看完全文
dependencies {
'123' {

}
'456' {

}
}
2、这样也实现不了,原因是xxx不是顶层,扩展类实现不了
dependencies {
xxx(
'123' {

}
'456' {

}
}
}
3、只能这样实现, xxx是一个方法来的
dependencies {
xxx('123') {

}
xxx('456') {

}
}

1、
build.gradle 如果要实现在root写上,如下,但解决不了问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

xxx { //自定义
yyy {
"123" {
}
"456" {
}
}
}

android {
}

dependencies {

}

方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class YYYObj {

String name

public XXXObj(String name) {
this.name = name
}
}

//创建一个扩展
class YYYExtension {

NamedDomainObjectContainer<YYYObj> testDomains

public YYYExtension(Project project) {
NamedDomainObjectContainer<YYYObj> domainObjs = project.container(YYYObj)
testDomains = domainObjs
}

//让其支持 Gradle DSL 语法
void yyy(Action<NamedDomainObjectContainer<YYYObj>> action) {
action.execute(testDomains)
}
}

getExtensions().create("xxx", YYYExtension, project) //这句话是实现在root有一个xxx的关键,这个名字不能是已经存在的,如dependencies,android等

xxx {
yyy {
"123" {

}
"456" {

}
}
}

2、下面这种写法解决了我的问题,并且优雅还过得去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
以下两个写法等价
def equalCurrentFlavorRun = {
flavor, closure ->
if (getCurrentFlavorName().toLowerCase() == flavor.toLowerCase()) {
closure(flavor)
}

}

def equalCurrentFlavorRun(String flavor, Closure closure) {
if (getCurrentFlavorName().toLowerCase() == flavor.toLowerCase()) {
closure(flavor)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取当前的flavor名字,得到小写的flavor
def getCurrentFlavorName() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else if (tskReqStr.contains("channel"))
pattern = Pattern.compile("channel(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")

Matcher matcher = pattern.matcher(tskReqStr)

if (matcher.find())
return matcher.group(1).toLowerCase()
else
return ""

}

后面实现就是这样

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
//公共的写在这里
testImplementation 'junit:junit:4.12'
equalCurrentFlavorRun("xxx") {
//xxx马甲包单独的
implementation 'androidx.core:core-ktx:1.1.0'
}
equalCurrentFlavorRun("yyy") {
//yyy马甲包单独的
implementation 'androidx.core:core-ktx:1.0.2'
}
}

记第一个flutter上线过程

####flutter上gp

使用app bundle上传google后台,然后google会使用key生成不同架构的app,显示app大小7M
~8M这样。
优势是从gp下载会自动匹配手机,下载的apk包会比较小。缺点是从gp上下一个apk,但这个包不会是一个完整架构的apk,这个包是不能拿去其他渠道推广的。推广包最好重新打一个具备全部架构的apk包。

使用apk上传的话是要14M。

上传bundle包给google出现 Your app bundle targets the following unrecognised languages: fb. The list of supported language codes can be found in the IANA registry. Invalid languages caused by third-party libraries can be excluded using the resConfigs Gradle property.
需要在app/build.gradle里加下面的代码

1
2
3
defaultConfig {
resConfigs "en"
}

切记不要在google做实验性的传包,google后台是对包做唯一性的,维度有packageName,versionCode

上传自己的key到google后台,这个有坑,注意看清楚在下一步

切记慎点continue,google推荐的第一个方式是google自己生成的key,生成后就改不了,也导不出key以后供其他渠道使用。

####配置阶段
需要在android工程上配置的有:

Androidmanifest.xml

权限:

1
<uses-permission android:name="android.permission.INTERNET"/>

图标+应用名称

1
2
android:label="MoneyTopic"   
android:icon="@mipmap/ic_launcher"

build.gradle

签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
signingConfigs {
release {
storeFile file('xxx.jks')
storePassword ''
keyAlias ''
keyPassword ''
v1SigningEnabled true
v2SigningEnabled false
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
useProguard true

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
注意混淆文件proguard-rules.pro的规则

app version 在pubspec.yaml

1
version: 1.0.0+100   //versionName+versionCode

firebase:使用

#输入法问题
解决方案
需要背景图片

  • SingleChildScrollView外包一层Stack
    不需要背景图片
  • 包一层SingleChildScrollView

#点击事件问题
onPressed: testAction() 页面加载完会自动进入方法一次

onPressed: () => testAction() 正常,页面加载结束不会进入方法

#try catch 不到问题

await语法块里面才能呗try catch

1
2
3
4
5
try {
await api.xxx()
} catch (e) {
print(e); //才能捕捉到xx里面的报错
}

如果await的方法报错,该方法不会执行后面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

run() {
try {
test();
} catch (e) {
print(e); //捕捉不到test里面api的错误,
} finally {
print("3"); //无论如何都会走,不管是报错,还是正确
}
}

Future test() async {
print("1") //跑这个
await api.xxx()
print("2") //如果报错,不会跑这个
}

android生成日历提醒事件

需要什么权限

1
2
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.READ_CALENDAR" />

如何生成日历提醒事件

老规则,上github找,如下

https://github.com/kylechandev/CalendarProviderManager

实战一下

1
2
3
4
5
6
7
8
9
10
val calendarEvent = CalendarEvent(
title,
desc,
position,
startTime,
endTime,
0, null
)
// 添加事件
CalendarProviderManager.addCalendarEvent(context, calendarEvent)

android扫描手机里的图片位置信息和exif信息

手机拍照的照片有一个叫Exif的东西,记录着图片拍摄时候的一些信息。正因为如此,很多人的隐私会暴露。

图片位置信息哪里来的呢

新手机在打开摄像头app的时候会提示,拍的照片是否要记录位置信息,一般的人在有了新手机后迫不及待自拍的都会选择是。后面拍照都会记录位置信息到照片中,这就很危险了,一些app获取这些位置信息就可以知道你的活动范围在哪里。。。

exif里面有什么信息呢

常用的Exif支持的tag有:
TAG_APERTURE:光圈值
TAG_DATETIME:拍摄时间
TAG_EXPOSURE_TIME:曝光时间
TAG_FLASH:闪光灯
TAG_FOCAL_LENGTH:焦距
TAG_IMAGE_LENGTH:图片高度
TAG_IMAGE_WIDTH:图片宽度
TAG_ISO:ISO
TAG_MAKE:设备
TAG_MODEL:设备型号
TAG_ORIENTATION:旋转角度

android如何读取exif

老规则,开源库走起,github找了一会,发现有轻量的,有重的,从中选择了一个轻量的,如下

https://github.com/sephiroth74/Android-Exif-Extended

其实google有提供了exif类,全部的tag这一个文件

https://android.googlesource.com/platform/packages/apps/Camera2/+/master/src/com/android/camera/exif/ExifInterface.java

实战一下,获取拍照路径下面的图片,进行遍历获取exif信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
fun Context.getPicExifInfo(startTime: Long = 0, endTime: Long = TimeUtils.getNowMills()): List<PicExifInfo> {
val list = mutableListOf<PicExifInfo>()
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null, "${MediaStore.Images.Media.DATE_ADDED} > ? and ${MediaStore.Images.Media.DATE_ADDED} <= ?",
arrayOf((startTime / 1000).toString(), (endTime / 1000).toString()),
MediaStore.Images.Media.DATE_ADDED
)
cursor?.let {
while (cursor.moveToNext()) {
val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA))
val title = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.TITLE))
val width = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH))
val height = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT))
val exif = ExifInterface()
try {
exif.readExif(path, ExifInterface.Options.OPTION_ALL)
val latLon = exif.latLongAsDoubles
if (latLon != null && latLon.size > 1) {
val latitude = String.format("%.6f", latLon[0])
val longitude = String.format("%.6f", latLon[1])
}
val model = exif.getTag(ExifInterface.TAG_MODEL)?.valueAsString?.trim(0.toChar()) ?: ""
val make = exif.getTag(ExifInterface.TAG_MAKE)?.valueAsString?.trim(0.toChar()) ?: ""
val temp = exif.getTag(ExifInterface.TAG_DATE_TIME)?.valueAsString?.trim(0.toChar())
val time = temp?.let { TimeUtils.string2Millis(it, "yyyy:MM:dd HH:mm:ss").toString() } ?: ""
val exifInfo = PicExifInfo(title, width, height, latitude, longitude, make, model, time)
list.add(exifInfo)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
cursor?.close()
return list
}

还有一个要注意的

kotlin里面如果byte数据后面全是0,转换成string要显示,要这样处理

1
string?.trim(0.toChar())

ps:

是不是可以做一个显示自己什么时候去过哪里的app,加上现在有各种存储云,无论多久以前的照片都会在会端拉回到手机。是不是很可怕的app。权限只需要读存储权限。

android集成google AdMob

申请AdMob账号

先有google账号,然后可以直接申请AdMob账号
https://apps.admob.com/signup

集成AdMob到项目中

https://developers.google.cn/admob/android/quick-start
一共有4种广告形式

横幅(banner)
插页式(Interstitial)
原生(native)
激励视频(rewarded)

调试,测试unitId和线上unidId

测试unitId 如下

banner unitId: ca-app-pub-3940256099942544/6300978111
Interstitial unitId: ca-app-pub-3940256099942544/1033173712
native unitId: ca-app-pub-3940256099942544/2247696110
rewarded unitId: ca-app-pub-3940256099942544/5224354917

测试的时候请用测试unitId,最后要上线的以后务必要用线上unitId 测试一下有没有广告,切记
这里需要注意的地方,如果需要多个马甲包,然后admob只上其中一个,那么请注意做

在build.gradle里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
buildTypes {
release {
...
buildConfigField("String", "BANNER_AD_UNIT_ID", getBannerAdUnitId("release"))
buildConfigField("String", "INTERSTITIAL_AD_UNIT_ID", getInterstitialAdUnitId("release"))
buildConfigField("String", "NATIVE_AD_UNIT_ID", getNativeAdUnitId("release"))
buildConfigField("String", "REWARDED_AD_UNIT_ID", getRewardedAdUnitId("release"))
}
debug {
...
buildConfigField("String", "BANNER_AD_UNIT_ID", getBannerAdUnitId("debug"))
buildConfigField("String", "INTERSTITIAL_AD_UNIT_ID", getInterstitialAdUnitId("debug"))
buildConfigField("String", "NATIVE_AD_UNIT_ID", getNativeAdUnitId("debug"))
buildConfigField("String", "REWARDED_AD_UNIT_ID", getRewardedAdUnitId("debug"))
}
}

def getBannerAdUnitId(type) {
def unitId = '""'
switch (getCurrentFlavorName()) {
case “majiabao":
if (type == "release") unitId = ‘“..."'
else unitId = '"ca-app-pub-3940256099942544/6300978111"'
break
}
return unitId
}
def getInterstitialAdUnitId(type) {
def unitId = '""'
switch (getCurrentFlavorName()) {
case “majiabao":
if (type == "release") unitId = ‘“…"'
else unitId = '"ca-app-pub-3940256099942544/1033173712"'
break
}
return unitId
}
def getNativeAdUnitId(type) {
def unitId = '""'
switch (getCurrentFlavorName()) {
case “majiabao":
if (type == "release") unitId = ‘“..."'
else unitId = '"ca-app-pub-3940256099942544/2247696110"'
break
}
return unitId
}
def getRewardedAdUnitId(type) {
def unitId = '""'
switch (getCurrentFlavorName()) {
case “majiabao":
if (type == "release") unitId = ‘“..."'
else unitId = '"ca-app-pub-3940256099942544/5224354917"'
break
}
return unitId
}
def getGoogleAdApplicationId() {
def appId = "ca-app-pub-1111111111111111~1111111111"
switch (getCurrentFlavorName()) {
case “majiabao":
appId = “..."
break
}
return appId
}
def getCurrentFlavorName() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
println(tskReqStr)
//里面需要把tskReqStr解析出majiabao,这个字符串处理一下
return tskReqStr
}

遇到的坑

换成线上unidId后发现返回的errorCode=0

  1. 经查明,需要在AdMob账号上面填写收款信息,AdMob账号首页会提示红框要填写收款信息,填写后需要审核,大概半天时间。
  2. 再次调用线上unidId,发现没有返回errorCode=0的情况,此时有广告出现了,但有时会返回errorCode=3,表示暂时没有合适的广告
  3. 记得加混淆代码,不然广告在release版本是没效果的。

电话拨打相关监听

mi6 双卡
拨打10086

Receiver:
第一次进来:android.intent.action.NEW_OUTGOING_CALL
getResultData:电话号码,例如10086
Intent.EXTRA_PHONE_NUMBER:电话号码,例如10086

第二次进来:android.intent.action.PHONE_STATE
getResultData:null
TelephonyManager.EXTRA_STATE:OFFHOOK
TelephonyManager.EXTRA_INCOMING_NUMBER:电话号码,例如10086

挂断:
第一次进来:action: android.intent.action.PHONE_STATE
getResultData:null
TelephonyManager.EXTRA_STATE:IDLE
TelephonyManager.EXTRA_INCOMING_NUMBER:null

第二次进来:action: android.intent.action.PHONE_STATE
getResultData:null
TelephonyManager.EXTRA_STATE:IDLE
TelephonyManager.EXTRA_INCOMING_NUMBER:10086

flutter for web first

创建项目

1
2
3
4
5
6
flutter channel dev
flutter upgrade
flutter config --enable-web
cd <into project directory>
flutter create .
pub get
  • 如果要部署,要先build出来
1
flutter build web

run起来

使用webdev可以启动一个web服务器,可能可以改port,试试

1
2
... 此处省略webdev的安装命令
webdev serve

报错,因为flutter for web是预览版,等以后官方更新

1
2
3
webdev could not run for this project.
You have a dependency on `flutter` which is not supported for flutter_web tech preview. See https://flutter.dev/web for more details.
You have a dependency on `flutter_test` which is not supported for flutter_web tech preview. See https://flutter.dev/web for more details.

debug命令,port是随机的,暂时没有找到可以改port的方法

1
2
flutter run -d Chrome
flutter run -d "Web Server"

打命令run起来

可以看到在chrome显示的效果
http://localhost:57313

如果发起请求,还会遇到CORS跨域问题

如请求一个天气api的例子:
http://t.weather.sojson.com/api/weather/city/101280101

一个思路就是将本地的chrome全局解除这个CORS,但是可能会重置一些缓存(如账号,密码等)

目前我用的是nginx来解决这个问题,有个缺点,port是不固定的,每次启动server都要配置一次nginx,烦人,只能等以后官方解决了

1
2
brew install nginx
brew uninstall nginx
nginx配置

vi /usr/local/etc/nginx/nginx.conf
添加如下的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
location / {
proxy_pass http://127.0.0.1:57313; # 注意这里的端口要跟run起来后的端口一致
proxy_redirect default;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
access_log logs/access.log main;
}

location /api { #这里等于匹配api的链接,代理到下面这个地址过去
proxy_pass http://t.weather.sojson.com; # 后台api接口地址
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
access_log logs/access.log main;
}

重启nginx

1
2
3
4
5
6
7
8
9
两种命令,看安装的时候支持哪一种

brew services stop nginx
brew services restart nginx
brew services start nginx

nginx -s stop
nginx
nginx -s reload

flutter里面api写成

1
Response response = await dio.get("http://localhost:9090/api/weather/city/101280101");

这一次要访问nginx的端口

http://localhost:9090

可以看到请求成功

等于页面挂在9090端口,接口也在接9090端口,然后通过代理访问其他网站的api,这样就可以解决CORS跨域问题

参考文章:

官网
https://flutter.dev/docs/get-started/web

https://itnext.io/flutter-for-web-c75011a41956

flutter for web debug
https://www.didierboelens.com/2019/05/flutter-for-the-web/

一些讨论
https://gitter.im/flutter/flutter_web?at=5d7a4bac21fddd21d31dc7d0