android使用技巧及踩过的坑-采坑记续集

118、adb 连接设备出现unable to connect devices 问题

通过网络adb连接设备:
adb connect [设备的ip地址]
adb disconnect 或者adb disconnect [设备的ip地址]

未root的手机,不能连接,提示 unable to connect devices
解决方法:
    先usb连接上,输入adb tcpip 5555
    再输入 adb connect 192.168.1.7:5555就可以连接上

119、activity的生命周期回调函数:

onCreate()
onStart():用户可见
onResume():在前台,用户可见
onPause():用户可见
onStop()
            -----onRestart()
onDestory()

打开应用时先后执行了onCreate()->onStart()->onResume三个方法.
按BACK键时,我们这个应用程序将结束,这时候我们将先后调用onPause()->onStop()->onDestory()三个方法.
按HOME键,onPause()->onStop()
再次启动ActivityDemo应用程序时,则先后分别执行了onRestart()->onStart()->onResume()三个方法

当用户点击A中按钮来到B时,假设B全部遮挡住了A,将依次执行A:onPause -> B:onCreate -> B:onStart -> B:onResume -> A:onStop。
此时如果点击Back键,将依次执行B:onPause -> A:onRestart -> A:onStart -> A:onResume -> B:onStop -> B:onDestroy。

120、一张图片在android中如果不压缩,实际占用内存计算

例如一张1440*2448(351K)大小的图片,加载到内存中,不压缩
1440*2448(个像素)*4(RGBA,每个像素有4个颜色值)*8(每个颜色值8bit) = 112803840 bit = 13.45MB(放大40倍左右)

121、今天分析了下activity的启动流程源码分析,大概总结下:

在我们学习AIDL的时候我们activity调用service里面的方法是通过Binder的(就是Service里面有个内部类实现了IBinder接口,在onBind方法中会把这个对象返回给activity里面
    我们在activity里面就可以使用这个Binder类的应用来调用service里面的方法了)。
    
网上摘录:
    在Actvity启动过程中,其实是应用进程与SystemServer进程相互配合启动Activity的过程,其中应用进程主要用于执行具体的Activity的启动过程,回调生命周期方法等操作,
    而SystemServer进程则主要是调用其中的各种服务,将Activity保存在栈中,协调各种系统资源等操作。

    注意细节:
    a.
        问题:
            通过这里的代码我们可以发现,其实我们在Activity中调用startActivity的内部也是调用的startActivityForResult的。
            那么为什么调用startActivityForResult可以在Activity中回调onActivityResult而调用startActivity则不可以呢 
        答案:
            可以发现其主要的区别是调用startActivity内部调用startActivityForResult传递的传输requestCode值为-1,
            也就是说我们在Activity调用startActivityForResult的时候传递的requestCode值为-1的话,那么onActivityResult是不起作用的。 
            实际上,经测试requestCode的值小于0的时候都是不起作用的,所以当我们调用startActivityForResult的时候需要注意这一点。

    b.
        在查看execStartActivity方法之前,我们需要对mInstrumentation对象有一个了解 什么是Instrumentation 
        Instrumentation是android系统中启动Activity的一个实际操作类,也就是说Activity在应用进程端的启动实际上就是Instrumentation执行的,那么为什么说是在应用进程端的启动呢 
        实际上acitivty的启动分为应用进程端的启动和SystemServer服务进程端的启动的,多个应用进程相互配合最终完成了Activity在系统中的启动的,
        而在应用进程端的启动实际的操作类就是Intrumentation来执行的,可能还是有点绕口,没关系,随着我们慢慢的解析大家就会对Instrumentation的认识逐渐加深的。

        可以发现execStartActivity方法传递的几个参数: 
        this,为启动Activity的对象; 
        contextThread,为Binder对象,是主进程的context对象; 
        token,也是一个Binder对象,指向了服务端一个ActivityRecord对象; 
        target,为启动的Activity; 
        intent,启动的Intent对象; 
        requestCode,请求码; 
        options,参数;
        
        public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
            ...
        }
        
    c.
        Binder接口 –> ActivityManagerNative/ActivityManagerProxy –> ActivityManagerService;

        这样,ActivityManagerNative与ActivityManagerProxy相当于一个Binder的客户端而ActivityManagerService相当于Binder的服务端,
        这样当ActivityManagerNative调用接口方法的时候底层通过Binder driver就会将请求数据与请求传递给server端,并在server端执行具体的接口逻辑。
        需要注意的是Binder机制是单向的,是异步的,也就是说只能通过client端向server端传递数据与请求而不同等待服务端的返回,也无法返回.
        
        那如果SystemServer进程想向应用进程传递数据怎么办 
        这时候就需要重新定义一个Binder请求以SystemServer为client端,以应用进程为server端,这样就是实现了两个进程之间的双向通讯。
      
    d.
        这里通过我们刚刚的分析,ActivityManagerNative.getDefault()方法会返回一个ActivityManagerProxy对象,那么我们看一下ActivityManagerProxy对象的startActivity方法:
        这里就涉及到了具体的Binder数据传输机制了,我们不做过多的分析,知道通过数据传输之后就会调用SystemServer进程的ActivityManagerService的startActivity就好了。
        以上其实都是发生在应用进程中,下面开始调用的ActivityManagerService的执行时发生在SystemServer进程。
        
    e.
        Activity生命周期中的第一个生命周期方法终于被我们找到了。。。。也就是说我们在启动一个Activity的时候最先被执行的是栈顶的Activity的onPause方法。
        (一般我们可能认为是新的activity的onCreate方法,通过源码分析可以知道不是的)
        记住这点吧,面试的时候经常会问到类似的问题。
    f.
        Activity的启动流程一般是通过调用startActivity或者是startActivityForResult来开始的

        startActivity内部也是通过调用startActivityForResult来启动Activity,只不过传递的requestCode小于0

        Activity的启动流程涉及到多个进程之间的通讯这里主要是ActivityThread与ActivityManagerService之间的通讯

        ActivityThread向ActivityManagerService传递进程间消息通过ActivityManagerNative,ActivityManagerService向ActivityThread进程间传递消息通过IApplicationThread。

        ActivityManagerService接收到应用进程创建Activity的请求之后会执行初始化操作,解析启动模式,保存请求信息等一系列操作。

        ActivityManagerService保存完请求信息之后会将当前系统栈顶的Activity执行onPause操作,并且IApplication进程间通讯告诉应用程序继承执行当前栈顶的Activity的onPause方法;

        ActivityThread接收到SystemServer的消息之后会统一交个自身定义的Handler对象处理分发;

        ActivityThread执行完栈顶的Activity的onPause方法之后会通过ActivityManagerNative执行进程间通讯告诉ActivityManagerService,栈顶Actiity已经执行完成onPause方法,继续执行后续操作;

        ActivityManagerService会继续执行启动Activity的逻辑,这时候会判断需要启动的Activity所属的应用进程是否已经启动,若没有启动则首先会启动这个Activity的应用程序进程;

        ActivityManagerService会通过socket与Zygote继承通讯,并告知Zygote进程fork出一个新的应用程序进程,然后执行ActivityThread的mani方法;

        在ActivityThead.main方法中执行初始化操作,初始化主线程异步消息,然后通知ActivityManagerService执行进程初始化操作;

        ActivityManagerService会在执行初始化操作的同时检测当前进程是否有需要创建的Activity对象,若有的话,则执行创建操作;

        ActivityManagerService将执行创建Activity的通知告知ActivityThread,然后通过反射机制创建出Activity对象,并执行Activity的onCreate方法,onStart方法,onResume方法;
            (源码看是通过类加载器来加载的,也是反射的一种)

        ActivityThread执行完成onResume方法之后告知ActivityManagerService onResume执行完成,开始执行栈顶Activity的onStop方法;

        ActivityManagerService开始执行栈顶的onStop方法并告知ActivityThread;

        ActivityThread执行真正的onStop方法;
      
