您现在的位置是:网站首页> Android

Android 开发实用技术收集

  • Android
  • 2024-12-18
  • 1183人已阅读
摘要

Android 开发实用技术收集


Android打印实现

锋邢天下 用AndroidStudio创建so

Android 生成.so文件

JAVA 大文件数据流分段输出,多段输出,支持视频图片等

防止APK被二次打包

Android事件监听器和回调方法

蓝牙连接 ESC/POS 热敏打印机打印(蓝牙连接篇)

Android Studio 代码混淆(你真的会混淆吗)

Android Studio中代码混淆

Android Studio 里面的引用第三方库总结

Android Studio工程libs

AAR应用方式和使用本地仓库

Android 使用ffmpeg

AndroidStudio简单的apk混淆

apk包解压,修改,和重新压缩

在Android Studio中调用so中的方法

低功耗蓝牙的四种工作模式.pdf

Android低功耗蓝牙通讯

直接用本地源码级库

Android Studio 添加依赖库三种方式




Android打印实现

赋予权限

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.qdian.testmyapp" >

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

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

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



    <application

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:roundIcon="@mipmap/ic_launcher_round"

        android:supportsRtl="true"

        android:requestLegacyExternalStorage="true"

        android:theme="@style/AppTheme" >

        <activity android:name=".MainActivity" >

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />


                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    </application>


</manifest>


Context.getExternalFilesDir() ;//取代android:requestLegacyExternalStorage="true"


import android.print.PrintAttributes;

import android.print.PrintManager;

void doPrint()

{

 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);

        PrintAttributes.Builder builder = new PrintAttributes.Builder();

        builder.setColorMode(PrintAttributes.COLOR_MODE_COLOR);

        printManager.print("test pdf print", new MyPrintDocumentAdapterEx(this,filePath), builder.build());

}

package com.qdian.testmyapp;


import android.content.Context;

import android.graphics.Bitmap;

import android.graphics.Canvas;

import android.graphics.Color;

import android.graphics.Matrix;

import android.graphics.Paint;

import android.graphics.pdf.PdfDocument;

import android.graphics.pdf.PdfRenderer;

import android.os.Bundle;

import android.os.CancellationSignal;

import android.os.ParcelFileDescriptor;

import android.print.PageRange;

import android.print.PrintAttributes;

import android.print.PrintDocumentAdapter;

import android.print.PrintDocumentInfo;

import android.print.pdf.PrintedPdfDocument;


import java.io.File;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.util.ArrayList;

import java.util.List;


public class MyPrintDocumentAdapterEx extends PrintDocumentAdapter {

    private Context context;

    private int pageHeight;

    private int pageWidth;

    private PdfDocument mPdfDocument;

    private int totalpages = 1;

    private String pdfPath;

    private List<Bitmap> mlist;


    public MyPrintDocumentAdapterEx(Context context, String pdfPath) {


        this.context = context;

        this.pdfPath = pdfPath;


    }


    @Override

    //在onLayout()方法中,你的适配器需要告诉系统框架文本类型,总页数等信息

    public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal,

                         LayoutResultCallback callback,

                         Bundle metadata) {


        mPdfDocument = new PrintedPdfDocument(context, newAttributes); //创建可打印PDF文档对象


        pageHeight = newAttributes.getMediaSize().ISO_A4.getHeightMils() * 72 / 1000;  //设置尺寸

        pageWidth = newAttributes.getMediaSize().ISO_A4.getWidthMils() * 72 / 1000;


        if (cancellationSignal.isCanceled()) {

            callback.onLayoutCancelled();

            return;

        }


       totalpages=2;


        if (totalpages > 0) {

            PrintDocumentInfo.Builder builder = new PrintDocumentInfo

                    .Builder("快速入门.pdf")

                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)

                    .setPageCount(totalpages);  //构建文档配置信息


            PrintDocumentInfo info = builder.build();

            callback.onLayoutFinished(info, true);

        } else {

            callback.onLayoutFailed("Page count is zero.");

        }

    }


    @Override

    //当需要将打印结果输出到文件中时,系统会调用onWrite()方法,该方法的参数指明要打印的页以及结果写入的文件,你的方法实现需要将页面的内容写入到一个多页面的PDF文档中,当这个过程完成时,需要调用onWriteFinished

    public void onWrite(final PageRange[] pageRanges, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal,

                        final WriteResultCallback callback) {


               for(int i=0;i<2;i++){

                PdfDocument.PageInfo newPage = new PdfDocument.PageInfo.Builder(pageWidth,

                        pageHeight, i).create();

                PdfDocument.Page page =

                        mPdfDocument.startPage(newPage);  //创建新页面


                if (cancellationSignal.isCanceled()) {  //取消信号

                    callback.onWriteCancelled();

                    mPdfDocument.close();

                    mPdfDocument = null;

                    return;

                }

                drawPage(page, i);  //将内容绘制到页面Canvas上

                mPdfDocument.finishPage(page);

          }


        try {

            mPdfDocument.writeTo(new FileOutputStream(

                    destination.getFileDescriptor()));

        } catch (IOException e) {

            callback.onWriteFailed(e.toString());

            return;

        } finally {

            mPdfDocument.close();

            mPdfDocument = null;

        }


        callback.onWriteFinished(pageRanges);

    }


    //页面绘制(渲染)

    private void drawPage(PdfDocument.Page page, int pagenumber) {

        Canvas canvas = page.getCanvas();


        /*

            Paint paint = new Paint();

           paint.setTextSize(100);

            canvas.drawText("打印测试",20,20,paint);

*/

        int titleBaseLine = 72;

        int leftMargin = 54;


        Paint paint = new Paint();

        paint.setColor(Color.RED);

        paint.setTextSize(36);

        canvas.drawText("Hello Title", leftMargin, titleBaseLine, paint);


        paint.setTextSize(11);

        canvas.drawText("Hello paragraph", leftMargin, titleBaseLine + 25, paint);


        paint.setColor(Color.BLUE);

        canvas.drawRect(100, 100, 172, 172, paint);

    }

}



1.打印网页 


PrintManager printManager = (PrintManager)mContext.getSystemService(Context.PRINT_SERVICE);

//打印任务的名字

String jobName = getString(R.string.app_name) + " 处方打印";

//这里需要webview创建PrintDocumentAdapter

PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter();

PrintAttributes.Builder builder = new PrintAttributes.Builder();

//选择A5纸张横向打印

builder.setMediaSize(PrintAttributes.MediaSize.ISO_A5.asLandscape());

printManager.print(jobName, printAdapter,builder.build());

上面的方法需要个你当前的webview,将当前的webview传给他他们,执行上面的方法就会跳转到系统的打印页面,非常简单


2、打印图片


try {

    PrintHelper printHelper = new PrintHelper(this);

    printHelper.printBitmap("jobName", BitmapFactory.decodeStream(getAssets().open("timg.jpg")));

} catch (IOException e) {

    e.printStackTrace(); 

}

直接传给该方法一个bitmap,就直接跳转到预览打印页面,你就可以选择相应的打印机进行打印了


打印PDF文件

赋予权限

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.qdian.testmyapp" >

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

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

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



    <application

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:roundIcon="@mipmap/ic_launcher_round"

        android:supportsRtl="true"

        android:requestLegacyExternalStorage="true"

        android:theme="@style/AppTheme" >

        <activity android:name=".MainActivity" >

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />


                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    </application>


</manifest>


Context.getExternalFilesDir() ;//取代android:requestLegacyExternalStorage="true"

动态获取权限

MainActivity.java


 private static final int REQUEST_EXTERNAL_STORAGE = 1;

    private static String[] PERMISSIONS_STORAGE = {

            Manifest.permission.READ_EXTERNAL_STORAGE,

            Manifest.permission.WRITE_EXTERNAL_STORAGE };


    public static void verifyStoragePermissions(Activity activity) {

        // Check if we have write permission

        int permission = ActivityCompat.checkSelfPermission(activity,

                Manifest.permission.WRITE_EXTERNAL_STORAGE);

        if (permission != PackageManager.PERMISSION_GRANTED) {

            // We don't have permission so prompt the user

            ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,

                    REQUEST_EXTERNAL_STORAGE);

        }

    }

@Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);


    // Example of a call to a native method

    TextView tv = (TextView) findViewById(R.id.sample_text);

    tv.setText(stringFromJNI());

    verifyStoragePermissions(this);

    findViewById(R.id.mybt).setOnClickListener(new View.OnClickListener() {

        @Override

        public void onClick(View v) {

            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);

            String[] strArray=new String[1];

            strArray[0]="application/pdf";

            intent.putExtra(Intent.EXTRA_MIME_TYPES,strArray);

            intent.setType("*/*");//无类型限制

            intent.addCategory(Intent.CATEGORY_OPENABLE);

            startActivityForResult(intent, 1);



        }

    });

    }

public static String getPath(Context context, Uri uri) throws URISyntaxException {


        if ("content".equalsIgnoreCase(uri.getScheme())) {

            String[] projection = { "_data" };

            Cursor cursor = null;

            try {


                    cursor = context.getContentResolver().query(uri, projection, null, null, null);

                int column_index = cursor.getColumnIndexOrThrow("_data");

                if (cursor.moveToFirst()) {


                    return cursor.getString(column_index);


                }


            } catch (Exception e) {


// Eat it Or Log it.


            }


        } else if ("file".equalsIgnoreCase(uri.getScheme())) {


            return uri.getPath();


        }

        return null;

    }


 @Override

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

                 if (resultCode == Activity.RESULT_OK) {

                     Uri uri = data.getData();

                     filePath = uri.getPath();

                     try {

                         filePath=getPath(MainActivity.this,uri);

                     } catch (URISyntaxException e) {

                         e.printStackTrace();

                     }

                     doPrint();                     

                 }



        }

void doPrint() {

 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);

        PrintAttributes.Builder builder = new PrintAttributes.Builder();

        builder.setColorMode(PrintAttributes.COLOR_MODE_COLOR);

        printManager.print("test pdf print", new MyPrintDocumentAdapterPDF(this,filePath), builder.build());



    }

MyPrintDocumentAdapterPDF.java



package com.qdian.testmyapp;

import android.content.Context;

import android.graphics.Bitmap;

import android.graphics.Canvas;

import android.graphics.Matrix;

import android.graphics.Paint;

import android.graphics.pdf.PdfDocument;

import android.graphics.pdf.PdfRenderer;

import android.os.Bundle;

import android.os.CancellationSignal;

import android.os.ParcelFileDescriptor;

import android.print.PageRange;

import android.print.PrintAttributes;

import android.print.PrintDocumentAdapter;

import android.print.PrintDocumentInfo;

import android.print.pdf.PrintedPdfDocument;


import java.io.File;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.util.ArrayList;

import java.util.List;


public class MyPrintDocumentAdapterPDF extends PrintDocumentAdapter {

    private Context context;

    private int pageHeight;

    private int pageWidth;

    private PdfDocument mPdfDocument;

    private int totalpages = 1;

    private String pdfPath;

    private List<Bitmap> mlist;


    public MyPrintDocumentAdapterPDF(Context context, String pdfPath) {


        this.context = context;

        this.pdfPath = pdfPath;


    }


    @Override

    //在onLayout()方法中,你的适配器需要告诉系统框架文本类型,总页数等信息

    public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal,

                         LayoutResultCallback callback,

                         Bundle metadata) {


        mPdfDocument = new PrintedPdfDocument(context, newAttributes); //创建可打印PDF文档对象


        pageHeight = newAttributes.getMediaSize().ISO_A4.getHeightMils() * 72 / 1000; //设置尺寸

        pageWidth = newAttributes.getMediaSize().ISO_A4.getWidthMils() * 72 / 1000;


        if (cancellationSignal.isCanceled()) {

            callback.onLayoutCancelled();

            return;

        }


        ParcelFileDescriptor mFileDescriptor = null;

        PdfRenderer pdfRender = null;

        PdfRenderer.Page page = null;

        try {

            mFileDescriptor = ParcelFileDescriptor.open(new File(pdfPath), ParcelFileDescriptor.MODE_READ_ONLY);

            if (mFileDescriptor != null)

                pdfRender = new PdfRenderer(mFileDescriptor);


            mlist = new ArrayList<>();


            if (pdfRender.getPageCount() > 0) {

                totalpages = pdfRender.getPageCount();

                for (int i = 0; i < pdfRender.getPageCount(); i++) {

                    if(null != page)

                        page.close();

                    page = pdfRender.openPage(i);

                    Bitmap bmp = Bitmap.createBitmap(page.getWidth()*2,page.getHeight()*2, Bitmap.Config.ARGB_8888);

                    page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);

                    mlist.add(bmp);

                }

            }

            if(null != page)

                page.close();

            if(null != mFileDescriptor)

                mFileDescriptor.close();

            if (null != pdfRender)

                pdfRender.close();

        } catch (FileNotFoundException e) {

            e.printStackTrace();

        } catch (IOException e) {

            e.printStackTrace();

        }


        if (totalpages > 0) {

            PrintDocumentInfo.Builder builder = new PrintDocumentInfo

                    .Builder("快速入门.pdf")

                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)

                    .setPageCount(totalpages); //构建文档配置信息


            PrintDocumentInfo info = builder.build();

            callback.onLayoutFinished(info, true);

        } else {

            callback.onLayoutFailed("Page count is zero.");

        }

    }


    @Override

    public void onWrite(final PageRange[] pageRanges, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal,

                        final WriteResultCallback callback) {

        for (int i = 0; i < totalpages; i++) {

            if (pageInRange(pageRanges, i)) //保证页码正确

            {

                PdfDocument.PageInfo newPage = new PdfDocument.PageInfo.Builder(pageWidth,

                        pageHeight, i).create();

                PdfDocument.Page page =

                        mPdfDocument.startPage(newPage); //创建新页面


                if (cancellationSignal.isCanceled()) { //取消信号

                    callback.onWriteCancelled();

                    mPdfDocument.close();

                    mPdfDocument = null;

                    return;

                }

                drawPage(page, i); //将内容绘制到页面Canvas上

                mPdfDocument.finishPage(page);

            }

        }


        try {

            mPdfDocument.writeTo(new FileOutputStream(

                    destination.getFileDescriptor()));

        } catch (IOException e) {

            callback.onWriteFailed(e.toString());

            return;

        } finally {

            mPdfDocument.close();

            mPdfDocument = null;

        }


        callback.onWriteFinished(pageRanges);

    }


    private boolean pageInRange(PageRange[] pageRanges, int page) {

        for (int i = 0; i < pageRanges.length; i++) {

            if ((page >= pageRanges[i].getStart()) &&

                    (page <= pageRanges[i].getEnd()))

                return true;

        }

        return false;

    }


    //页面绘制(渲染)

    private void drawPage(PdfDocument.Page page,int pagenumber) {

        Canvas canvas = page.getCanvas();

        if(mlist != null){

            Paint paint = new Paint();

            Bitmap bitmap = mlist.get(pagenumber);

            int bitmapWidth = bitmap.getWidth();

            int bitmapHeight = bitmap.getHeight();

            // 计算缩放比例

            float scale = (float)pageWidth/(float)bitmapWidth;

            // 取得想要缩放的matrix参数

            Matrix matrix = new Matrix();

            matrix.postScale(scale, scale);

            canvas.drawBitmap(bitmap,matrix,paint);

        }

    }


}


锋邢天下 用AndroidStudio创建so

Android开发中经常遇到so,下面介绍用Android Studio创建so。Android Studio版本3.0.1.


一 编写代码


1 创建java类myJNI.java,并且声明一个native方法HelloWorld。


  native方法由java声明,由c/c++实现。

1.png

  


2  编译myJNI.java,生成class文件myJNI.class


  打开命令行工具,cd进入myJNI.java所在的目录,然后用javac myJNI.java命令编译myJNI.java,成功后在myJNI.java目录下生成myJNI.class文件

2.png

  


3 生成.h文件。


  cd到F:\CreateSo\app\src\main\java目录,使用javah -jni 包名.类名 命令生成.h文件。示例项目包名com.createso,类名myJNI。成功后会在该目录生成com_createso_myJNI.h文件。

3.png

  


4 创建c代码文件


  在main目录下新建一个jni文件夹,新建一个文件命名为com_createso_myJNI.c,把com_createso_myJNI.h文件里的内容复制到main.c中,并且实现Java_com_createso_myJNI_HelloWorld方法。


   4.png

5.png


二 填写配置


1 在Android Studio里下载CMake和LLDB,File->Setting->Appearance->System Settings->Android SDK,点击SDK Tools标签页,勾选CMake和LLDB,点击OK开始下载。


2 在当前工程app的build.gradle的defaultConfig节点中加入:


  // 使用Cmake工具

  externalNativeBuild {

    cmake {

      cppFlags ""

      //生成多个版本的so文件

      abiFilters 'armeabi','armeabi-v7a','x86'

           }


            }


3 在build.gradle的android节点中,加入:


  //配置CMakeList.txt路径


  externalNativeBuild{


    cmake{


      path "CMakeLists.txt"


       }


            }


4 添加CMakeLists.txt文件到当前工程app的build.gradle文件同级目录下,CMakeLists.txt内容如下:



  cmake_minimum_required(VERSION 3.4.1)

  add_library( 

  # 设置so文件名称.

  TestSo



  # 设置这个so文件为共享.

  SHARED


  # 指向要编译的c文件.

  src/main/jni/chenxi_com_serialportjni_SerialPort.c)


  find_library(

  log-lib


  # Specifies the name of the NDK library that

  # you want CMake to locate.

  log )


  # Specifies libraries CMake should link to your target library. You

  # can link multiple libraries, such as libraries you define in this

  # build script, prebuilt third-party libraries, or system libraries.


  target_link_libraries( # Specifies the target library.

    # 指定目标库.

    TestSo


    # Links the target library to the log library

    # included in the NDK.

    ${log-lib} )


 三 编译输出SO


  如果项目太大编译一次时间太长,这儿我们只编译创建so的这个模块:Android Studio中点击Build->Make Module "app"


  编译完成后在F:\CreateSo\app\build\intermediates\cmake\debug\obj路径下生成各个平台的so,生成的so会在指定名字的前面加上lib,即"libTestSo"。



Android 生成.so文件

原文

Android中,我们经常会用到.so文件,.so文件是一个C/C++的函数库,Android中调用.so文件都是通过jni的方式。Android系统中每一个CPU架构对应一个ABI,目前有以下几种:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64,所以我们可以生成对应ABI的.so文件。


接下来看如何生成我们的.so文件。


一、NDK环境搭建

1、下载NDK

下载链接:https://developer.android.com/ndk/downloads/index.html

当然没有梯子的话可以baidu找一下。PS:建议下载r9+的版本


2、下载完后解压,并将其根目录配置到Android Studio中,打开Project Structure(win快捷键:ctrl+alt+shfit+s),如下图:

配置ndk_1

1.png

在红框中配置你的ndk根目录,然后ok确认。

3、打开项目根目录的local.properties文件,如下:

1.png

配置ndk_2


可以看到ndk的目录,说明添加成功了。


4、打开项目根目录的gradle.properties文件,添加android.useDeprecatedNdk=true,如下:

1.png

配置ndk_3

到此ndk环境搭建完毕。


二、Java、C/C++代码编写

1、创建一个JniUtil类,以实现jni调用


public class JniUtil {

    static {

        //jniutil这个参数,可根据需要任意修改

        System.loadLibrary("jniutil");

    }


    //java调C/C++中的方法都需要用native声明且方法名必须和C/C++的方法名一样

    public native String test();

}

然后Make Project:

Make Project

1.png

完成后会生成对应的class文件:

1.png


2、根据JniUtil.class生成.h文件

打开Android Studio的Terminal,切换到项目的app/src/main目录下,执行命令:

javah -d jni -classpath 编译后的class文件的绝对路径

如下图:

1.png

jni命令


即可生成在app/src/main目录下自动创建一个包含.h文件的jni文件夹:


1.png

.h文件


我们不需要对这个.h文件做任何修改,其内容如下:


#include <jni.h>


#ifndef _Included_com_othershe_jnitest_JniUtil

#define _Included_com_othershe_jnitest_JniUtil

#ifdef __cplusplus

extern "C" {

#endif

JNIEXPORT jstring JNICALL Java_com_othershe_jnitest_JniUtil_test

  (JNIEnv *, jobject);


#ifdef __cplusplus

}

#endif

#endif

3、编写jniutil.c

这里的jniutil文件名需要和JniUtil类中System.loadLibrary("jniutil");的参数一致。

jniutil.c具体的编写可根据自己的业务实现,这里仅做测试:



#include "com_othershe_jnitest_JniUtil.h"


JNIEXPORT jstring JNICALL Java_com_othershe_jnitest_JniUtil_test

        (JNIEnv *env, jobject obj) {

    return (*env)->NewStringUTF(env, "jni调用成功");

}

在jniutil.c中我们需要导入上边的.h文件,然后实现具体的test方法。


4、配置项目app目录下的build.gradle文件


defaultConfig {

        applicationId "com.othershe.jnitest"

        minSdkVersion 14

        targetSdkVersion 24

        versionCode 1

        versionName "1.0"


        ndk {

            moduleName "jniutil"

            abiFilters 'arm64-v8a', 'x86', 'armeabi-v7a'

        }

    }

其中ndk标签是新添加的,moduleName 的值同样为System.loadLibrary("jniutil");的参数。由于配置了abiFilters,则只会得到armeabi、x86、armeabi-v7a三种ABI对应的.so文件。


最后还需要在生成的jni文件夹下创建一个空的util.c文件,否则会有如下异常:

1.png


Exception

jni文件夹的结构如下:


jni目录结构

三、生成.so文件

简单的做下调用操作,运行项目看效果:


textView = (TextView) findViewById(R.id.test);

textView.setText(new JniUtil().test());

1.png


到这里就成功的实现了jni的调用。


说好的.so文件呢?看下图:



1.png


上边运行项目后就生成了对应ABI的.so文件。


四、使用生成的.so文件

上边的这些步骤只是帮助我们得到需要的.so文件,供自己的项目或给其它项目使用。所以使用阶段就简单多了:

首先在项目的app/src/main目录创建一个jniLibs文件夹,将生成的.so文件连同对应的所在的文件夹拷贝到创建的jniLibs中,如下图:

1.png

之前的的jni文件夹和build.gradle文件配置就不需要了。调用方法还和上边的一样。

生成的so文件

1.png


JAVA 大文件数据流分段输出,多段输出,支持视频图片等

为了防止直接请求文件而导致数据被采集,通过接口逻辑判断后再输出文件流的方式模拟完成直接请求文件的操作,支持大文件流操作


JAVA代码:


package lan.dk.podcastserver.utils.multipart;


import lan.dk.podcastserver.utils.MimeTypeUtils;

import org.apache.commons.lang3.StringUtils;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;


import javax.servlet.ServletOutputStream;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.*;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.nio.file.attribute.FileTime;

import java.time.LocalDateTime;

import java.time.ZoneId;

import java.time.ZoneOffset;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.List;


/**

 * Created by kevin on 10/02/15.

 */

public class MultipartFileSender {


    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    

    private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.

    private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.

    private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

    

    Path filepath;

    HttpServletRequest request;

    HttpServletResponse response;

    

    public MultipartFileSender() {

    }


    public static MultipartFileSender fromPath(Path path) {

        return new MultipartFileSender().setFilepath(path);

    }

    

    public static MultipartFileSender fromFile(File file) {

        return new MultipartFileSender().setFilepath(file.toPath());

    }


    public static MultipartFileSender fromURIString(String uri) {

        return new MultipartFileSender().setFilepath(Paths.get(uri));

    }


    //** internal setter **//

    private MultipartFileSender setFilepath(Path filepath) {

        this.filepath = filepath;

        return this;

    }

    

    public MultipartFileSender with(HttpServletRequest httpRequest) {

        request = httpRequest;

        return this;

    }

    

    public MultipartFileSender with(HttpServletResponse httpResponse) {

        response = httpResponse;

        return this;

    }

    

    public void serveResource() throws Exception {

        if (response == null || request == null) {

            return;

        }


        if (!Files.exists(filepath)) {

            logger.error("File doesn't exist at URI : {}", filepath.toAbsolutePath().toString());

            response.sendError(HttpServletResponse.SC_NOT_FOUND);

            return;

        }


        Long length = Files.size(filepath);

        String fileName = filepath.getFileName().toString();

        FileTime lastModifiedObj = Files.getLastModifiedTime(filepath);


        if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) {

            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

            return;

        }