自己总结:
    通过源码分析可以看到:
        startActivity(客户端) --- ActivityManagerNative.getDefault() ---  进入到ActivityManagerService里面(服务端)。
        【ActivityManagerNative是一个抽象类继承Binder,具体的实现类是ActivityManager】所以这两个进程是通过ActivityManagerNative这个Binder通信的。
        
        服务端 --- app.thread.scheduleLauncherActivity(IApplicationThread) --- ApplicationThread(是ActivityThread的一个内部类)(客户端)
        【app.thread是一个IApplicationThread,也是一个Binder接口,具体是实现类是ApplicationThread】所以最终调用到了ActivityThread里面的方法了,又回到客户端了
    
        最终就是通过类加载器,加载Activity类,然后调用onCreate方法启动Activity
        
    所以2个进程间通信是通过Binder实现的。具体Binder底层是怎么通信的,还需要继续研究。

122、iperf3.0编译

sudo -s
source /etc/profile
export PATH=/home/hw/CTS/cts/android-ndk-linux/AndroidToolChain/bin:$PATH
make distclean
./configure --host=arm-linux CC=arm-linux-androideabi-gcc CXX=arm-linux-androideabi-g++ CFLAGS=-static CXXFLAGS=-static

tempdir = "/data/data/com.example.test";
test->outfile = stdout;

make

123、android使用su命令

String cmd = "ls -l /data/log/LogService/0/";
		
process = Runtime.getRuntime().exec("su");
DataOutputStream os = new DataOutputStream(process.getOutputStream());
os.writeBytes("ls -l /data/log/LogService/0/\n");//一定要加\n分割
os.writeBytes("exit\n");
os.flush();

//process = Runtime.getRuntime().exec(cmd);
buf = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"));
int status = process.waitFor();//0正常,1无权限,2找不到文件

Log.d("gaoqiang", "status:" + status);

String str = "";
final StringBuffer sb = new StringBuffer();
while ((str = buf.readLine()) != null) {
    Log.d("gaoqiang", str+"\n");
    sb.append(str);
}
Toast.makeText(this, sb.toString(), 1).show();

124、非root的手机,将下载的命令push到手机上执行。

有时候我们需要在app中执行一个命令,例如iperf,我们不想重新写个demo,只是想验证下命令是不是能正常执行。
这个时候我们可以在  /data/local/tmp目录下执行

步骤:
    1.push到/data/local/tmp/下(例如push iperf命令到这个目录下)
    2.chmod 777 iperf
    3./data/local/tmp/iperf -c 192.168.1.5
备注:其它的命令也一样,例如下载busybox也可以执行

125、android traceroute功能实现