        long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneOffset.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);

        String contentType = MimeTypeUtils.probeContentType(filepath);


        // Validate request headers for caching ---------------------------------------------------


        // If-None-Match header should contain "*" or ETag. If so, then return 304.

        String ifNoneMatch = request.getHeader("If-None-Match");

        if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) {

            response.setHeader("ETag", fileName); // Required in 304.

            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);

            return;

        }


        // If-Modified-Since header should be greater than LastModified. If so, then return 304.

        // This header is ignored if any If-None-Match header is specified.

        long ifModifiedSince = request.getDateHeader("If-Modified-Since");

        if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {

            response.setHeader("ETag", fileName); // Required in 304.

            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);

            return;

        }


        // Validate request headers for resume ----------------------------------------------------


        // If-Match header should contain "*" or ETag. If not, then return 412.

        String ifMatch = request.getHeader("If-Match");

        if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) {

            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);

            return;

        }


        // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.

        long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");

        if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {

            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);

            return;

        }


        // Validate and process range -------------------------------------------------------------


        // Prepare some variables. The full Range represents the complete file.

        Range full = new Range(0, length - 1, length);

        List<Range> ranges = new ArrayList<>();


        // Validate and process Range and If-Range headers.

        String range = request.getHeader("Range");

        if (range != null) {


            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.

            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {

                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.

                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);

                return;

            }


            String ifRange = request.getHeader("If-Range");

            if (ifRange != null && !ifRange.equals(fileName)) {

                try {

                    long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.

                    if (ifRangeTime != -1) {

                        ranges.add(full);

                    }

                } catch (IllegalArgumentException ignore) {

                    ranges.add(full);

                }

            }


            // If any valid If-Range header, then process each part of byte range.

            if (ranges.isEmpty()) {

                for (String part : range.substring(6).split(",")) {

                    // Assuming a file with length of 100, the following examples returns bytes at:

                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).

                    long start = Range.sublong(part, 0, part.indexOf("-"));

                    long end = Range.sublong(part, part.indexOf("-") + 1, part.length());


                    if (start == -1) {

                        start = length - end;

                        end = length - 1;

                    } else if (end == -1 || end > length - 1) {

                        end = length - 1;

                    }


                    // Check if Range is syntactically valid. If not, then return 416.

                    if (start > end) {

                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.

                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);

                        return;

                    }


                    // Add range.                    

                    ranges.add(new Range(start, end, length));

                }

            }

        }


        // Prepare and initialize response --------------------------------------------------------


        // Get content type by file name and set content disposition.

        String disposition = "inline";


        // If content type is unknown, then set the default value.

        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp

        // To add new content types, add new mime-mapping entry in web.xml.

        if (contentType == null) {

            contentType = "application/octet-stream";

        } else if (!contentType.startsWith("image")) {

            // Else, expect for images, determine content disposition. If content type is supported by

            // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.

            String accept = request.getHeader("Accept");

            disposition = accept != null && HttpUtils.accepts(accept, contentType) ? "inline" : "attachment";

        }

        logger.debug("Content-Type : {}", contentType);

        // Initialize response.

        response.reset();

        response.setBufferSize(DEFAULT_BUFFER_SIZE);

        response.setHeader("Content-Type", contentType);

        response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");

        logger.debug("Content-Disposition : {}", disposition);

        response.setHeader("Accept-Ranges", "bytes");

        response.setHeader("ETag", fileName);

        response.setDateHeader("Last-Modified", lastModified);

        response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);


        // Send requested file (part(s)) to client ------------------------------------------------


        // Prepare streams.

        try (InputStream input = new BufferedInputStream(Files.newInputStream(filepath));

             OutputStream output = response.getOutputStream()) {


            if (ranges.isEmpty() || ranges.get(0) == full) {


                // Return full file.

                logger.info("Return full file");

                response.setContentType(contentType);

                response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);

                response.setHeader("Content-Length", String.valueOf(full.length));

                Range.copy(input, output, length, full.start, full.length);


            } else if (ranges.size() == 1) {


                // Return single part of file.

                Range r = ranges.get(0);

                logger.info("Return 1 part of file : from ({}) to ({})", r.start, r.end);

                response.setContentType(contentType);

                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

                response.setHeader("Content-Length", String.valueOf(r.length));

                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.


                // Copy single part range.

                Range.copy(input, output, length, r.start, r.length);


            } else {


                // Return multiple parts of file.

                response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);

                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.


                // Cast back to ServletOutputStream to get the easy println methods.

                ServletOutputStream sos = (ServletOutputStream) output;


                // Copy multi part range.

                for (Range r : ranges) {

                    logger.info("Return multi part of file : from ({}) to ({})", r.start, r.end);

                    // Add multipart boundary and header fields for every range.

                    sos.println();

                    sos.println("--" + MULTIPART_BOUNDARY);

                    sos.println("Content-Type: " + contentType);

                    sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);


                    // Copy single part range of multi part range.

                    Range.copy(input, output, length, r.start, r.length);

                }


                // End with multipart boundary.

                sos.println();

                sos.println("--" + MULTIPART_BOUNDARY + "--");

            }

        }


    }


    private static class Range {

        long start;

        long end;

        long length;

        long total;


        /**

         * Construct a byte range.

         * @param start Start of the byte range.

         * @param end End of the byte range.

         * @param total Total length of the byte source.

         */

        public Range(long start, long end, long total) {

            this.start = start;

            this.end = end;

            this.length = end - start + 1;

            this.total = total;

        }


        public static long sublong(String value, int beginIndex, int endIndex) {

            String substring = value.substring(beginIndex, endIndex);

            return (substring.length() > 0) ? Long.parseLong(substring) : -1;

        }


        private static void copy(InputStream input, OutputStream output, long inputSize, long start, long length) throws IOException {

            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

            int read;


            if (inputSize == length) {

                // Write full range.

                while ((read = input.read(buffer)) > 0) {

                    output.write(buffer, 0, read);

                    output.flush();

                }

            } else {

                input.skip(start);

                long toRead = length;


                while ((read = input.read(buffer)) > 0) {

                    if ((toRead -= read) > 0) {

                        output.write(buffer, 0, read);

                        output.flush();

                    } else {

                        output.write(buffer, 0, (int) toRead + read);

                        output.flush();

                        break;

                    }

                }

            }

        }

    }

    private static class HttpUtils {


        /**

         * Returns true if the given accept header accepts the given value.

         * @param acceptHeader The accept header.

         * @param toAccept The value to be accepted.

         * @return True if the given accept header accepts the given value.

         */

        public static boolean accepts(String acceptHeader, String toAccept) {

            String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");

            Arrays.sort(acceptValues);


            return Arrays.binarySearch(acceptValues, toAccept) > -1

                    || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1

                    || Arrays.binarySearch(acceptValues, "*/*") > -1;

        }


        /**

         * Returns true if the given match header matches the given value.

         * @param matchHeader The match header.

         * @param toMatch The value to be matched.

         * @return True if the given match header matches the given value.

         */

        public static boolean matches(String matchHeader, String toMatch) {

            String[] matchValues = matchHeader.split("\\s*,\\s*");

            Arrays.sort(matchValues);

            return Arrays.binarySearch(matchValues, toMatch) > -1

                    || Arrays.binarySearch(matchValues, "*") > -1;

        }

    }

}


防止APK被二次打包

自己的程序内部在启动的时候可以通过获取APK本身的签名然后和正确的签名做对比来识别自己是否被二次打包

1.jpg

通过PackageManag对象可以获取APK自身的签名。

1.jpg

通过对签名的码的分解得到一串20左右的字符串,此字符串则是APK的签名的MD5值,通过获取的签名MD5值与正确的MD5值进行对比,就可以识别其APK是否被盗版



Android事件监听器和回调方法

事件是 Android 平台与用户交互的手段。当用户对手机进行操作时,会产生各种各样的输入事件,Android 框架捕获到这些事件,进而进行处理。


Android 平台提供了多种用于获取用户输入事件的方式,考虑到用户事件都是在特定的用户界面中产生的,因此 Android 选用特定 View 组件来获取用户输入事件的方式,由 View 组件提供事件的处理方法。这就是为什么 View 类内部带有处理特定事件的监听器。


事件监听器

监听器用于对特定事件进行监听,一旦监听到特定事件,则由监听器截获该事件,并回调自身的特定方法对事件进行处理。


在教程之前的实例中,我们使用的事件处理方式都是监听器。根据用户输入方式的不同,View 组件将截获的事件分为 6 种,对应以下 6 种事件监听器接口。


1) OnClickListener 接口


此接口处理的是单击事件,例如,在 View 上进行单击动作,在 View 获得焦点的情况下单击“确定”按钮或者单击轨迹球都会触发该事件。


当单击事件发生时,OnClickListener 接口会回调 public void onClick(View v) 方法对事件进行处理。其中参数 v 指的是发生单击事件的 View 组件。


2) OnLongClickListener 接口


此接口处理的是长按事件,当长时间按住某个 View 组件时触发该事件。


其对应的回调方法为 public boolean onLongClick(View v),当返回 true 时,表示已经处理完此事件,若事件未处理完,则返回 false,该事件还可以继续被其他监听器捕获并处理。


3) OnFocusChangeListener 接口


此接口用于处理 View 组件焦点改变事件。当 View 组件失去或获得焦点时会触发该事件。


其对应的回调方法为 public void onFocusChange(View v, Boolean hasFocus),其中参数 v 表示产生事件的事件源,hasFocus 表示事件源的状态,即是否获得焦点。


4) OnKeyListener 接口


此接口用于对手机键盘事件进行监听,当View获得焦点并且键盘被敲击时会触发该事件。


其对应的回调方法为 public boolean onKey(View v, int keyCode, KeyEvent event)。


方法参数说明:


keyCode 为键盘码。

event 为键盘事件封装类的对象。

5) OnTouchListener 接口


此接口用来处理手机屏幕事件,当在 View 的范围内有触摸、按下、抬起、滑动等动作时都会触发该事件,并触发该接口中的回调方法。


其对应的回调方法为 public boolean onTouch(View v, MotionEvent event),对应的参数同上。


6) OnCreateContextMenuListener 接口


此接口用于处理上下文菜单被创建的事件。


其对应的回调方法为 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info),其中参数 menu 为事件的上下文菜单,参数 info 是该对象中封装了有关上下文菜单的其他信息。


在之前Android菜单教程实例 MenusDemo 中(网址:http://c.biancheng.net/view/3035.html),创建上下文菜单使用的是 registerForContextMenu(View v)方法,其本质是为 View 组件 v 注册该接口,并实现了相应的回调方法。


回调事件响应

在 Android 框架中,除了可以使用监听器进行事件处理之外,还可以通过回调机制进行事件处理。


Android SDK 为 View 组件提供了 5 个默认的回调方法,如果某个事件没有被任意一个 View 处理,就会在 Activity 中调用响应的回调方法,这些方法分别说明如下。


名称说明/作用调用时间参数说明返回值说明

public boolean onKeyDown(int keyCode, KeyEvent event)接口 KeyEvent.Callback 中的抽象方法键盘按键被按下时由系统调用

keyCode 即键盘码,系统根据键盘码得知按下的是哪个按钮。


event为按钮事件的对象,包含触发事件的详细信息。例如事件的类型、状态等


true 已完成处理此事件

false 表示该事件还可以被其他监听器处理

public boolean onKeyUp(int keyCode, KeyEvent event)接口 KeyEvent.Callback中的抽象方法按钮向上弹起时被调用

keyCode 即键盘码,系统根据键盘码得知按下的是哪个按钮。


event为按钮事件的对象,包含触发事件的详细信息。例如事件的类型、状态等


true 代表已完成处理此事件

false 表示该事件还可以被其他监听器处理

public boolean onTouchEvent(MotionEvent event)方法在 View 中定义用户触摸屏幕时被自动调用event 为触摸事件封装类的对象,封装了该事件的相关信息

 

当用户触摸到屏幕,屏幕被按下时,MotionEvent.getAction()的值为 MotionEvent.ACTION_ DOWN;


 


当用户将触控物体离开屏幕时,MotionEvent.getAction() 的值为 MotionEvent.ACTION_UP;


 


当触控物体在屏幕上滑动时,MotionEvent.getAction() 的值为 MotionEvent.ACTION_MOVE。


true 表示事件处理完成

false 表示未完成

public boolean onTrackballEvent(MotionEvent event)处理手机中轨迹球的相关事件。

在 Activity 中重写,也可以在 View 中重写 event 为手机轨迹球事件封装类的对象。true 表示事件处理完成

false 表示未完成

protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)只能在 View 中重写,当 View 组件焦点改变时被自动调用 gainFocus 表示触发该事件的 View 是否获得了焦点,获得焦点为 true。