在windows下面的命令【tracert】
    C:\Users\CHINA\Desktop>tracert www.tencent.com

    通过最多 30 个跃点跟踪
    到 ssd.tcdn.qq.com [119.147.227.25] 的路由:

      1    <1 毫秒   <1 毫秒   <1 毫秒 192.168.1.1
      2     1 ms     1 ms    <1 毫秒 192.168.0.200
      3     3 ms     5 ms     4 ms  100.64.0.1
      4     3 ms     3 ms     3 ms  253.185.37.59.broad.dg.gd.dynamic.163data.com.cn [59.37.185.2
      5     *        *        *     请求超时。
      6     *        *        *     请求超时。
      7     *        *        *     请求超时。
      8     *        *        *     请求超时。
      9    13 ms    13 ms    13 ms  119.147.227.25

    跟踪完成。
    
在linux下面的命令【traceroute】,但是android里面是没有这个命令的,使用busybox很多需要有root权限。
现在有2种实现方案:
    1、使用交叉编译好的traceroute命令,这个暂时没有找到好的方法。
    2、使用ping去实现。【github有很多这种案例,可以参考】
   
ping -c 1 -W 10 -t 1 www.baidu.com (获取每个节点的ip)
解析出ip地址,查询ip的位置
ping -c 1 - W 10 192.168.1.1 (解析出ping的时延,可以ping多次,window上的tracert命令是ping了3次)

MTU的探测方法
windows系统:

    查看网卡的mtu的值
    netsh interface ipv4 show interfaces
    修改本地的mtu值
    netsh interface ipv4 set subinterface "本地连接 2" mtu=1500 store=persistent

    测试最大MTU值方法
    ping -n 1 -f -l 1472 www.baidu.com
    -f不允许私自拆分数据包
    -l发送指定大小的数据包

android 系统 :
    查看手机mtu值设置
    cat /sys/class/net/wlan0/mtu  
    ifconfig命令也可以查看
    ip ad |grep wlan0 也可以查看
    
    【ping -c 1 -M do -s 1373 www.baidu.com】nova plus手机验证结果
    1|shell@HWMLA:/sys/class/net/wlan0 $ ping -c 1 -M do -s 1373 www.baidu.com
    ping -c 1 -M do -s 1373 www.baidu.com
    PING www.a.shifen.com (163.177.151.109) 1373(1401) bytes of data.
    ping: local error: Message too long, mtu=1400

    --- www.a.shifen.com ping statistics ---
    1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms
    
    【ping -c 1 -M do -s 1373 www.baidu.com】p8max手机验证结果
    shell@HWP8max:/ $ ping -c 1 -M do -s 1373 www.baidu.com
    ping -c 1 -M do -s 1373 www.baidu.com
    PING www.a.shifen.com (163.177.151.109) 1373(1401) bytes of data.
    From 192.168.3.3: icmp_seq=1 Frag needed and DF set (mtu = 1400)

    --- www.a.shifen.com ping statistics ---
    0 packets transmitted, 0 received, +1 errors
        
    只有设置1372才能通过,加上28的包头大小刚好1400。所以测试不了网关的mtu最大值
    
    设置手机的mtu值(需要root手机)
    ip link set dev wlan0 mtu 1500

126、在调试已安装好的应用方法(手机需要root)

有时候安装了app,在启动的时候想打断点调试,如果等点击app启动再在as中启动断点会很快就消失了,断点位置已经运行过了。
这个时候我们可以使用命令来启动指定的activity【am start -D -n  com.hw.smarthome/com.hw.smarthome.about.AgreementActivity】
输入命令的时候页面会弹框提示连接debugger,这个时候在as中把调试器关联上就可以运行断点了

127、对文本指定的字符修改颜色和大小,加粗等的方法

SpannableString SpannableStringBody = new SpannableString(this.getString(R.string.app_policy_msg_new));
SpannableStringBody.setSpan(new StyleSpan(Typeface.BOLD), 29, 84, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
cView.setText(SpannableStringBody);

可以使用SpannableString,可以设置样式。

128、退出app方法:

App.systemExit();

自杀进程
Process.killProcess(Process.myPid());

129、调试本地mysql数据库

启动服务:【net start mysql57】启动这个服务名

130、将一键提单的代码放到linux上使用gradlew编译出现的问题:

a、访问不到gradle里面配置的仓库地址
    解决方法:配置代理服务器

b、提示没有licence的错误 “Warning: License for package Android Support Repository not accepted.”
    原因:这个是由于在uploadLog模块中使用了和linux电脑中sdk不一样的support-v4包,导致需要license。
    解决方法:将版本修改为25.1.1和智能家居app一致,再次编译OK

131、使用codedex扫描操作说明:

a、编译服务器的地址
    100.100.151.218
    jenkins
    123456
    直接samba访问\\100.100.151.218

    工程地址:/cloud/wifiapp/OneClickSubmission
    一些工具地址:/cloud/tools(jdk 、 sdk)
    
    jenkins地址:http://10.22.142.35:8080/jenkins/job/Codedex_wifiapp/5/tqereports/

b、linux一些基本的vi命令
    vi
    操作命令
    定位到某一行
    vi build.gradle +100  【直接打开光标停留在100行的位置】

    使用【i】进入编辑模式,使用【esc】退出编辑模式,使用【:wq】保存退出

c、遇到的一些问题:
    1、开始的时候遇到不能get那个mcloud插件的uri地址
        解决方法:配置了代理就OK了
        
    2、遇到一个下面的报错
        > You have not accepted the license agreements of the following SDK components:
        [Android Support Repository].
        
        原因:这个问题由于使用的support-v4的版本在那台linux服务器没有,需要license。
        解决方法:将uploadLog这个模块中的v4版本配置为和智能家居保持一致
            compile 'com.android.support:support-v4:25.1.1'
            再次编译没有报这个错误。
    
    3、又遇到一个不能下载到mcloud的一个lark-runtime的配置文件
        Can not resolve com.hw.hae.mcloud.bundle....
        一直提示:Recieved status code 502 from server:badgateway
        手动去服务器访问是能访问到这个文件的。
        解决的方法:
            直接配置固定的版本号,不去加载服务器的配置文件。
            compile 'com.hw.hae.mcloud.bundle.android:lark-runtime:3.2.7'
        再次编译OK
        
    4、后面又遇到一个ExecutionException:java.lang.OutOfMemoryError:GC overhead limit exceeded的错误
        这个大概的意思就是编译内存不够。
        解决方法:
            修改了下工程目录下的 gradle.properties这个文件的配置
            org.gradle.jvmargs=-Xmx2048m
        
    注意:编译工程的时候注意把模块中的一些不需要的本地配置文件删除掉,不然编译也会出问题。

132、使用jarsigner工具对已有的apk签名

jarsigner -verbose -keystore keystore文件路径 -signedjar 签名后生成的apk路径 待签名的apk路径 别名
上面只支持v1的签名,遇到一个 [INSTALL_PARSE_FAILED_NO_CERTIFICATES]问题。

使用apksigner.bat签名
cd 到目录 C:\Users\gq.CHINA\AppData\Local\Android\sdk\build-tools\25.0.1
apksigner sign --ks 密钥库名 --ks-key-alias 密钥别名 xxx.apk 【直接覆盖了原来的apk,直接安装就OK】

查看签名信息:
	【apksigner verify -v --print-certs xxx.apk】
	
查看CERT.RSA文件的内容:
	可以使用jre里面的keytool工具查看【keytool -printcert -file CERT.RSA】

133、X工程现网替换成sdk 27 遇到的一些问题:

a.将工程选择为sdk 27后,出现 Unsupported class version number [52.0] (maximum 51.0, Java 1.7)
    网上提示proguard版本较低,需要更新proguard版本(当前的版本为4.7)
    
    解决方法:从网上下载一个新版的proguard版本(版本号6.0.3)
    下载完成替换【D:\android-sdk-windows-4.3\tools\proguard】这个目录,重新编译导出,没有报上面的那个错误

b.遇到的一个新问题
    替换新的proguard后,没有遇到之前那个问题。但是遇到一个新的问题
    eclipse弹框提示:proguard-android.txt does not exist or is not a regular file
    可能是proguard配置不对
    
    解决方法:
        将工程下的project.properties配置修改下
        之前的:proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
        修改后的:proguard.config=proguard-project.txt 
        重新编译导出发现是OK的,使用反编译工具查看,混淆是有效的。但是后面clean后试了又不行
        
        最后将从老版本的proguard目录下面将proguard-android.txt 复制到新版本目录下,编译混淆OK
    
c.使用sdk 27 有时候clean工程的时候,再导出apk,eclipse会弹框提示【Failed to export application】
    界面上没有任何的提示,这个时候我们去看下log
    log目录在【D:\Workspace\.metadata\.log】中
    可以看到log打印的错误信息
    Output: 
    D:\Workspace\SVN\xmos_33234\bin\AndroidManifest.xml: error: Unable to open file for read: No such file or directory
    
    解决方法:
    需要先用android6.0等待bin目录先自动生成AndroidManifest.xml,再切换到android8.0编译
    
备注:由于官方不推荐使用eclipse,所以使用新的sdk会出现一些兼容性问题,暂时只能先用android6.0自动生成后在切到8.0编译。
但是有个问题就是直接run是不行的,所以如果调试代码还需要使用android6.0。

134、后台接口打包

cd到工程的根目录【D:\0_xmos_server\xmos_Server\project\common-api】
运行命令【mvn clean package】
成功后会在target里面生成jar包

135、android源码查看方法

使用Source Insight 工具查看源码,下载3.5的版本,网上有比较多的破解码。
下载安装完成后 Project --- New Project 创建一个源码工程,将你的源码地址链接过来

设置代码窗口的文字大小方法:Options --- File Type Options --- Screen Font 既可以修改字体的大小了

使用的快捷技巧:
    a.在Project Files里面可以搜到你需要查看的java类。快捷键(ctrl+o)
    b.在Symbol Name里面可以搜索到这个类里面所有的方法和属性,快捷键(alt+l)
    c.跳转到上一个搜索的位置,点击界面上向左的箭头,快捷键(alt+,)
    d.跳转到下一个搜索的位置,点击界面上向右的箭头,快捷键(alt+.)

136、保存mpchart图片到本地sdcard

看了api里面有个saveToPath的方法,一直使用返回false,后面查阅源码需要这样使用
boolean res = chart_send_package.saveToPath("mpchart", "/test/"); //ture 成功 , false 失败
注意:生成文件的路径【/sdcard/test/mpchart.png】,test目录需要存在,第二个参数貌似前面必须加 /

137、android studio调试服务,自动断开

需要在手机开发者选项中把 “显示所有应用无响应ANR” 勾选上

138、部分android9.0的手机点击home退到后台,再次点击图标重新走welcome的问题

在MainActivity的onCreate方法中加入
	if (!isTaskRoot()) {
		finish();
		return;
	}

139、在android9.0上不显示通知的问题

设置的target sdk=27

String title = "一键提单工具测评通知";
String content = "日志上传成功";

String id = "channel_id_01";
String name="channel_id_01_name";
Context context = AppContext.getInstance().getApplication();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW);
    Toast.makeText(context, mChannel.toString(), Toast.LENGTH_SHORT).show();
    notificationManager.createNotificationChannel(mChannel);
    notification = new Notification.Builder(context)
            .setChannelId(id)
            .setContentTitle(title)
            .setContentText(content)
            .setSmallIcon(R.mipmap.ic_launcher).build();
} else {
    NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
            .setContentTitle(title)
            .setContentText(content)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setOngoing(true);
    notification = notificationBuilder.build();
}
notificationManager.notify(1008, notification);

140、调试服务端的接口注意事项:

a.启动服务配置好端口号
    【application-prod.properties】文件中配置
    server.port=6931

b.配置数据库地址,用户名和密码
    【application-prod.properties】
    【mybatis-generator.xml】
     2个文件都需要配置
     
c.配置访问的协议类型是http还是https
    【application-prod.properties】
    注释最后面https的就变成了http了,默认发布的时候使用https
    
d.本机调试的地址
    https://localhost:6931/api/swagger-ui.html

141、android8.0以上,应用内升级遇到的坑

a.找不到对应apk下载路径,这个是在android7.0以上就会出现的问题
    使用FileProvider就可以解决
b.下载完成后,不会进入app安装页面
    这个是由于android8.0上修改导致的,需要用户手动授予安装的权限
    
完整的适配代码如下:
    protected void installAPK(File file) {
        if (!file.exists())
            return;
        Intent intent = new Intent(Intent.ACTION_VIEW);
        //start gaoqiang 2018-12-17 modify,match for api 27
        Uri uri = null;
        if (Build.VERSION.SDK_INT >= 24) {
            uri = FileProvider.getUriForFile(this, CONTANT.FILE_PROVIDER, file);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

            //如果是android8.0判断是否有安装权限 gaoqiang 2019-01-21
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                hasInstallPermission = mContext.getPackageManager().canRequestPackageInstalls();
                if (!hasInstallPermission) {
                    LogUtils.d(TAG,"no install permission,go setting ,please grant permission");
                    //Toast.makeText(mContext,"请授予安装权限",Toast.LENGTH_SHORT).show();
                    startInstallPermissionSettingActivity();
                    return;
                }
            }

        } else {
            uri = Uri.fromFile(file);
        }

        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        // 在服务中开启activity必须设置flag,后面解释
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        LogUtils.d(TAG,"startActivity");
        mContext.startActivity(intent);
    }
    
    //使用onActivityResult不行,因为跳转的页面是一个singleTask所以这个回调会立即执行,我们只有在onResume周期函数中做业务处理
    @Override
    protected void onResume() {
        super.onResume();
        if(!hasInstallPermission){
            //检查安装权限
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                hasInstallPermission = mContext.getPackageManager().canRequestPackageInstalls();
                if(hasInstallPermission){
                    //如果授予权限,则启动安装
                    installApp(downloadPath);
                }
            }
        }
    }

特别注意:
    a.intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 我们一定要用addFlags不要用setFlags。不然覆盖掉flag可能会导致出现“解析包时错误”
    b.一定要在xml配置权限 

142、android悬浮框权限判断

public static boolean checkAlertWindowPermission(Context context){
    LogUtils.d("gaoqiang", "Build.VERSION.SDK_INT=" +Build.VERSION.SDK_INT);
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){
        LogUtils.d("gaoqiang", "<19 默认有悬浮框权限");
        return true;
    }
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {//<23
        try {
            Class cls = Class.forName("android.content.Context");
            Field declaredField = cls.getDeclaredField("APP_OPS_SERVICE");
            declaredField.setAccessible(true);
            Object obj = declaredField.get(cls);
            if (!(obj instanceof String)) {
                return false;
            }
            String str2 = (String) obj;
            obj = cls.getMethod("getSystemService", String.class).invoke(context, str2);
            cls = Class.forName("android.app.AppOpsManager");
            Field declaredField2 = cls.getDeclaredField("MODE_ALLOWED");
            declaredField2.setAccessible(true);
            Method checkOp = cls.getMethod("checkOp", Integer.TYPE, Integer.TYPE, String.class);
            int result = (Integer) checkOp.invoke(obj, 24, Binder.getCallingUid(), context.getPackageName());
            return result == declaredField2.getInt(cls);
        } catch (Exception e) {
            return false;
        }
    } else {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//>26 android8.0
            AppOpsManager appOpsMgr = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            if (appOpsMgr == null)
                return false;
            int mode = appOpsMgr.checkOpNoThrow("android:system_alert_window", android.os.Process.myUid(), context
                    .getPackageName());
            LogUtils.d("gaoqiang", "mode = " + mode);

// return mode == AppOpsManager.MODE_ALLOWED || mode == AppOpsManager.MODE_IGNORED;
return mode == AppOpsManager.MODE_ALLOWED;
} else {
boolean p = Settings.canDrawOverlays(context);
LogUtils.d(“gaoqiang”, "canDrawOverlays = " + p);
return p;
}
}
}

使用
private DisplayMetrics dm ;
private WindowManager wmFloatWindow;
private WindowManager.LayoutParams paramsFloatWindow;
private static boolean isShowFloatWindow = true;
private void createFloatWindow(){
    LogUtils.d(TAG, "createFloatWindow");
	if(!PermissionUtils.checkAlertWindowPermission(this)){
		LogUtils.d(TAG, "没有悬浮框权限,弹框授权");
        Uri packageUri = Uri.parse("package:" + mContext.getPackageName());
		showErrorDialogPosition(this,"需要悬浮权限,快去设置再来吧!",Settings.ACTION_MANAGE_OVERLAY_PERMISSION,packageUri);
	}else{

		dm = getResources().getDisplayMetrics();//2017-11-13 gaoqiang
		if(null == tv_float_window){
			wmFloatWindow = (WindowManager) getApplicationContext().getSystemService(WINDOW_SERVICE);
            paramsFloatWindow = new WindowManager.LayoutParams();
            //paramsFloatWindow.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                paramsFloatWindow.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            }else{
                paramsFloatWindow.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
            }

			// 不设置这个弹出框的透明遮罩显示为黑色
			paramsFloatWindow.format = PixelFormat.TRANSLUCENT;

			paramsFloatWindow.gravity= Gravity.LEFT|Gravity.TOP;

// paramsFloatWindow.x = dm.widthPixels/3;
paramsFloatWindow.x = 0;
paramsFloatWindow.y = dm.heightPixels/3;

// paramsFloatWindow.width = WindowManager.LayoutParams.WRAP_CONTENT;
paramsFloatWindow.width = (dm.widthPixels/3)*2;
paramsFloatWindow.height = WindowManager.LayoutParams.WRAP_CONTENT;

			paramsFloatWindow.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
			paramsFloatWindow.alpha=(float) 1.0;

			tv_float_window = new TextView(this);
			String str = "Wi-Fi名称:NA\n";
			str += "路由器MAC:NA\n";
			str += "信道:NA\n";
			str += "信号强度:NA\n";
			str += "连接速率:NA\n";
			str += "路由器IP:NA";
			tv_float_window.setText(str);
			tv_float_window.setTextSize(13);
			tv_float_window.setBackgroundResource(R.drawable.background_float_window_style);
			tv_float_window.setTextColor(0xffffffff);

			setTouchListener();

			wmFloatWindow.addView(tv_float_window, paramsFloatWindow);
			LogUtils.d(TAG, "有悬浮框权限,显示弹框");
		}else{
			tv_float_window.setVisibility(View.VISIBLE);
		}

	}

}

143、通过包名打开一个应用

例如今日头条
String appName = "com.ss.android.article.lite";
PackageManager manager = getPackageManager();
Intent launchIntentForPackage = manager.getLaunchIntentForPackage(appName);
startActivity(launchIntentForPackage);

144、定义一个圆角矩形的弹框

a.定义一个dialog继承AlertDialog
    public class PixelDialog extends AlertDialog{
        protected PixelDialog(Context context) {
            super(context, R.style.PixelDialogStyle);
        }
    }
    
b.PixelDialogStyle这个属性中
    <style name="PixelDialogStyle" parent="@android:style/Theme.Dialog">
        <item name="android:background">#00000000</item>//这个属性一定要设置成透明的,不然会出现直角
        <item name="android:windowBackground">@drawable/background_pixel_dialog</item>//这个设置貌似作用不大
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
    </style>

c.在dialog的xml文件中定义圆角和背景
    android:background="@drawable/background_pixel_dialog"
    
    background_pixel_dialog.xml
        <?xml version="1.0" encoding="utf-8"?>
        <shape android:shape="rectangle"
          xmlns:android="http://schemas.android.com/apk/res/android">
            <corners android:radius="8dip" />//这个就是显示在弹框的圆角,很重要
            <solid android:color="#60000000" />//这个就是弹框的背景颜色,上面的那个已经设置成透明的了
        </shape>

145、在蓝区遇到请求welink的mcloud地址出现502错误的问题:

a.不要接路由器组小网。
b.使用openproxy.hw.com的代理。
	会出现jcenter中的jar不能下载问题,切换成proxy.hw.com代理即可
	记住需要配置C:\Users\gq\.gradle\gradle.properties里面的密码
c.一些gradle配置
	as版本:3.3.2
	项目build.gradle:
		classpath 'com.android.tools.build:gradle:3.2.1'
		classpath 'com.hw.hae.mcloud.tools.build:hae-tools-public:1.1.6'
	gradle-wrapper.properties:
		distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
	app的gradle:
		将compile修改为implementation
	uploadLog的gradle:
		//implementation fileTree(include: ['*.jar'], dir: 'libs')必须要注释,
		compileOnly files('libs/fastjson-1.2.37.jar')只编译不打包,因为主app里面有这个jar

146、后台工程移植需要注意的问题

a.eclipse直接拷贝过来,里面eclipse.ini
	-vm
	C:/Program Files (x86)/Java/jdk1.7.0_51/bin(需要配置当前的java环境变量的路径,注意不能是1.8的,因为这个eclipse是32位版本,1.8的不兼容)
	可以直接安装工具包中的jdk-7u51-windows-i586.exe 这个是1.7版本的
	
b.配置maven的环境变量
	D:\xmos_Server\apache-maven-3.2.1-bin\bin(这个需要看你拷贝的路径)

c.cd到你的工程目录
	执行【mvn clean package】注意需要断开iaccess

d.打开eclipse,import工程
	import  -- Maven -- Existing Maven Projects
	