参数 direction 表示焦点移动的方向。

参数 previouslyFocusedRect 是在触发事件的 View 的坐标系中前一个获得焦点的矩形区域

界面事件响应实例

在之前的教程中,多次使用监听器对事件进行处理,应该此已经很熟悉了。


接下来我们将通过一个实例来演示回调事件响应的处理过程,该实例 EventDemo 的运行效果如图 1 所示。

 


实例EventDemo的运行效果

图 1  实例EventDemo的运行效果


其布局文件 main.xml 内容如下:


 

<?xml version="l.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:orientation="vertical">

 

<TextView

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="回调事件处理演示" />

 

    <LinearLayout

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:orientation="horizontal">

 

        <Button

android:id="@+id/button1"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:focusableInTouchMode="true"

android:text="按钮1" />

 

        <Button

android:id="@+id/button2"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:focusableInTouchMode="true"

android:text="按钮2" />

 

        <Button

android:id="@+id/button3"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:focusableInTouchMode="true"

android:text="按钮3" />

    </LinearLayout>

</LinearLayout>

当用户在屏幕上做移动触摸、单击按钮等操作时,主 Activity MainActivity 会捕获相应事件并进行处理,在 LogCat 中打印相关内容,运行效果如图 2 所示。

 


Activity EventDemo捕获事件

图 2  Activity EventDemo捕获事件


EventDemo.java代码如下:


纯文本复制

 

package introduction.android.notidemo;

 

import android.app.Activity;

import android.content.Context;

import android.graphics.Rect;

import android.os.Bundle;

import android.util.Log;

import android.view.KeyEvent;

import android.view.MotionEvent;

import android.view.View;

 

import android.view.View.OnFocusChangeListener;

import android.widget.Button;

import android.widget.Toast;

 

public class MainActivity extends Activity implements OnFocusChangeListener {

Button[] buttons = new Button[3];

 

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

 

buttons[0] = (Button) findViewById(R.id.button1);

buttons[1] = (Button) findViewById(R.id.button2);

buttons[2] = (Button) findViewById(R.id.button3);

for (Button button : buttons) {

button.setOnFocusChangeListener(this);

}

}

 

//按钮按下触发的事件

public boolean onKeyDown(int keyCode, KeyEvent event) {

switch (keyCode) {

 

case KeyEvent.KEYCODE_DPAD_UP:

DisplayInformation("按上下方向键,KEYCODE_DPAD_UP");

break;

case KeyEvent.KEYCODE_DPAD_DOWN:

DisplayInformation("按上下方向键,KEYCODE__DPAD_UP");

break;

}

return false;

}

 

//按钮弹起触发的案件

public boolean onKeyUp(int keyCode, KeyEvent event) {

switch (keyCode) {

 

case KeyEvent.KEYCODE_DPAD_UP:

DisplayInformation("松开上方向键,KEYCODE_DPAD_UP");

break;

case KeyEvent.KEYCODE_DPAD_DOWN:

DisplayInformation("松开下方向键,KEYCODE_DPAD_UP");

break;

}

return false;

}

 

//触摸事件

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

DisplayInformation("手指正在往屏幕上按下");

break;

 

case MotionEvent.ACTION_MOVE:

DisplayInformation("手指正在往屏幕上移动");

break;

case MotionEvent.ACTION_UP:

DisplayInformation("手指正在往屏幕上按抬起");

break;

}

return false;

 

}

 

//焦点事件

@Override

public void onFocusChange(View view, boolean arg1) {

switch (view.getId()) {

case R.id.button1:

DisplayInformation("第一个按钮获得了焦点");

break;

case R.id.button2:

DisplayInformation("第二个按钮获得了焦点");

break;

case R.id.button3:

DisplayInformation("第三个按钮获得了焦点");

break;

}

}

 

//显示Toast

public void DisplayInformation(String string) {

//Toast.makeText (EventDemo.this,string,Toast.LENGTH_SHORT).show();

Log.i("enentDemo", string);

}

}


蓝牙连接 ESC/POS 热敏打印机打印(蓝牙连接篇)

公司的一个手机端的 CRM 项目最近要增加小票打印的功能,就是我们点外卖的时候经常会见到的那种小票。这里主要涉及到两大块的知识:


蓝牙连接及数据传输

ESC/POS 打印指令

蓝牙连接不用说了,太常见了,这篇主要介绍这部分的内容。但ESC/POS 打印指令是个什么鬼?简单说,我们常见的热敏小票打印机都支持这样一种指令,只要按照指令的格式向打印机发送指令,哪怕是不同型号品牌的打印机也会执行相同的动作。比如打印一行文本,换行,加粗等都有对应的指令,这部分内容放在下一篇介绍。


本篇主要基于官方文档,相比官方文档,省去了大段的说明,更加便于快速上手。

demo及打印指令讲解请看下篇


1. 蓝牙权限

想要使用蓝牙功能,首先要在 AndroidManifest 配置文件中声明蓝牙权限:


<manifest> 

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

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

  ...

</manifest>

BLUETOOTH 权限只允许建立蓝牙连接以及传输数据,但是如果要进行蓝牙设备发现等操作的话,还需要申请 BLUETOOTH_ADMIN 权限。


2. 初始配置

这里主要用到一个类 BluetoothAdapter。用法很简单,直接看代码:


BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

if (mBluetoothAdapter == null) {

    // Device does not support Bluetooth

}

单例模式,全局只有一个实例,只要为 null,就代表设备不支持蓝牙,那么需要有相应的处理。

如果设备支持蓝牙,那么接着检查蓝牙是否打开:


if (!mBluetoothAdapter.isEnabled()) {

    Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

    startActivityForResult(intent, REQUEST_ENABLE_BT);

}

如果蓝牙未打开,那么执行 startActivityForResult() 后,会弹出一个对话框询问是否要打开蓝牙,点击`是`之后就会自动打开蓝牙。成功打开蓝牙后就会回调到 onActivityResult()。


除了主动的打开蓝牙,还可以监听 BluetoothAdapter.ACTION_STATE_CHANGED

广播,包含EXTRA_STATE和EXTRA_PREVIOUS_STATE两个 extra 字段,可能的取值包括 STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, and STATE_OFF。含义很清楚了,不解释。


3. 发现设备

初始化完成之后,蓝牙打开了,接下来就是扫描附近的设备,只需要一句话:


mBluetoothAdapter.startDiscovery();

不过这样只是开始执行设备发现,这肯定是一个异步的过程,我们需要注册一个广播,监听发现设备的广播,直接上代码:


private final BroadcastReceiver mReceiver = new BroadcastReceiver() {

    public void onReceive(Context context, Intent intent) {

        String action = intent.getAction();

        

        // 当有设备被发现的时候会收到 action == BluetoothDevice.ACTION_FOUND 的广播

        if (BluetoothDevice.ACTION_FOUND.equals(action)) {


            //广播的 intent 里包含了一个 BluetoothDevice 对象

            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);


            //假设我们用一个 ListView 展示发现的设备,那么每收到一个广播,就添加一个设备到 adapter 里

            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());

        }

    }

};

// 注册广播监听

IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);

registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

注释已经写的很清楚了,除了 BluetoothDevice.EXTRA_DEVICE 之外,还有一个 extra 字段 BluetoothDevice.EXTRA_CLASS, 可以得到一个 BluetoothClass 对象,主要用来保存设备的一些额外的描述信息,比如可以知道这是否是一个音频设备。


关于设备发现,有两点需要注意:


startDiscovery() 只能扫描到那些状态被设为 可发现 的设备。安卓设备默认是不可发现的,要改变设备为可发现的状态,需要如下操作:

Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);

//设置可被发现的时间,300s

intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);

startActivity(intent);

执行之后会弹出对话窗询问是否允许设备被设为可发现的状态,点击`是`之后设备即被设为可发现的状态。


startDiscovery()是一个十分耗费资源的操作,所以需要及时的调用cancelDiscovery()来释放资源。比如在进行设备连接之前,一定要先调用cancelDiscovery()

4. 设备配对与连接

4.1 配对

当与一个设备第一次进行连接操作的时候,屏幕会弹出提示框询问是否允许配对,只有配对成功之后,才能建立连接。

系统会保存所有的曾经成功配对过的设备信息。所以在执行startDiscovery()之前,可以先尝试查找已配对设备,因为这是一个本地信息读取的过程,所以比startDiscovery()要快得多,也避免占用过多资源。如果设备在蓝牙信号的覆盖范围内,就可以直接发起连接了。


查找配对设备的代码如下:


Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();

if (pairedDevices.size() > 0) {

    for (BluetoothDevice device : pairedDevices) {

        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());

    }

}

代码很简单,不解释了,就是调用BluetoothAdapter.getBondedDevices()得到一个 Set<BluetoothDevice> 并遍历取得已配对的设备信息。


4.2 连接

蓝牙设备的连接和网络连接的模型十分相似,都是Client-Server 模式,都通过一个 socket 来进行数据传输。那么作为一个 Android 设备,就存在三种情况:


只作为 Client 端发起连接

只作为 Server 端等待别人发起建立连接的请求

同时作为 Client 和 Server

因为是为了下一篇介绍连接热敏打印机打印做铺垫,所以这里先讲 Android 设备作为 Client 建立连接的情况。因为打印机是不可能主动跟 Android 设备建立连接的,所以打印机必然是作为 Server 被连接。


4.2.1 作为 Client 连接

首先需要获取一个 BluetoothDevice 对象。获取的方法前面其实已经介绍过了,可以通过调用 startDiscovery()并监听广播获得,也可以通过查询已配对设备获得。

通过 BluetoothDevice.createRfcommSocketToServiceRecord(UUID) 得到 BluetoothSocket 对象

通过BluetoothSocket.connect()建立连接

异常处理以及连接关闭

废话不多说,上代码:


private class ConnectThread extends Thread {

    private final BluetoothSocket mmSocket;

    private final BluetoothDevice mmDevice;

 

    public ConnectThread(BluetoothDevice device) {


        BluetoothSocket tmp = null;

        mmDevice = device;

        try {

            // 通过 BluetoothDevice 获得 BluetoothSocket 对象

            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);

        } catch (IOException e) { }

        mmSocket = tmp;

    }

     

    @Override

    public void run() {

        // 建立连接前记得取消设备发现

        mBluetoothAdapter.cancelDiscovery();

        try {

            // 耗时操作,所以必须在主线程之外进行

            mmSocket.connect();

        } catch (IOException connectException) {

            //处理连接建立失败的异常

            try {

                mmSocket.close();

            } catch (IOException closeException) { }

            return;

        }

        doSomething(mmSocket);

    }

 

    //关闭一个正在进行的连接

    public void cancel() {

        try {

            mmSocket.close();

        } catch (IOException e) { }

    }

}

device.createRfcommSocketToServiceRecord(MY_UUID) 这里需要传入一个 UUID,这个UUID 需要格外注意一下。简单的理解,它是一串约定格式的字符串,用来唯一的标识一种蓝牙服务。


Client 发起连接时传入的 UUID 必须要和 Server 端设置的一样!否则就会报错!


如果是连接热敏打印机这种情况,不知道 Server 端设置的 UUID 是什么怎么办?

不用担心,因为一些常见的蓝牙服务协议已经有约定的 UUID。比如我们连接热敏打印机是基于 SPP 串口通信协议,其对应的 UUID 是 "00001101-0000-1000-8000-00805F9B34FB",所以实际的调用是这样:


device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))

其他常见的蓝牙服务的UUID大家可以自行搜索。如果只是用于自己的应用之间的通信的话,那么理论上可以随便定义一个 UUID,只要 server 和 client 两边使用的 UUID 一致即可。更多关于 UUID 的介绍可以参考这里


4.2.2 作为 Server 连接

通过BluetoothAdapter.listenUsingRfcommWithServiceRecord(String, UUID)获取一个 BluetoothServerSocket 对象。这里传入的第一个参数用来设置服务的名称,当其他设备扫描的时候就会显示这个名称。UUID 前面已经介绍过了。

调用BluetoothServerSocket.accept()开始监听连接请求。这是一个阻塞操作,所以当然也要放在主线程之外进行。当该操作成功执行,即有连接建立的时候,会返回一个BluetoothSocket 对象。

调用 BluetoothServerSocket.close() 会关闭监听连接的服务,但是当前已经建立的链接并不会受影响。

还是看代码吧:


private class AcceptThread extends Thread {