特别注意:需要配置Maven的配置
	在eclipse的设置中找到Maven 的User Settings :D:\xmos_Server\apache-maven-3.2.1-bin\conf\settings.xml
	
	这个settings.xml文件配置
		<localRepository>D:/xmos_Server/apache-maven-3.2.1-bin/repo</localRepository>配置的是本地的仓库
		
再次右击工程,运行mvn命令【mvn clean】 【mvn package】,将GBK编码改为UTF-8,工程OK,2未报错。

将工程导入idea遇到的问题:
	open... --- pom.xml
	1、idea配置maven:
		Maven home derectory:D:/xmos_Server/apache-maven-3.2.1-bin
		User settings file:D:\xmos_Server\apache-maven-3.2.1-bin\conf\settings.xml (overwrite勾选上)里面配置同上
		Local responsitory:D:\xmos_Server\apache-maven-3.2.1-bin\repo(overwrite勾选上)
		
	2、遇到很多的依赖下载不下来,“Cannot resolve plugin org.apache.maven”
		在设置中搜索maven ,选择Remote Jar Responsitories,在Remote Jar Responsitories增加2个网址:(参考网上解决方法https://www.cnblogs.com/phpdragon/p/7216626.html)
		http://maven.aliyun.com/nexus/content/repositories/central/
		http://maven.aliyun.com/nexus/content/groups/public/
	
	3、发现还是不行,使用everything搜索不能下载的jar(可能是之前下载没有成功)
		把搜索出来的.lastUpdated文件删除,例如:xml-maven-plugin-1.0.pom.lastUpdated
		再次Reimport all Maven Projects

147、android反编译AndroidManifest.xml文件

a、先下载【AXMLPrinter2.jar】文件,下载路径:https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/android4me/AXMLPrinter2.jar
b、将jar放到 C:\Users\gq\AppData\Local\Android\Sdk\tools 中
c、解压apk中的AndroidManifest.xml文件,放到tools目录中
d、输入命令【java -jar AXMLPrinter2.jar AndroidManifest.xml>AndroidManifest.txt】就可以了
实测OK

148、上传文件到服务器中

参考网址:https://www.cnblogs.com/woolhc/p/6123975.html
方式1:
客户端编写:
	/**
	 * server param is string , file name
	 * @author gaoqiang
	 * @param urlStr em:https://192.168.3.12:6931/api/xmos/uploadDetailFile
	 * @param folderPath em:/sdcard/xmos/xmos_game/
	 * @param fileName em:test.txt
	 * @return status
	 */
	public static String uploadFile(String urlStr, String folderPath, String fileName){
		urlStr = urlStr  + "?fileName=" + fileName;
		LogUtils.d(TAG, "start upload file to server , urlStr = " + urlStr);
		HttpsUtil.initHttpsUrlConnection();

		DataOutputStream ds = null;
		InputStream inputStream = null;
		InputStreamReader inputStreamReader = null;
		try {
			URL url = new URL(urlStr);
			URLConnection urlConnection = url.openConnection();
			HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
			httpURLConnection.setDoInput(true);
			httpURLConnection.setDoOutput(true);
			httpURLConnection.setUseCaches(false);
			httpURLConnection.setRequestMethod("POST");
			httpURLConnection.setRequestProperty("Connection", "Keep-Alive");
			httpURLConnection.setRequestProperty("Charset", "GBK");
			// 设置请求内容类型
			httpURLConnection.setRequestProperty("Content-Type", "text/xml");

			String filePath = folderPath + fileName;

			// 设置DataOutputStream
			ds = new DataOutputStream(httpURLConnection.getOutputStream());
			FileInputStream fStream = new FileInputStream(filePath);
			int bufferSize = 1024;
			byte[] buffer = new byte[bufferSize];
			int length = -1;
			while ((length = fStream.read(buffer)) != -1) {
				ds.write(buffer, 0, length);
			}
			ds.flush();

			LogUtils.d(TAG,"upload file response code = " + httpURLConnection.getResponseCode());
			if (httpURLConnection.getResponseCode() == 200) {
				fStream.close();
				LogUtils.d(TAG, "detail record file upload success ");
				return "success";
			}else{
				return "fail";
			}

		} catch (Exception e) {
			e.printStackTrace();
			LogUtils.e(TAG, "error=" + e.getMessage());
		} finally {
			if (ds != null) {
				try {
					ds.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (inputStreamReader != null) {
				try {
					inputStreamReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}
服务端编写:
	@RequestMapping(value = "/uploadDetailFile")
	@ApiOperation(notes="上传详细测试记录文件",value="上传详细测试记录文件",httpMethod="POST")
	@ApiResponses({@ApiResponse(code = 404, message = "请求路径没有或页面跳转路径不对") })
	public Object uploadDetailFile(HttpServletRequest request) {
		String msg = "ok";
		List<Map<String, Object>> list =new ArrayList<>();
		
		FileOutputStream fos = null;
		BufferedOutputStream bos = null;
		InputStream is = null;
		
		HashMap<String, Object> map = new HashMap<>();
		String dirName = "";
		String filePath = gameLogSavePath;
		List<String> accTypes = Arrays.asList(".txt",".zip");
		
		try {
//    		SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
//    		String dateFolder = df.format(new Date());
//    		String folderPath = "E:\\xmos\\game\\" + dateFolder + "\\";
			String fileName = request.getParameter("fileName") + ".txt";
			//获取文件后缀名
			String suffixName = fileName.substring(fileName.lastIndexOf("."));
			if(accTypes.contains(suffixName)){
				System.out.println(suffixName+"后缀合法");
			}else{
				System.out.println(suffixName+"后缀不合法");
				map.put("success", false);
				map.put("message", "上传文件类型有误");
				return map;
			}
			//提取YYYY-MM-DD
			String dirName_pattern = "\\d{4}_\\d{2}_\\d{2}";
			Pattern r1 = Pattern.compile(dirName_pattern);
			Matcher m1 = r1.matcher(fileName);
			if (m1.find()){
				dirName = m1.group(0);
			}
			//获取文件后缀名
			//String suffixName = fileName.substring(fileName.lastIndexOf("."));
			//重新生成文件名
			//fileName = UUID.randomUUID()+suffixName;
			//保存到static文件夹里
			String folderPath = filePath + dirName + "/";
			File f = new File(folderPath);
			if(!f.exists()){
				f.mkdirs();
			}
			File file = new File(folderPath+fileName);
			if(!file.exists()){
				file.createNewFile();
			}

			fos = new FileOutputStream(folderPath+fileName);
			bos = new BufferedOutputStream(fos);
			is = request.getInputStream();
			int len = -1;
			byte[] buf = new byte[1024];
			while ((len = is.read(buf)) != -1) {
				bos.write(buf, 0, len);
			}
			bos.flush();
			System.out.println("detail file upload ok");
			
			map.put("success", true);
			map.put("message", "success to upload");
		} catch (Exception e) {
			msg = e.getCause().toString();
			System.err.println(msg);
			map.put("success", false);
			map.put("message", msg);
		}finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (bos != null) {
				try {
					bos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
//    	return binder.toJson(Common.generateResJson(list, msg));
		return map;
	}


方式2:
客户端编写:
	/**
	 * server param is file object
	 * @author gaoqiang
	 * @param urlStr em:https://192.168.3.12:6931/api/xmos/uploadDetailFile
	 * @param folderPath em:/sdcard/xmos/xmos_game/
	 * @param fileName em:sheet_game_025GSJ188Y002465_2019_09_23_10_37_45.txt
	 * @return status
	 */
	public static String uploadMultipartFile(String urlStr, String folderPath, String fileName){
		LogUtils.d(TAG, "start upload file to server , urlStr = " + urlStr);
		HttpsUtil.initHttpsUrlConnection();

		DataOutputStream ds = null;
		InputStream inputStream = null;
		InputStreamReader inputStreamReader = null;
		try {
			URL url = new URL(urlStr);
			URLConnection urlConnection = url.openConnection();
			HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
			httpURLConnection.setDoInput(true);
			httpURLConnection.setDoOutput(true);
			httpURLConnection.setUseCaches(false);
			httpURLConnection.setRequestMethod("POST");
			httpURLConnection.setRequestProperty("Connection", "Keep-Alive");
			httpURLConnection.setRequestProperty("Charset", "GBK");
			// 设置请求内容类型
			httpURLConnection.setRequestProperty("Content-Type", "text/xml");

			String filePath = folderPath + fileName;
			LogUtils.d(TAG,"filePath = " + filePath);

			// add file special head
			String end = "\r\n";
			String twoHyphens = "--";
			String boundary = "---------------------------823928434";
			httpURLConnection.setRequestProperty("Content-Type", "multipart/form-data;file="+ fileName+";boundary=" + boundary);
			httpURLConnection.setRequestProperty("fileName",fileName);

			// 设置DataOutputStream
			ds = new DataOutputStream(httpURLConnection.getOutputStream());
			FileInputStream fStream = new FileInputStream(filePath);

			//write file special head
			ds.writeBytes(twoHyphens + boundary + end);
			ds.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"" + end);
			ds.writeBytes(end);

			//write file content
			int bufferSize = 1024;
			byte[] buffer = new byte[bufferSize];
			int length = -1;
			while ((length = fStream.read(buffer)) != -1) {
				ds.write(buffer, 0, length);
			}

			//write file special head
			ds.writeBytes(end);
			ds.writeBytes(twoHyphens + boundary + twoHyphens + end);

			ds.flush();
			LogUtils.d(TAG,"upload file response code = " + httpURLConnection.getResponseCode());
			if (httpURLConnection.getResponseCode() == 200) {
				fStream.close();
				LogUtils.d(TAG, "file upload success ");
				return "success";
			}else{
				return "fail";
			}
		} catch (Exception e) {
			e.printStackTrace();
			LogUtils.e(TAG, "error=" + e.getMessage());
		} finally {
			if (ds != null) {
				try {
					ds.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (inputStreamReader != null) {
				try {
					inputStreamReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}
服务端编写:
	/*
	APK端上传接口-上传游戏测试日志文件
	 */
	@RequestMapping(value = "/uploadDetailFile", method = RequestMethod.POST)
	@ResponseBody
	public Map<String, Object> uploadDetailFile(MultipartFile file) {
		HashMap<String, Object> map = new HashMap<>();
		String dirName = "";
		String filePath = gameLogSavePath;
		List<String> accTypes = Arrays.asList(".txt",".zip");
		try {
			//获取文件名
			String fileName = file.getOriginalFilename();
			//获取文件后缀名
			String suffixName = fileName.substring(fileName.lastIndexOf("."));
			if(accTypes.contains(suffixName)){
				System.out.println(suffixName+"后缀合法");
			}else{
				System.out.println(suffixName+"后缀不合法");
				map.put("success", false);
				map.put("message", "上传文件类型有误");
				return map;
			}
			//提取YYYY-MM-DD
			String dirName_pattern = "\\d{4}_\\d{2}_\\d{2}";
			Pattern r1 = Pattern.compile(dirName_pattern);
			Matcher m1 = r1.matcher(fileName);
			if (m1.find()){
				dirName = m1.group(0);
			}
			//获取文件后缀名
			//String suffixName = fileName.substring(fileName.lastIndexOf("."));
			//重新生成文件名
			//fileName = UUID.randomUUID()+suffixName;
			//保存到static文件夹里
			filePath = filePath + dirName + "/";
			System.out.println(">>>>>>>>>>>>>>>>>>>>" + filePath);
			File f = new File(filePath+fileName);
			File fileParent = f.getParentFile();
			if(!fileParent.exists()){
				fileParent.mkdirs();
			}
			file.transferTo(f);
			map.put("success", true);
			map.put("message", "success to upload");
		} catch (Exception e) {
			e.printStackTrace();
			String msg = e.getCause().toString();
			map.put("success", false);
			map.put("message", msg);
		}
		return map;
	}

149、遇到一个Runtime 出现ANR的问题

部分手机会出现这种问题

有问题的写法:
	...
	String cmd = "getprop";
	process = Runtime.getRuntime().exec(cmd);
	LogUtils.d(TAG, "start run command");
	int status = process.waitFor();//这里可能会阻塞,需要将这个代码放到读取流完成(网上查询的资料说runtime流中的数据没有读取走的话,buffer满了会一直阻塞在这里)
	LogUtils.d(TAG, "status=" + status);
	...
	
可能由于getprop的内容太多,导致buffer满了,

解决方法:
new Thread(){
        @Override
        public void run() {
            LogUtils.d(TAG,"Thread 2");
            Process process = null;
            BufferedReader buf = null;
            try {
                String cmd = "getprop";
                process = Runtime.getRuntime().exec(cmd);
                LogUtils.d(TAG, "start run command2" );

                buf = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"));
                String str = "";

                while ((str = buf.readLine()) != null) {
                    if (str.contains("ro.boot.odm.conn.schiptype") || str.contains("ro.connectivity.sub_chiptype")) {
                        LogUtils.d(TAG, "str2=" + str);
                    }
                }
                int status = process.waitFor();
                LogUtils.d(TAG, "status2=" + status);

            } catch (RuntimeException e) {
                LogUtils.d(TAG, e.getMessage());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    process.destroy();
                    buf.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }.start();

150、采集不到游戏时延数据日志导出方法:

adb shell "touch /sdcard/Android/data/com.tencent.tmgp.sgame/files/vmpdebug.log"
adb shell "touch /sdcard/Android/data/com.tencent.tmgp.sgame/files/vmpdebug"
pause
然后重启游戏,把/sdcard/Android/data/com.tencent.tmgp.sgame/files/vmpdebug.log这个导出来发下

151、更新悬浮框位置方法(兼容平板):

ll_float_window.setOnTouchListener(new View.OnTouchListener() {
        int startX, startY;
        int currentX, currentY;
        private boolean mScrolling = true;
        private boolean touchEvent;
        int gap = ViewConfiguration.get(MainActivity.this).getScaledTouchSlop();

        private int x,y;
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = (int) event.getX();
                    startY = (int) event.getY();

                    x = (int) event.getRawX();
                    y = (int) event.getRawY();
                    LogUtils.d(TAG,"updateWindowPosition , phoneOrientation = " + phoneOrientation + " || systemTop = " +systemTop + " || systemBottom = " + systemBottom
                            + " || systemLeft = " + systemLeft + " || systemRight = " + systemRight + " || startX = " + startX + " || startY =" + startY
                            + " || x = " + x + " || y =" + y);

                    mScrolling = false;//记录是否滚动了
                    touchEvent = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    LogUtils.d(TAG,"MotionEvent.ACTION_MOVE");
                    int currentX = (int) event.getRawX();
                    int currentY = (int) event.getRawY();
                    int movedX = currentX - x;
                    int movedY = currentY - y;
                    x = currentX;
                    y = currentY;
                    paramsFloatWindow.x = paramsFloatWindow.x + movedX;
                    paramsFloatWindow.y = paramsFloatWindow.y + movedY;

                    if (!mScrolling) {//如果正在滑动就不进行判断
                        if (Math.abs(startX - event.getX()) >= gap || Math.abs(startY - event.getY()) >= gap) {
                            mScrolling = true;
                        } else {
                            mScrolling = false;
                        }
                    }
                    // TODO: 2019/10/17 需要打开
                    if (mScrolling) {

// wmFloatWindow.updateViewLayout(view, paramsFloatWindow);
}

                    wmFloatWindow.updateViewLayout(view, paramsFloatWindow);
                    break;
                case MotionEvent.ACTION_UP:
                    if (mScrolling) {
                        touchEvent = true;//滑动事件拦截
                    }
                    mScrolling = false;
                    break;
            }
            return touchEvent;
        }

    });

152、动态广播注册,双向权限保护:

2个app:接收端game app   ; 发送端xmos app
a、只校验发送者权限(只要发送者有这个权限就能正常接收到广播):
	广播接收端:
		代码:
			//注册广播
			myReceiver = new MyReceiver();
			IntentFilter filter = new IntentFilter();
			filter.addAction(BROADCAST_ACTION_KILL_HICOM);
			registerReceiver(myReceiver, filter,"com.xmos.game.SEND_BROADCAST_PERMISSION",null);//这个权限是要求发送者必须要有的权限
	
		xml:
			AndroidManifest.xml 里面不用配置权限

	广播发送端:
		xml:
			<!--广播发送端在AndroidManifest中申明的权限,在xml申请,发送广播都是具有这个发送权限的-->
			<uses-permission android:name="com.xmos.game.SEND_BROADCAST_PERMISSION" />
			<permission
				android:name="com.xmos.game.SEND_BROADCAST_PERMISSION"
				android:protectionLevel="signatureOrSystem" ><!--注意需要设置这个权限的level,实际测试不设置接收不到,设置normal和signature都行-->
			</permission>
	
		代码:
			Intent intentReadDatabaseEnd = new Intent();
			intentReadDatabaseEnd.setAction(BROADCAST_ACTION_KILL_HICOM);
			sendBroadcast(intentReadDatabaseEnd);

			LogUtils.d(TAG,"发送一条具有发送权限的广播");

b、只校验接收者权限(只要接收端具有这个权限就能正常接收,否则接收不到)
	广播接收端:
		代码:
			//注册广播
			myReceiver = new MyReceiver();
			IntentFilter filter = new IntentFilter();
			filter.addAction(BROADCAST_ACTION_KILL_HICOM);
			registerReceiver(myReceiver, filter);//不要求发送端有发送的权限
	
		xml:
			<!--xml中配置接收权限-->
			<uses-permission android:name="com.xmos.game.RECEIVED_BROADCAST_PERMISSION" />
			<permission
				android:name="com.xmos.game.RECEIVED_BROADCAST_PERMISSION"
				android:protectionLevel="signatureOrSystem" ><!--注意需要设置这个权限的level,实际测试不设置接收不到,设置normal和signature都行-->
			</permission>
			
	广播发送端:
		xml:
			AndroidManifest.xml 里面不用配置权限
	
		代码:
			Intent intentReadDatabaseEnd = new Intent();
			intentReadDatabaseEnd.setAction(BROADCAST_ACTION_KILL_HICOM);
			sendBroadcast(intentReadDatabaseEnd,"com.xmos.game.RECEIVED_BROADCAST_PERMISSION");//接收者必须要有的权限

			LogUtils.d(TAG,"发送一条具有接收权限的广播");	
			
c、双向权限保护:
	广播接收端:
		代码:
			//注册广播
			myReceiver = new MyReceiver();
			IntentFilter filter = new IntentFilter();
			filter.addAction(BROADCAST_ACTION_KILL_HICOM);
			registerReceiver(myReceiver, filter,"com.xmos.game.SEND_BROADCAST_PERMISSION",null);//这个权限是要求发送者必须要有的权限
	
		xml:
			<!--xml中配置接收权限-->
			<uses-permission android:name="com.xmos.game.RECEIVED_BROADCAST_PERMISSION" />
			<permission
				android:name="com.xmos.game.RECEIVED_BROADCAST_PERMISSION"
				android:protectionLevel="signatureOrSystem" ><!--注意需要设置这个权限的level-->
			</permission>
			
	广播发送端:
		xml:
			<!--广播发送端在AndroidManifest中申明的权限,在xml申请,发送广播都是具有这个发送权限的-->
			<uses-permission android:name="com.xmos.game.SEND_BROADCAST_PERMISSION" />
			<permission
				android:name="com.xmos.game.SEND_BROADCAST_PERMISSION"
				android:protectionLevel="signatureOrSystem" ><!--注意需要设置这个权限的level,-->
			</permission>
	
		代码:
			Intent intentReadDatabaseEnd = new Intent();
			intentReadDatabaseEnd.setAction(BROADCAST_ACTION_KILL_HICOM);
			sendBroadcast(intentReadDatabaseEnd,"com.xmos.game.RECEIVED_BROADCAST_PERMISSION");//接收者必须要有的权限

			LogUtils.d(TAG,"发送一条具有双向权限的广播");	
			
	实际测试OK,能正常发送接收。
	
有没有发现,上面设置其实是有问题的,我们设置了 signatureOrSystem权限,但是2个app签名不一样同样也能接收到广播。其实搞反了。
例如我们在接收端要求发送端具有registerReceiver(myReceiver, filter,"com.xmos.game.SEND_BROADCAST_PERMISSION",null);,但是我们权限定义是在发送端,所以没有达到校验的效果。
在同一个手机中,一个自定义权限只能定义在一个app里面,接收端定义了权限,发送端就定义不了!!!
	我们接收端 registerReceiver(myReceiver, filter,"com.xmos.game.SEND_BROADCAST_PERMISSION",null);的xml中设置 SEND_BROADCAST_PERMISSION 权限,发送端只是申请(这是正常逻辑),只有设置android:protectionLevel="normal"或者
	设置android:protectionLevel="signatureOrSystem"签名一样才能达到效果。

总结:
	uses-permission是申请权限;
	permission是自己定义权限;
	android组件中的permission指明调用这个组件需要的权限。

153、混淆AIDL导致注册失败:

解决方法:
	#不能混淆aidl不然会注册失败
	-keep public interface android.emcom.IListenDataCallback

154、多语言设置

在资源文件res中
values
    strings.xml
    
values-en
    strings.xml
	
参考的博客://https://blog.csdn.net/wangsf1112/article/details/89852385
切换的代码:
    public void eventLanguage(View v) {
        String currentLanguage = getCurrLanguage(this);
        LogUtils.d(TAG,"current language  = " + currentLanguage);

        Locale targetLocale = Locale.ENGLISH;
        if(currentLanguage.equals(targetLocale.getLanguage())){
            targetLocale = Locale.CHINESE;
        }
        LogUtils.d(TAG,"target language  = " + targetLocale.getLanguage());

        Resources res = getResources();
        DisplayMetrics dm = res.getDisplayMetrics();
        Configuration conf = res.getConfiguration();
        conf.setLocale(targetLocale);
        res.updateConfiguration(conf, dm);

        LogUtils.d(TAG,"set language  = " + getCurrLanguage(this));

        //createConfigurationContext(conf);
        //recreate();

        /** restart activity */
        finish();
        startActivity(new Intent(this,MainActivity.class));
    }

    public String getCurrLanguage(Context context) {
        Locale locale = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            locale = context.getResources().getConfiguration().getLocales().get(0);
        } else {
            locale = context.getResources().getConfiguration().locale;
        }
        return locale.getLanguage();
    }

155、后台插入数据并且获取返回值方法(mysql语句):

mapper.xml写法:

	<!-- 登录记录 -->
	<insert id="uploadAccount" parameterType="java.util.HashMap">
		<selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER" >
			SELECT LAST_INSERT_ID()
		</selectKey>
		
		insert into t_xmos_login_account(
			login_time,
			userinfo_phone_os_version,
			userinfo_phone_model,
			userinfo_phone_brand,
			userinfo_phone_mac,
			w3_account,
			email,
			employee_number,
			user_cn,
			user_name_en,
			user_name_zh,
			login_type,
			phone_sn,
			phone_imei,
			app_version,
			reserved_field_1,
			reserved_field_2,
			reserved_field_3,
			reserved_field_4,
			reserved_field_5)
		select
			#{login_time},
			#{userinfo_phone_os_version},
			#{userinfo_phone_model},
			#{userinfo_phone_brand},
			#{userinfo_phone_mac},
			#{w3_account},
			#{email},
			#{employee_number},
			#{user_cn},
			#{user_name_en},
			#{user_name_zh},
			#{login_type},
			#{phone_sn},
			#{phone_imei},
			#{app_version},
			#{reserved_field_1},
			#{reserved_field_2},
			#{reserved_field_3},
			#{reserved_field_4},
			#{reserved_field_5}
		where
			(
			select count(*) from t_xmos_login_account 
			where 
			w3_account = #{w3_account} 
			and phone_sn = #{phone_sn}
			and phone_imei = #{phone_imei}
			) = 0;
	</insert>
增加 selectKey 标签,SELECT LAST_INSERT_ID()返回插入记录的条数(没有插入返回0,插入一条记录返回1)

接口如下:
	@RequestMapping(value = "/uploadAccount")
	@ApiOperation(notes="上传登录账号记录",value="上传登录账号记录",httpMethod="POST")
	@ApiResponses({@ApiResponse(code = 404, message = "请求路径没有或页面跳转路径不对") })
	public String uploadAccount(HttpServletRequest request) {
		String msg = "ok";
		List<Map<String, Object>> list =new ArrayList<>();
		
		try {
			System.out.println("client string = " + request.getParameter("loginInfo"));
			uploadAccount uploadBean = binder.fromJson(request.getParameter("loginInfo"), uploadAccount.class);
			
			HashMap<String, Object> insertMap = uploadBean.getInsertMap(uploadBean);
			
			int res = xmosTestServer.uploadAccount(insertMap);
			HashMap<String, Object> map = new HashMap<>();
			if(res >0){
				map.put("res","success");
				System.out.println("insert w3 account success.");
			}else{
				map.put("res","exist");
				System.out.println("w3 account is already existed.");
			}
			list.add(map);
		} catch (Exception e) {
			System.out.println(e.getCause().toString());
		}
		
		return binder.toJson(Common.generateResJson(list, msg));
	}

客户端请求返回的结果:
	{"total":1,"result":[{"res":"exist"}],"status":"200","msg":"ok"}

156、X工程合入日志上传uploadLog模块遇到的问题及解决方法:

错误:
	ERROR: Manifest merger failed with multiple errors, see logs
解决:
	当我们项目Manefest的某些属性和第三方库中Manefest的属性有冲突会报上面的错误。
	比如第三方库中也定义了icon、allowBackup等属性,且会与你的项目不同,则发生冲突,编译就会报错。
	
	解决办法就是,uploadLog模块中的AndroidManifest保持和app中一致就OK了。

错误:
	D:\Workspace_AS\IotWifiTester\app\src\main\AndroidManifest.xml Error:
			uses-sdk:minSdkVersion 17 cannot be smaller than version 21 declared in library [:uploadLog] D:\Workspace_AS\IotWifiTester\uploadLog\build\intermediates\library_manifest\debug\AndroidManifest.xml as the library might be usin
	g APIs not available in 17
			Suggestion: use a compatible library with a minSdk of at most 17,
					or increase this project's minSdk version to at least 21,
					or use tools:overrideLibrary="com.hw.automation" to force usage (may lead to runtime failures)
解决:
	将模块中的minSdkVersion 21 修改为17

157、访问xmos.consumer.hw.com异常

原因:
属于HSTS的范畴,被浏览器重定向了,而我们服务是http,当然访问不了。

解决:
chrome://net-internals/#hsts,选型Delete domain security policies中删除该域
填写“consumer.hw.com”删除

158、服务接口使用url拼接,代理网关需要特殊配置,接口免登录也需要不同的配置

接口写法,例如:(直接将taskid拼接的)
	@RequestMapping(value="/complete/taskid/{taskid}", method=RequestMethod.POST)
	public ResultBean<String> complete(@PathVariable("taskid")String taskId, @RequestBody JSONObject jsonParam ) {
		
		HashMap<String, Object> map = new HashMap<>();
		map.put("msg",jsonParam.get("msg"));
		map.put("approve_content",jsonParam.get("approve_content"));		
		
		try{
			workflowService.completeTask(taskId,map); 
			ResultBean<String> resultBean = new ResultBean<String>(ResultBean.SUCCESS, "completeTask", "success");
			return resultBean; 
		}catch(Exception e) {
			ResultBean<String> resultBean = new ResultBean<String>(ResultBean.FAILED, e.getMessage(), "FAILED");
			return resultBean;
		}
	}

代理网关网关配置:(需要使用$替换,如果不使用这种方式配置,会返回404错误码)
	com.hw.xmos	http://100.100.100.100:9000/api/workflow/complete/taskid/${taskid}	sz-inside	public	/workflow/complete/taskid	HTTP/HTTPS	POST	当前	永久	com.hw.xmos		审批接口

接口免登录配置:(否则会出现401错误码)
	"*workflow/complete/taskid*," +

app端的调用方式(切记需要使用 ?taskid=123456 这种,代理网关会默认将${taskid}替换为123456 )
	String 代理网关Service = getString(R.string.PERMISSION_APPLY_COMPLETE_代理网关);
	res = 代理网关HttpUtils.doPostTo代理网关BodyJson(代理网关Service + "?taskid=" + taskid, JsonUtils.toJson(requestBean));
	LogUtils.d(TAG,"complete 完成请求结果 代理网关 res = " + res);

159、hw JDK 替换

hw JDK下载路径:https://test.hw.com/cmcversion/index/releaseView?deltaId=19545500
版本号:
C:\Users\gq\Desktop>java -version
openjdk version "1.8.0_242"
OpenJDK Runtime Environment (build 1.8.0_242-hw_JDK_V100R001C00SPC180B003-b08)
OpenJDK 64-Bit Server VM (build 25.242-b08, mixed mode)

以前环境变量配置(win10):
    JAVA_HOME:D:\Program Files (x86)\Java\jdk1.8.0_31
    CLASSPATH:%JAVA_HOME%\lib\tools.jar;%JAVA_HOME%\jre\lib\rt.jar;%JAVA_HOME%\lib\comm.jar
    Path:
    %CLASSPATH%
    %JAVA_HOME%\bin

Android Studio工程替换:
打开Project Structure --- SDK Location --- JDK location,去掉“Use embedded JDK(recommended)”.选择hw JDK的安装路径即可
【例如:C:\Program Files\hw\jdk1.8.0_242】

160、xml中有webview会导致多语言切换失效

现象:语言切换成engilish,第一次启动app,进入有webview的activity中,显示的还是中文。而且退出的时候,进入其它页面也变成了中文(webview会将语言切换成手机默认的语言)
解决方法:在myapplication的oncreate回调中加入如下代码:
new WebView(this).destroy();
在LoginActivity的onCreate方法中调用setLanguage()方法。

    private void setLanguage(){
		LogUtils.d(TAG,"setLanguage");

		/** 获取当前手机语言 */
		String currentLanguage = LanguageUtils.getCurrLanguage(this);
		LogUtils.d(TAG,"current language  = " + currentLanguage);

		/** 读取持久化语言设置 */
		SaveReadSettingXML ssx = new SaveReadSettingXML(this, CONTANT.SettingXml);
		String languageXML = ssx.read(CONTANT.app_language, "");
		LogUtils.d(TAG,"languageXML = " + languageXML);

		if(!languageXML.equals("") && !currentLanguage.equals(languageXML) ){
			/** 切换语言 */
			Locale targetLocale = Locale.CHINESE;
			if(languageXML.equals("en")){
				targetLocale = Locale.ENGLISH;
			}
			LanguageUtils.changeLanguage(this,targetLocale);
			LogUtils.d(TAG,"切换语言 = " + targetLocale.getLanguage());
		}else{
			LogUtils.d(TAG,"xml语言为空 or 和当前语言一致,不需要切换语言。");
		}

	}
实际测试验证OK

161、遇到一个“Didn’t find class “android.support.v4.content.FileProvider” on path:”问题

现象:使用gradlew assembleDebug编译版本,安装会出现上面的问题。
查阅资料,这个和multiDexEnabled true分包有关系。参考:https://blog.csdn.net/fox_wei_hlz/article/details/78732907
我当前最小的minSdkVersion设置是17.

目前解决方法2个:
1、将minSdkVersion设置成21,测试OK
2、debug版本编译也使用混淆,测试OK

备注:如果设置成17,参考网上的解决方法并不生效。(在application中重写 attachBaseContext方法)
发布了126 篇原创文章 · 获赞 42 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/gaopinqiang/article/details/105083127