    private final BluetoothServerSocket mmServerSocket;

 

    public AcceptThread() {


        BluetoothServerSocket tmp = null;

        try {

            // client 必须使用一样的 UUID !!!

            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);

        } catch (IOException e) { }

        mmServerSocket = tmp;

    }


    @Override

    public void run() {

        BluetoothSocket socket = null;

        //阻塞操作

        while (true) {

            try {

                socket = mmServerSocket.accept();

            } catch (IOException e) {

                break;

            }

            //直到有有连接建立,才跳出死循环

            if (socket != null) {

                //要在新开的线程执行,因为连接建立后,当前线程可能会关闭

                doSomething(socket);

                mmServerSocket.close();

                break;

            }

        }

    }

 

    public void cancel() {

        try {

            mmServerSocket.close();

        } catch (IOException e) { }

    }

}

5. 数据传输

终于经过了前面的4步,万事俱备只欠东风。而最后这一部分其实是最简单的,因为就只是简单的利用 InputStream 和OutputStream进行数据的收发。

示例代码:



private class ConnectedThread extends Thread {

    private final BluetoothSocket mmSocket;

    private final InputStream mmInStream;

    private final OutputStream mmOutStream;

 

    public ConnectedThread(BluetoothSocket socket) {

        mmSocket = socket;

        InputStream tmpIn = null;

        OutputStream tmpOut = null;

        //通过 socket 得到 InputStream 和 OutputStream

        try {

            tmpIn = socket.getInputStream();

            tmpOut = socket.getOutputStream();

        } catch (IOException e) { }

 

        mmInStream = tmpIn;

        mmOutStream = tmpOut;

    }

 

    public void run() {

        byte[] buffer = new byte[1024];  // buffer store for the stream

        int bytes; // bytes returned from read()

 

        //不断的从 InputStream 取数据

        while (true) {

            try {

                bytes = mmInStream.read(buffer);

                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)

                        .sendToTarget();

            } catch (IOException e) {

                break;

            }

        }

    }

 

    //向 Server 写入数据

    public void write(byte[] bytes) {

        try {

            mmOutStream.write(bytes);

        } catch (IOException e) { }

    }

 

    public void cancel() {

        try {

            mmSocket.close();

        } catch (IOException e) { }

    }

}


终于抽时间整了一个可以运行的demo出来,实现了以下功能:


检测蓝牙开启状态

显示已配对设备

连接打印机

打印测试,包括打印标题,打印两列三列文字,打印图片等

最终demo及打印的小票示例:


1.png

2.png

========================以下是原文=======================

1. 构造输出流

首先要明确一点,就是蓝牙连接打印机这种场景下,手机是 Client 端,打印机是 Server 端。


在上一篇的最后,我们从 BluetoothSocket 得到了一个OutputStream。这里我们做一层包装,得到一个OutputStreamWriter 对象:


OutputStreamWriter writer = new OutputStreamWriter(outputStream, "GBK");

这样做主要是为了后面可以直接输出字符串,不然只能输出 int 或 byte 数据;


2. 常用打印指令

手机通过蓝牙向打印机发送的都是纯字节流,那么打印机如何知道该打印的是一个文本,还是条形码,还是图片数据呢?这里就要介绍 ESC/POS 打印控制命令。


初始化打印机 :

1.png

初始化打印机指令

在每次打印开始之前要调用该指令对打印机进行初始化。向打印机发送这条指令对应的代码就是:


  protected void initPrinter() throws IOException {  

        writer.write(0x1B);  

        writer.write(0x40);  

        writer.flush();  

  }

打印文本:

没有对应指令,直接输出

protected void printText(String text) throws IOException {  

        writer.write(text);

        writer.flush();

    } 

设置文本对齐方式:

1.png

文本对齐方式指令

对应的发送指令的代码:


    /* 设置文本对齐方式

     * @param align 打印位置  0:居左(默认) 1:居中 2:居右 

     * @throws IOException 

     */  

    protected void setAlignPosition(int align) throws IOException {  

        writer.write(0x1B);  

        writer.write(0x61);  

        writer.write(align);  

        writer.flush();  

    }

与初始化指令不同的是,这条指令带有一个参数n。


换行和制表符:

直接输出对应的字符:

    protected void nextLine() throws IOException {  

        writer.write("\n");  

        writer.flush();  

    }


    protected void printTab(int length) throws IOException {  

        for (int i = 0; i < length; i++) {  

            writer.write("\t");  

        }  

        writer.flush();  

    }  

这两个指令在打印订单详情的时候使用最多。尤其是制表符,可以让每一列的文字对齐。


设置行间距:

1.png

设置行间距指令


n表示行间距为n个像素点,最大值256


protected void setLineGap(int gap) throws IOException {  

        writer.write(0x1B);  

        writer.write(0x33);  

        writer.write(gap);  

        writer.flush();  

}

这个指令在后面打印图片的时候会用到。


3. 打印图片

很多小票上面都会附上一个二维码,用户扫描之后,可以获得更多的信息。因为热敏打印机只能打印黑白两色,所以首先把图片转成纯黑白的,再调用图片打印指令进行打印。


3.1 打印图片指令

1.png

打印图片指令

这个指令的参数很多,一个一个来说:


m:取值十进制 0、1、32、33。设置打印精度,0、1对应每行8个点,32、33对应每行24个点,对应最高的打印精度(其实这里也没太搞清楚取值0、1或者取值32、33的区别,只要记住取值33,对应每行24个点,后面还有用)

n1, n2 : 表示图片的宽度,为什么有两个?其实只是分成了高位和低位两部分,因为每部分只有8bit,最大表示256。所以 n1 = 图片宽度 % 256,n2 = 图片宽度 / 256。假设图片宽300,那么n1=1,n2=44

d1 d2 ... dk 这部分就是转换成字节流的图像数据了

3.2 图片分辨率调整

如果分辨率过大,超过了打印机可打印的最大宽度,那么超出的部分将无法打印。我试验的这台最大宽度是 384 个像素点,超过这个宽度的数据无法被打印出来。所以在开始打印之前,我们需要调整图片的分辨率。代码如下:


    /**

     * 对图片进行压缩(去除透明度)

     *

     * @param bitmapOrg

     */

    public static Bitmap compressPic(Bitmap bitmap) {

        // 获取这个图片的宽和高

        int width = bitmap.getWidth();

        int height = bitmap.getHeight();

        // 指定调整后的宽度和高度

        int newWidth = 240;

        int newHeight = 240;

        Bitmap targetBmp = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);

        Canvas targetCanvas = new Canvas(targetBmp);

        targetCanvas.drawColor(0xffffffff);

        targetCanvas.drawBitmap(bitmap, new Rect(0, 0, width, height), new Rect(0, 0, newWidth, newHeight), null);

        return targetBmp;

    }

3.2 图片黑白化处理

因为能够打印的图像只有黑白两色,所以需要先做黑白化的处理。这一部分其实又细分为彩色图片->灰度图片,灰度图片->黑白图片两步。直接上代码:


    /**

     * 灰度图片黑白化,黑色是1,白色是0

     *

     * @param x   横坐标

     * @param y   纵坐标

     * @param bit 位图

     * @return

     */

    public static byte px2Byte(int x, int y, Bitmap bit) {

        if (x < bit.getWidth() && y < bit.getHeight()) {

            byte b;

            int pixel = bit.getPixel(x, y);

            int red = (pixel & 0x00ff0000) >> 16; // 取高两位

            int green = (pixel & 0x0000ff00) >> 8; // 取中两位

            int blue = pixel & 0x000000ff; // 取低两位

            int gray = RGB2Gray(red, green, blue);

            if (gray < 128) {

                b = 1;

            } else {

                b = 0;

            }

            return b;

        }

        return 0;

    }


    /**

     * 图片灰度的转化

     */

    private static int RGB2Gray(int r, int g, int b) {

        int gray = (int) (0.29900 * r + 0.58700 * g + 0.11400 * b);  //灰度转化公式

        return gray;

    }

其中的灰度化转换公式是一个广为流传的公式,具体原理不明。我们直接看灰度转化为黑白的函数 px2Byte(int x, int y, Bitmap bit)。对于一个 Bitmap 中的任意一个坐标点,取出其 RGB 三色信息后做灰度化处理,然后对于灰度小于128的,用黑色表示,灰度大于128的,用白色表示。


3.3 逐行打印图片

其实打印图片和打印文本是一样的,也是一行一行的打印。直接上代码吧,注释已经尽量详细了。


    /*************************************************************************

     * 假设一个240*240的图片,分辨率设为24, 共分10行打印

     * 每一行,是一个 240*24 的点阵, 每一列有24个点,存储在3个byte里面。

     * 每个byte存储8个像素点信息。因为只有黑白两色,所以对应为1的位是黑色,对应为0的位是白色

     **************************************************************************/

    /**

     * 把一张Bitmap图片转化为打印机可以打印的字节流

     *

     * @param bmp

     * @return

     */

    public static byte[] draw2PxPoint(Bitmap bmp) {

        //用来存储转换后的 bitmap 数据。为什么要再加1000,这是为了应对当图片高度无法      

        //整除24时的情况。比如bitmap 分辨率为 240 * 250,占用 7500 byte,

        //但是实际上要存储11行数据,每一行需要 24 * 240 / 8 =720byte 的空间。再加上一些指令存储的开销,

        //所以多申请 1000byte 的空间是稳妥的,不然运行时会抛出数组访问越界的异常。

        int size = bmp.getWidth() * bmp.getHeight() / 8 + 1000;

        byte[] data = new byte[size];

        int k = 0;

        //设置行距为0的指令

        data[k++] = 0x1B;

        data[k++] = 0x33;

        data[k++] = 0x00;

        // 逐行打印

        for (int j = 0; j < bmp.getHeight() / 24f; j++) {

            //打印图片的指令

            data[k++] = 0x1B;

            data[k++] = 0x2A;

            data[k++] = 33; 

            data[k++] = (byte) (bmp.getWidth() % 256); //nL

            data[k++] = (byte) (bmp.getWidth() / 256); //nH

            //对于每一行,逐列打印

            for (int i = 0; i < bmp.getWidth(); i++) {

                //每一列24个像素点,分为3个字节存储

                for (int m = 0; m < 3; m++) {

                    //每个字节表示8个像素点,0表示白色,1表示黑色

                    for (int n = 0; n < 8; n++) {

                        byte b = px2Byte(i, j * 24 + m * 8 + n, bmp);

                        data[k] += data[k] + b;

                    }

                    k++;

                }

            }

            data[k++] = 10;//换行

        }

        return data;

    }

4. 总结

用两篇介绍了一个比较冷门的应用,纯粹是因为自己花了很多时间去搞懂原理,所以希望记录下来。尤其是图片打印部分,废了好多纸啊哈哈哈,一个字节操作错误,打印出来就是一堆乱码。感觉和 java 的 .class 文件很像,每一个指令占用多少位,每一位表示什么都是严格规定好的,不能超出也不能缺少。

最后希望能帮到需要的人吧,感觉网上这部分资料还是比较少的。



Android Studio 代码混淆(你真的会混淆吗)

混淆规则

1.基本规则

两个常用的混淆命令,注意一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;


-keep class cn.hadcn.test.**

-keep class cn.hadcn.test.*


用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了


-keep class com.example.bean.** { *; }


在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extend,implement等这些Java规则。如下例子就避免所有继承Activity的类被混淆


# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆

# 因为这些子类都有可能被外部调用

-keep public class * extends android.app.Activity

-keep public class * extends android.app.Appliction

-keep public class * extends android.app.Service

-keep public class * extends android.content.BroadcastReceiver

-keep public class * extends android.content.ContentProvider

-keep public class * extends android.app.backup.BackupAgentHelper

-keep public class * extends android.preference.Preference

-keep public class * extends android.view.View

-keep public class com.android.vending.licensing.ILicensingService


2.什么时候不被混淆


一般以下情况都会不混淆:

1.使用了自定义控件那么要保证它们不参与混淆

2.使用了枚举要保证枚举不被混淆

3.对第三方库中的类不进行混淆

4.运用了反射的类也不进行混淆

5.使用了 Gson 之类的工具要使 JavaBean 类即实体类不被混淆

6.在引用第三方库的时候,一般会标明库的混淆规则的,建议在使用的时候就把混淆规则添加上去,免得到最后才去找

7.有用到 WebView 的 JS 调用也需要保证写的接口方法不混淆,原因和第一条一样

8.Parcelable 的子类和 Creator 静态成员变量不混淆,否则会产生 Android.os.BadParcelableException 异常


基本的混淆模板

最后提供一份基本的混淆模板,当然第三方库,或者上面提到的地方,根据自己的实际需求进行混淆



#############################################

#

# 对于一些基本指令的添加

#

#############################################

# 代码混淆压缩比,在0~7之间,默认为5,一般不做修改

-optimizationpasses 5


# 混合时不使用大小写混合,混合后的类名为小写

-dontusemixedcaseclassnames


# 指定不去忽略非公共库的类

-dontskipnonpubliclibraryclasses


# 这句话能够使我们的项目混淆后产生映射文件

# 包含有类名->混淆后类名的映射关系

-verbose


# 指定不去忽略非公共库的类成员

-dontskipnonpubliclibraryclassmembers


# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。

-dontpreverify


# 保留Annotation不混淆

-keepattributes *Annotation*,InnerClasses


# 避免混淆泛型

-keepattributes Signature


# 抛出异常时保留代码行号

-keepattributes SourceFile,LineNumberTable


# 指定混淆是采用的算法,后面的参数是一个过滤器

# 这个过滤器是谷歌推荐的算法,一般不做更改

-optimizations !code/simplification/cast,!field/*,!class/merging/*



#############################################

#

# Android开发中一些需要保留的公共部分

#

#############################################


# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆

# 因为这些子类都有可能被外部调用

-keep public class * extends android.app.Activity

-keep public class * extends android.app.Appliction

-keep public class * extends android.app.Service

-keep public class * extends android.content.BroadcastReceiver

-keep public class * extends android.content.ContentProvider

-keep public class * extends android.app.backup.BackupAgentHelper

-keep public class * extends android.preference.Preference

-keep public class * extends android.view.View

-keep public class com.android.vending.licensing.ILicensingService



# 保留support下的所有类及其内部类

-keep class android.support.** {*;}


# 保留继承的

-keep public class * extends android.support.v4.**

-keep public class * extends android.support.v7.**

-keep public class * extends android.support.annotation.**


# 保留R下面的资源

-keep class **.R$* {*;}


# 保留本地native方法不被混淆

-keepclasseswithmembernames class * {

    native <methods>;

}


# 保留在Activity中的方法参数是view的方法,

# 这样以来我们在layout中写的onClick就不会被影响

-keepclassmembers class * extends android.app.Activity{

    public void *(android.view.View);

}


# 保留枚举类不被混淆

-keepclassmembers enum * {

    public static **[] values();

    public static ** valueOf(java.lang.String);

}


# 保留我们自定义控件(继承自View)不被混淆

-keep public class * extends android.view.View{

    *** get*();

    void set*(***);

    public <init>(android.content.Context);

    public <init>(android.content.Context, android.util.AttributeSet);

    public <init>(android.content.Context, android.util.AttributeSet, int);

}


# 保留Parcelable序列化类不被混淆

-keep class * implements android.os.Parcelable {

    public static final android.os.Parcelable$Creator *;

}


# 保留Serializable序列化的类不被混淆

-keepclassmembers class * implements java.io.Serializable {

    static final long serialVersionUID;

    private static final java.io.ObjectStreamField[] serialPersistentFields;

    !static !transient <fields>;

    !private <fields>;

    !private <methods>;

    private void writeObject(java.io.ObjectOutputStream);

    private void readObject(java.io.ObjectInputStream);

    java.lang.Object writeReplace();

    java.lang.Object readResolve();

}


# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆

-keepclassmembers class * {

    void *(**On*Event);

    void *(**On*Listener);

}


# webView处理,项目中没有使用到webView忽略即可

-keepclassmembers class fqcn.of.javascript.interface.for.webview {

    public *;

}

-keepclassmembers class * extends android.webkit.webViewClient {

    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);

    public boolean *(android.webkit.WebView, java.lang.String);

}

-keepclassmembers class * extends android.webkit.webViewClient {

    public void *(android.webkit.webView, jav.lang.String);

}



Android Studio中代码混淆

我们辛辛苦苦开发一款app,如果被心怀叵测的人恶意反编译,会让人感到恶心至极!所以考虑到安全性和应用的私密性,在打包的时候,都会进行一些代码混淆处理,Android Studio(以下简称AS)中的其实已经为我们处理到了极致,我们只需要简单的配置就可以,下面就为大家在AS开发中如何完成代码的混淆进行详细介绍:



1.在 buildType中打开混淆的开关和指定混淆文件的路径:


buildTypes {

         release {

             // 不显示Log

             buildConfigField "boolean", "LOG_DEBUG", "false"

             signingConfig signingConfigs.release

             //apk瘦身,移除无用的resource文件

             shrinkResources false



             //是否开启代码混淆

             minifyEnabled true



             //此处是系统默认的混淆文件路径

             //proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

             //指定混淆配置文件的路径

             proguardFile '/Users/xxx/Documents/AndroidStudioworkspace/Project/app/proguard-rules.pro'

         }

     }


2.在项目中app目录下的proguard-rules.pro文件中添加以下内容:

#指定代码的压缩级别

 -optimizationpasses 5

 #包明不混合大小写

 -dontusemixedcaseclassnames

 #不去忽略非公共的库类

 -dontskipnonpubliclibraryclasses

  #优化  不优化输入的类文件

 -dontoptimize

  #预校验

 -dontpreverify

  #混淆时是否记录日志

 -verbose

  # 混淆时所采用的算法

 -optimizations !code/simplification/arithmetic,!field/*,!class/merging/*

 #保护注解

 -keepattributes *Annotation*

 # 保持哪些类不被混淆

 -keep public class * extends android.app.Fragment

 -keep public class * extends android.app.Activity

 -keep public class * extends android.app.Application

 -keep public class * extends android.app.Service

 -keep public class * extends android.content.BroadcastReceiver

 -keep public class * extends android.content.ContentProvider

 -keep public class * extends android.app.backup.BackupAgentHelper

 -keep public class * extends android.preference.Preference

 -keep public class com.android.vending.licensing.ILicensingService

 #如果有引用v4包可以添加下面这行

 -keep public class * extends android.support.v4.app.Fragment

 #忽略警告

 -ignorewarning

 #####################记录生成的日志数据,gradle build时在本项目根目录输出################

 #apk 包内所有 class 的内部结构

 -dump class_files.txt

 #未混淆的类和成员

 -printseeds seeds.txt

 #列出从 apk 中删除的代码

 -printusage unused.txt

 #混淆前后的映射

 -printmapping mapping.txt

 #####################记录生成的日志数据,gradle build时 在本项目根目录输出-end################

 ################<span></span>混淆保护自己项目的部分代码以及引用的第三方jar包library#########################

 #-libraryjars libs/umeng-analytics-v5.2.4.jar

 #-libraryjars libs/alipaysd<span></span>k.jar

 #<span></span>-libraryjars libs/alipaysecsdk.jar

 #-libraryjars libs/alipayutdid.jar

 #-libraryjars libs/wup-1.0.0-SNAPSHOT.jar

 #-libraryjars libs/weibosdkcore.jar

 #三星应用市场需要添加:sdk-v1.0.0.jar,look-v1.0.1.jar

 #-libraryjars libs/sdk-v1.0.0.jar

 #-libraryjars libs/look-v1.0.1.jar

 #我是以libaray的形式引用了一个图片加载框架,如果不想混淆 keep 掉

 -keep class com.nostra13.universalimageloader.** { *; }

 #友盟

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

 #支付宝

 -keep class com.alipay.android.app.IAliPay{*;}

 -keep class com.alipay.android.app.IAlixPay{*;}

 -keep class com.alipay.android.app.IRemoteServiceCallback{*;}

 -keep class com.alipay.android.app.lib.ResourceMap{*;}

 #信鸽推送

 -keep class com.tencent.android.tpush.**  {* ;}

 -keep class com.tencent.mid.**  {* ;}

 #自己项目特殊处理代码

 #忽略警告

 -dontwarn com.veidy.mobile.common.**

 #保留一个完整的包

 -keep class com.veidy.mobile.common.** {

     *;

  }

 -keep class  com.veidy.activity.login.WebLoginActivity{*;}

 -keep class  com.veidy.activity.UserInfoFragment{*;}

 -keep class  com.veidy.activity.HomeFragmentActivity{*;}

 -keep class  com.veidy.activity.CityActivity{*;}

 -keep class  com.veidy.activity.ClinikActivity{*;}

 #如果引用了v4或者v7包

 -dontwarn android.support.**

 ############<span></span>混淆保护自己项目的部分代码以及引用的第三方jar包library-end##################

 -keep public class * extends android.view.View {

     public <init>(android.content.Context);

     public <init>(android.content.Context, android.util.AttributeSet);

     public <init>(android.content.Context, android.util.AttributeSet, int);

     public void set*(...);

 }

 #保持 native 方法不被混淆

 -keepclasseswithmembernames class * {

     native <methods>;

 }

 #保持自定义控件类不被混淆

 -keepclasseswithmembers class * {

     public <init>(android.content.Context, android.util.AttributeSet);

 }

 #保持自定义控件类不被混淆

 -keepclasseswithmembers class * {

     public <init>(android.content.Context, android.util.AttributeSet, int);

 }

 #保持自定义控件类不被混淆

 -keepclassmembers class * extends android.app.Activity {

     public void *(android.view.View);

 }

 #保持 Parcelable 不被混淆

 -keep class * implements android.os.Parcelable {

   public static final android.os.Parcelable$Creator *;

 }

 #保持 Serializable 不被混淆

 -keepnames class * implements java.io.Serializable

 #保持 Serializable 不被混淆并且enum 类也不被混淆

 #-keepclassmembers enum * {

 #  public static **[] values();

 #  public static ** valueOf(java.lang.String);

 #}

 -keepclassmembers class * {

     public void *ButtonClicked(android.view.View);

 }

 #不混淆资源类

 -keepclassmembers class **.R$* {

     public static <fields>;

 }

 #避免混淆泛型 如果混淆报错建议关掉

 #–keepattributes Signature

 #移除log 测试了下没有用还是建议自己定义一个开关控制是否输出日志

 #-assumenosideeffects class android.util.Log {

 #    public static boolean isLoggable(java.lang.String, int);

 #    public static int v(...);

 #    public static int i(...);

 #    public static int w(...);

 #    public static int d(...);

 #    public static int e(...);

 #}


3.解压apk,获取到classes.dex文件(如果解压不了,先把apk后缀名改成zip。我用好压可以直接解压);


4.下载dex2jar, 链接:http://pan.baidu.com/s/1qX80c36 密码:dvbb ,把classes.dex文件放到 dex2jar 根目录,用dos定位到该目录,windows执行dos 命令:dex2jar.bat calsses.dex (Mac执行:sh dex2jar.sh classes.dex) 生成classes_dex2jar文件;


5. 下载并安装反编译工具jd-gui,链接:http://pan.baidu.com/s/1qY7uxwK密码:rk1h;


6.下载jd-gui并且安装,用jd-gui打开classes_dex2jar,点击列表类,如果出现abc等等类名表示混淆成功!


Android Studio 里面的引用第三方库总结

Android Studio引用第三方库很方便,只需要一句代码就可以搞定,几种引用第三方库的方式,总结一下:

方式:1:它就会自动把这个包下载下来,并且引用它。节省git空间,而且修改版本也很方便。

compile 'com.android.support:support-v4:23.3.0'

方式2:引用libs下所有jar包

compile fileTree(dir: 'libs', include: ['*.jar'])

方式3:引用一个jar

compile files('libs/fastjson-1.1.53.android.jar')

方式4:引用一个aar文件,注意并不能像 方式2 那样自动引用全部的aar,而需要对每个aar分别进行引用。

compile(name: 'aar_file_name', ext: 'aar')

方式5:引用库类型的项目

compile project(':xxxsdk')

方式6:仅仅在编译时使用,但最终不会被编译到apk或aar里

provided files('libs/glide-3.7.0.jar')

新版本使用 implementation代替compile,provided 用 compile only代替


Android Studio工程libs

.so文件和jar本地库都放在libs目录下

so根据cpu 不同版本放在不不同目录下

arm64-v8a

armeabi

armeabi-v7a

armeabii

x84

x86_64

说明

armeabiv-v7a: 第7代及以上的 ARM 处理器。2011年15月以后的生产的大部分Android设备都使用它.

arm64-v8a: 第8代、64位ARM处理器,很少设备,三星 Galaxy S6是其中之一。

armeabi: 第5代、第6代的ARM处理器,早期的手机用的比较多。

x86: 平板、模拟器用得比较多。

x86_64: 64位的平板。



AAR应用方式和使用本地仓库

引用第三方的aar,可以有三种方式来集成

  1. 将aar上传到远程仓库,然后直接在build.gradle中引用;
  2. 将aar放到项目的libs目录中,本地引用;
  3. 将aar放到本地仓库,然后直接在build.gradle中引用。

由于某些原因,同事无法上传aar,第一种方法无法使用。同事的aar也引用了很多模块提供的aar,如果使用第二种,那我这边还得另外跟着引用。作为嫌麻烦的人,我选择pass掉。所以最终只能使用第三种方式,即使用本地Maven仓库的方式。

使用本地Maven 仓库

首先在项目根目录创建repos目录,将aar以及pom按引用路径放在repos目录下。

1.png

然后在项目根目录的build.gradle文件中,引用新建的repos作为本地仓库

allprojects {

    repositories {

        google()

        jcenter()


//引用本地仓库repos

        jcenter{

            url rootProject.file('repos')

        }

    }


    buildscript {

        repositories {

        //引用本地仓库repos

            jcenter {

                url rootProject.file('repos')

            }

        }

    }

}


最后直接在app目录的build.gradle文件中引入我们的本地aar就行了。这样就会根据aar的pom文件,自动引用aar需要的依赖,不用我们在build.gradle文件中一个一个引用了。

dependencies {


    ...


    implement 'cloudon:xxx:0.0.1'


}


扩展

在实际开发中,如果遇到多个项目需要使用同个aar,然后这个aar同样没上传到远程仓库。可以将aar放到本地的公共目录,然后将根目录的maven的url指定到那个目录路径即可,比如:


allprojects {

    repositories {

        google()

        jcenter()


//引用本地仓库repos

        jcenter{

            url 'file:///D:/test'

        }

    }


    buildscript {

        repositories {

        //引用本地仓库repos

            jcenter {

                url 'file:///D:/test'

            }

        }

    }

}



AndroidStudio简单的apk混淆

打包APK又一个看起来难 却不难并且不可或缺的标配,为什么这样说呢?由于没有混淆,你的代码被别人反编译你的代码将像一个裸奔的人一样展示在别人的面前。你的APP的安全将得不到保证。Android搞的混淆就是为了解决此类的问题。当然,很重要的东西还是建议用JNI完毕。

第一步: 我们要了解build.gradle文件里须要做什么配置



buildTypes {

    release {

        // 不显示Log

        buildConfigField "boolean", "LOG_DEBUG", "false"


        //混淆

        minifyEnabled true


        //Zipalign优化

        zipAlignEnabled true



        // 移除没用的resource文件

        shrinkResources true

        //混淆文件

        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        signingConfig signingConfigs.config

    }

    debug {

        signingConfig signingConfigs.config

    }

}

第二步:我们到proguard-rules.pro文件里配置那些混淆 那些不



-optimizationpasses 10                                                           # 指定代码的压缩级别

-dontusemixedcaseclassnames                                                     # 是否使用大写和小写混合

#-dontskipnonpubliclibraryclasses                                                # 是否混淆第三方jar

-dontpreverify                                                                  # 混淆时是否做预校验

-verbose                                                                        # 混淆时是否记录日志

-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*        # 混淆时所採用的算法


-keep public class * extends android.app.Activity                               # 保持哪些类不被混淆

-keep public class * extends android.app.Application                            # 保持哪些类不被混淆

-keep public class * extends android.app.Service                                # 保持哪些类不被混淆

-keep public class * extends android.content.BroadcastReceiver                  # 保持哪些类不被混淆

-keep public class * extends android.content.ContentProvider                    # 保持哪些类不被混淆

-keep public class * extends android.app.backup.BackupAgentHelper               # 保持哪些类不被混淆

-keep public class * extends android.preference.Preference                      # 保持哪些类不被混淆

-keep public class com.android.vending.licensing.ILicensingService              # 保持哪些类不被混淆


-keepclasseswithmembernames class * {                                           # 保持 native 方法不被混淆

    native <methods>;

}


-keepclasseswithmembers class * {                                               # 保持自己定义控件类不被混淆

    public <init>(android.content.Context, android.util.AttributeSet);

}


-keepclasseswithmembers class * {

    public <init>(android.content.Context, android.util.AttributeSet, int);     # 保持自己定义控件类不被混淆

}


-keepclassmembers class * extends android.app.Activity {                        # 保持自己定义控件类不被混淆

   public void *(android.view.View);

}


-keepclassmembers enum * {                                                      # 保持枚举 enum 类不被混淆

    public static **[] values();

    public static ** valueOf(java.lang.String);

}


-keep class * implements android.os.Parcelable {                                # 保持 Parcelable 不被混淆

  public static final android.os.Parcelable$Creator *;

}


#-keep class MyClass;                                                            # 保持自己定义的类不被混淆

#假设有引用v4包能够加入以下这行

-keep class android.support.v4.** { *; }

-keep public class * extends android.support.v4.**

-keep public class * extends android.app.Fragment

以上通常是我们项目中必须设置的基本混淆配置


#假设引用了v4或者v7包,能够忽略警告,由于用不到android.support

-dontwarn android.support.**


#保持自己定义组件不被混淆

-keep public class * extends android.view.View {

    public <init>(android.content.Context);

    public <init>(android.content.Context, android.util.AttributeSet);

    public <init>(android.content.Context, android.util.AttributeSet, int);

    public void set*(...);

}


#保持 Serializable 不被混淆

-keepnames class * implements java.io.Serializable


#保持 Serializable 不被混淆而且enum 类也不被混淆

-keepclassmembers class * implements java.io.Serializable {

    static final long serialVersionUID;

    private static final java.io.ObjectStreamField[] serialPersistentFields;

    private void writeObject(java.io.ObjectOutputStream);

    private void readObject(java.io.ObjectInputStream);

    java.lang.Object writeReplace();

    java.lang.Object readResolve();

}


-keepclassmembers class * {

    public void *ButtonClicked(android.view.View);

}


#不混淆资源类

-keepclassmembers class **.R$* {

    public static <fields>;

}


#xUtils(保持注解,及使用注解的Activity不被混淆。不然会影响Activity中你使用注解相关的代码无法使用)

-keep class * extends java.lang.annotation.Annotation {*;}

-keep class com.otb.designerassist.activity.** {*;}


#自己项目特殊处理代码(这些地方我使用了Gson类库和注解,所以不希望被混淆。以免影响程序)

-keep class com.otb.designerassist.entity.** {*;}

-keep class com.otb.designerassist.http.rspdata.** {*;}

-keep class com.otb.designerassist.service.** {*;}


# 以libaray的形式引用的图片载入框架,不想混淆(注意。此处不是jar包形式,想混淆去掉"#")

-keep class com.nostra13.universalimageloader.** { *; }


###-------- Gson 相关的混淆配置--------

-keepattributes Signature

-keepattributes *Annotation*

-keep class sun.misc.Unsafe { *; }


###-------- pulltorefresh 相关的混淆配置---------

-dontwarn com.handmark.pulltorefresh.library.**

-keep class com.handmark.pulltorefresh.library.** { *;}

-dontwarn com.handmark.pulltorefresh.library.extras.**

-keep class com.handmark.pulltorefresh.library.extras.** { *;}

-dontwarn com.handmark.pulltorefresh.library.internal.**

-keep class com.handmark.pulltorefresh.library.internal.** { *;}


###---------  reservoir 相关的混淆配置-------

-keep class com.anupcowkur.reservoir.** { *;}



###-------- ShareSDK 相关的混淆配置---------

-keep class cn.sharesdk.** { *; }

-keep class com.sina.sso.** { *; }



###--------------umeng 相关的混淆配置-----------

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

-keep class com.umeng.analytics.** { *; }

-keep class com.umeng.common.** { *; }

-keep class com.umeng.newxp.** { *; }



###-----------MPAndroidChart图库相关的混淆配置------------

-keep class com.github.mikephil.charting.** { *; }

#微信支付

-keep class com.tencent.mm.sdk.** {*;}

以上是一些第三方使用的jar包等 混淆配置

第一个问题:




这样的是包的警告  这里说明一下 dontwarn 是告诉打包工具。这个包别警告了。 keep是保持包里面的类和方法。


apk包解压,修改,和重新压缩

1. 安装apktool

2. 解压apk包

apktool d apkname.apk foldername

apkname.apk 表示要进行反编译的APK文件

foldername表示反编译后文件存放的目录

3. 修改内容

4. 压缩apk包

apktool b foldername -o new.apk

5. 签名

keytool -genkey -alias android.keystore -keyalg RSA -validity 20000 -keystore android.keystore

jarsigner -verbose -keystore keystore文件路径 -signedjar 签名后生成的apk路径 待签名的apk路径 别名



对于已经签名好的APK,我们可能因为修复问题、逆向等原因需要替换其中的so或者jar之类的部分,如果是本地测试替换so,倒是可以通过Root设备来直接替换。但如果是Java部分或者非Root设备就不能直接换了,要重新签名打包一下APK。

下面记录一下重新替换so再打包签名的办法:

(1)下载ApkTool工具,该工具用于解包、重新打包都非常好用

https://ibotpeaches.github.io/Apktool/install/

下载其中的 apktool_x.x.x.jar 就可以了。


(2)先解包:对已经签名的APK执行如下命令

java  -jar  apktool的jar文件  d(表示反编译)  要解包的apk  -o(输出)  输出文件名

# 例如(默认会输出同名文件夹到当前同级路径):java -jar apktool_2.3.4.jar d Auto_480_release.apk


(3)替换输出文件夹中的内容,例如替换so文件到文件夹内


(4)再打包:对刚才的输出目录重新打包为APK


# 针对上面输出目录重新打包,默认会输出到dist子目录(会有提示):java -jar apktool_2.3.4.jar b Auto_480_release

(5)在输出的dist目录中可以看到重新打包好的APK,注释是没有签名的,要重新签一下名:

jarsigner -keystore 签名文件完整路径 -signedjar 要输出的签名APK名 上一步打包好的APK路径 签名的别名

# 例如(密码:123456):jarsigner -keystore /Users/kuliuheng/workspace/androidWorkspace/android.keystore -signedjar Auto_480_release_signed.apk Auto_480_release.apk testalias

(6)验证签名:



在Android Studio中调用so中的方法

本节用的so是上节用Android Studio创建的so。想在Android Studio中调用so中的方法,需要先引用so。Android Studio中引用so的方法有二种,下面开始介绍。

一 引用so

   在app/src/main目录下新建Directory,命名文件夹为jniLIB(文件名不能错),把so文件放进去 ,如图:

  1.png

二 编写java代码调用so中方法

  ①在代码中引用so

  创建myJNI.java文件,用System.loadLibrary加载so,同时声明so中的HelloWorld方法:

  2.png

  ②在代码中调用so中方法

  在MainActivity.java中调用so中的HelloWorld方法

  3.png

三 运行程序

  用模拟器或连接手机调试程序,可见打印出来的log

  4.png

运行同时,生成了apk,so被打包到apk里,我们可以用这个apk去反逆向so文件。


Android低功耗蓝牙通讯

1.设置权限

step1、在AndroidManifest.xml中声明权限

  1. <!-- 蓝牙所需权限 -->
  2. <uses-permission android:name="android.permission.BLUETOOTH" />
  3. <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />


  • 第一个权限是允许程序连接到已配对的蓝牙设备。
  • 第二个权限是允许程序发现和配对蓝牙设备。

  • 因为只有在API18(Android4.3)以上的手机才支持ble开发,所以还要声明一个feature。

  1. <uses-feature
  2. android:name="android.hardware.bluetooth_le"
  3. android:required="true" />


  • required为true时,应用只能在支持BLE的Android设备上安装运行
  • required为false时,Android设备均可正常安装运行,需要在代码运行时判断设备是否支持BLE。

  • 注意:还得写上定位权限,要不然有的机型扫描不到ble设备。

  1. <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
  2. <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>



step2、获取蓝牙适配器

BluetoothManager  mBluetoothManager =(BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);

  BluetoothAdapter  mBluetoothAdapter = mBluetoothManager.getAdapter();

如果mBluetoothAdapter为空,是因为手机蓝牙不支持与ble设备通讯,换句话说就是安卓手机系统在4.3以下了


step3、判断手机蓝牙是否被打开

mBluetoothAdapter.isEnabled()

  • 如果返回true,这个时候就可以扫描了
  • 如果返回false,这时候需要打开手机蓝牙。 可以调用系统方法让用户打开蓝牙。


2、搜索蓝牙

step1、开始扫描


new Handler().postDelayed(new Runnable() {

                @Override

                public void run() {

                    mBluetoothAdapter.stopLeScan(mLeScanCallback);

                }

            }, 1000 * 10);

 

UUID[] serviceUuids = {UUID.fromString(service_uuid)};

mBluetoothAdapter.startLeScan(serviceUuids, mLeScanCallback);

  • startLeScan中,第一个参数是只扫描UUID是同一类的ble设备,第二个参数是扫描到设备后的回调。
  • 因为蓝牙扫描比较耗电,建议设置扫描时间,一定时间后停止扫描。

  • 如果不需要过滤扫描到的蓝牙设备,可用mBluetoothAdapter.startLeScan(mLeScanCallback);进行扫描。

Step2.扫描的回调


//蓝牙扫描回调接口

private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback(){

        @Override

        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {

            if (device.getName() == null) {

                return;

            }

            Log.e("--->搜索到的蓝牙名字:", device.getName());

            //可以将扫描的设备弄成列表,点击设备连接,也可以根据每个设备不同标识,自动连接。

 

        }

    };

3、连接蓝牙

step1、获取设备的mac地址,然后连接

//获取所需地址

  String mDeviceAddress = device.getAddress();

  BluetoothGatt mBluetoothGatt = device.connectGatt(context, false, mGattCallback);

step2、onConnectionStateChange()被调用

  • 连接状态改变时,mGattCallback中onConnectionStateChange()方法会被调用,当连接成功时,需要调用 
    mBluetoothGatt.discoverServices();
    去获取服务。

step3、onServicesDiscovered()被调用

  • 调用mBluetoothGatt.discoverServices();方法后,onServicesDiscovered()这个方法会被调用,说明发现当前设备了。然后我们就可以在里面去获取BluetoothGattService和BluetoothGattCharacteristic。

  • 下面就是mGattCallback回调方法。

// BLE回调操作

    private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {

 

        @Override

        public void onConnectionStateChange(BluetoothGatt gatt, int status,int newState){

            super.onConnectionStateChange(gatt, status, newState);

 

            if (newState == BluetoothProfile.STATE_CONNECTED) {

                // 连接成功

 

                mBluetoothGatt.discoverServices();

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {

                // 连接断开

               Log.d("TAG","onConnectionStateChange fail-->" + status);

            }

        }

 

        @Override

        public void onServicesDiscovered(BluetoothGatt gatt, int status) {

            super.onServicesDiscovered(gatt, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {

                //发现设备,遍历服务,初始化特征

                initBLE(gatt);

            } else {

               Log.d("TAG","onServicesDiscovered fail-->" + status);

            }

        }

 

        @Override

        public void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status){

            super.onCharacteristicRead(gatt, characteristic, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {

                // 收到的数据

                byte[] receiveByte = characteristic.getValue();

 

            }else{

               Log.d("TAG","onCharacteristicRead fail-->" + status);

            }

        }

 

        @Override

        public void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic){

            super.onCharacteristicChanged(gatt, characteristic);

            //当特征中value值发生改变

        }

 

        /**

         * 收到BLE终端写入数据回调

         * @param gatt

         * @param characteristic

         * @param status

         */

        @Override

        public void onCharacteristicWrite(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status) {

            super.onCharacteristicWrite(gatt, characteristic, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {

             // 发送成功

 

            } else {

             // 发送失败

            }

        }

 

        @Override

        public void onDescriptorWrite(BluetoothGatt gatt,

                                      BluetoothGattDescriptor descriptor, int status) {

            super.onDescriptorWrite(gatt, descriptor, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {

 

            }

        }

 

        @Override

        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {

            super.onReadRemoteRssi(gatt, rssi, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {

 

            }

        }

 

        @Override

        public void onDescriptorRead(BluetoothGatt gatt,BluetoothGattDescriptor descriptor, int status) {

            super.onDescriptorRead(gatt, descriptor, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {

 

            }

        }

    };

4、获取特征

step1、ble设备相关的UUID

//写通道uuid

    private static final UUID writeCharactUuid = UUID.fromString("0000fff6-0000-1000-8000-00805f9b34fb");

    //通知通道 uuid

    private static final UUID notifyCharactUuid =UUID.fromString( "0000fff7-0000-1000-8000-00805f9b34fb");

  • 不同的ble设备的UUID不相同,请根据自己的设备初始化UUID。

step2、获取bluetoothGattCharacteristic(因为有的设备可能存在双服务的情况,所以这里遍历所有服务)

//初始化特征

    public void initBLE(BluetoothGatt gatt) {

        if (gatt == null) {

            return;

        }

        //遍历所有服务

        for (BluetoothGattService BluetoothGattService : gatt.getServices()) {

            Log.e(TAG, "--->BluetoothGattService" + BluetoothGattService.getUuid().toString());

 

            //遍历所有特征

            for (BluetoothGattCharacteristic bluetoothGattCharacteristic : BluetoothGattService.getCharacteristics()) {

                Log.e("---->gattCharacteristic", bluetoothGattCharacteristic.getUuid().toString());

 

                String str = bluetoothGattCharacteristic.getUuid().toString();

                if (str.equals(writeCharactUuid)) {

                    //根据写UUID找到写特征

                    mBluetoothGattCharacteristic = bluetoothGattCharacteristic;

                } else if (str.equals(notifyCharactUuid)) {

                    //根据通知UUID找到通知特征

                    mBluetoothGattCharacteristicNotify = bluetoothGattCharacteristic;

                }

            }

        }

    }

step3、开启通知

  • 设置开启之后,才能在onCharacteristicRead()这个方法中收到数据。

mBluetoothGatt.setCharacteristicNotification(mGattCharacteristicNotify, true);

5、发送消息


 mGattCharacteristicWrite .setValue(sData);

    if (mBluetoothGatt != null) {

        mBluetoothGatt.setCharacteristicNotification(notifyCharactUuid , true);

        mBluetoothGatt.writeCharacteristic(mGattCharacteristicWrite );

    }

6、接收消息

  • 接收到数据后,mGattCallback 中的onCharacteristicRead()这个方法会被调用。

@Override

        public void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status){

            super.onCharacteristicRead(gatt, characteristic, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {

                // 收到的数据

                byte[] receiveByte = characteristic.getValue();

 

            }else{

               Log.d("TAG","onCharacteristicRead fail-->" + status);

            }

        }

7、释放资源

  • 断开连接、关闭资源。

public boolean disConnect() {

        if (mBluetoothGatt != null) {

            mBluetoothGatt.disconnect();

            mBluetoothGatt.close();

            mBluetoothGatt = null;

            return true;

        }

        return false;

    }

五、开发中踩过的坑

  1. 通知开启后,才能读到数据,否则读不到。
  2. 发送数据时,如果一包数据超过20字节,需要分包发送,一次最多发送二十字节。
  3. 接收数据时,一次最多也只接收20字节的数据,需要将接收到的数据拼接起来,在数据的结尾弄一个特定的标识,去判断数据是否接受完毕。
  4. 每次发送数据或者数据分包发送时, 操作间要有至少15ms的间隔。
  5. 最近公司来了个新的蓝牙产品,发现获取不到需要的特征,后来打断点,发现他们蓝牙设备的通知特征根本没有,是他们给错协议了。。。所以建议各位开发的时候,如果一直连接失败,也可以查看一下写特征和通知特征是否为空,是不是卖家搞错了,协议和产品不匹配。(当然,这样马虎的卖家估计是少数)。
  6. 又补充来了!这个蓝牙如果出现扫描不到的情况,那是因为手机没有开启定位权限,清单文件中写上定位权限,代码中在动态获取下就OK了。



直接用本地源码级库

有一个自定义的控件类库,需要在不同的项目中使用,我可以将类库拷贝到这些项目中,项目结构如下:

1.png

然后在app/build.gradle中添加依赖implementation project(’:OXViewLib’),这样就可以在项目中使用了


Android Studio 添加依赖库三种方式

11


前置知识点:AndroidStudio中项目组织方式,最高层为Project(虽然结构层次和Eclipse里的workplace有些相似,但还是有很大区别的),下面可以包括很多module,每个module可完全独立作为一个项目,运行处一个APK。(这在结构层次上又相当于eclipse里的project)

经过实践总结,以gradle为构建工具的AndroidStudio在依赖方面可以分为

1、库依赖(library)

2、模块依赖(module)

简单区分,模块就是源代码模块,库依赖就打包好的jar、aar文件

以上两种每种又可以分为内部和外部。

1.png

2.png

一种一种来,先说库依赖

A、内部库

这个应该是从eclipse那边延续过来的,算是对eclipse的兼容,指位于/Project/module/libs/下的jar、aar文件

使用方法:这种依赖比较简单,直接放在指定目录就好了,在模块配置/Project/module/build.gradle中的dependencies标签下加入


compile fileTree(include: ['*.jar'], dir: 'libs')

compile(name: 'FileSelector-release', ext: 'aar')


androidstudio会自动加载指定目录下的依赖库

B、外部库

这个是gradle的标准依赖方式,导入、管理、升级等都非常便捷

使用方法:在项目配置文件(顶级gradle文件/Project/build.gradle)中声明中心仓库,自动生成的项目中,AS会默认配置好,默认中心仓库为jcenter,也可以使用maven。

在module配置文件里(/Project/module/build.gradle)的dependencies标签下有类似


compile 'com.google.code.gson:gson:2.4'

AS会从中心仓库下载相应版本的库文件,实际文件一般在/用户文件夹/.gradle\caches\modules-2\files-2.1


并在工程结构的ExternalLibraries中列出,这也就是为什么叫这种为“外部库”

这里的引入配置代码需要库的提供方提供,一般gitbug的项目如果同时支持gradle构建方式引用,会在readme里面说明,如果没在github上,可以去中央仓库去搜,中央仓库的位置是可以从配置文件里追溯源码追到的。jcenter:

https://jcenter.bintray.com/


C、内部模块

前面说了,一个Project下面可以包括很多Module,这些module可以是相互完全独立的,也可以是被依赖的。

使用方法:

如果希望一个module被一个或者多个其他的Module依赖,那么,需要在该module的build.gradle文件把当前模块声明为Library

即不能用

apply plugin: 'com.android.application'

要用

apply plugin: 'com.android.library'


好吧,这个又类似于eclipse里面设置某个eclipse工程“Is Library”
在顶层工程目录下的settings.gradle中include模块名
include ':app',':module-name'


D、外部模块(这种方式,是我自己YY出来,经过验证可行的)
前面说,AndroidStudio组织结构顶层是一个Project,如果这么说,理解起来,其实在同一个项目里,
不应该有与project同级的模块存在,但实际并不绝对
例如工程A、B分别各自包含两个module,其中一个模块另一个是library模块,也即上面提到的C内部模块。
此时,A工程是可以依赖B工程中的library模块的。
使用方法:和内部模块类似,在settings.gradle中include 后面加上模块名,这里的模块名需要自己定义


include ':app', ':module-name'
include ':external-module-name'
project(':external-module-name').projectDir = new File(rootDir, '../../Example/sdkexample')


意思比较明显,声明一个模块,指定一下这个模块的地址。把这个include到工程中。比较讲究的是那个地址的写法,要切记不能写错了,写错了找不到,在AS的工程结构中,这种外部模块的依赖,是不能组织在工程内部的,实际上会合工程顶层,以及ExternalLibraries处于同一级如图,那个sdkexample是另一个工程中的内部模块。最后说一下,为什么会有这篇文章,我也不想搞这么复杂啊,又是这样依赖那样依赖的,其实都是项目需要被逼的了。要是所有依赖的代码都可以用第二种方式使用外部依赖。尤其是D这种外部模块依赖,很奇葩的,大家要是仔细理解D的这种方式其实应该不难想到,这是为了解决联合开发的问题而被逼出来的想法。


适用场景如下:

双方协作开发,一方依赖另一方的代码,被依赖方代码也正在开发过程中,随时会变化。关键双方不是基于同一个git仓库开发,各自有各自的,自然本地目录也不是同一个了。这样形式的依赖,可以使双方各自独立开发,依赖方在本地clone两个仓库,一个依赖另一个,各自还可以独立开发,依赖方只需要在必要的时候pull代码就OK了。






Top