Wangtao's Blog

Android Developer

谷歌认为,图标变更功能应该使用独立的LAUNCHER Activity实现,而不应借助activity-alias。
最建议方案如下:

1. 新建 NewMainActivity()

1
class NewMainActivity : MainActivity()

2. 配置 AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<activity
android:name=".MainActivity"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".NewMainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_show"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

3. MainActivity() 中切换代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
open class MainActivity : AppCompatActivity() {
private var position = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

findViewById<View>(R.id.btn_default).setOnClickListener {
position = 0
// setDefaultAlias()
Toast.makeText(this@MainActivity, "退到后台后切换", Toast.LENGTH_SHORT).show()
}
findViewById<View>(R.id.btn_alias1).setOnClickListener {
position = 1
// setAlias1()
Toast.makeText(this@MainActivity, "退到后台后切换", Toast.LENGTH_SHORT).show()
}

ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_RESUME -> {
}

Lifecycle.Event.ON_START -> {
Log.i("LifecycleObserver", "应用回到前台")
}

Lifecycle.Event.ON_STOP -> {
Log.i("LifecycleObserver", "应用退到后台")
//根据具体业务需求设置切换条件,我公司采用接口控制icon切换
if (position == 0) {
setDefaultAlias()
} else {
setAlias1()
}
}

else -> {}
}
}
})
}

/**
* 设置默认的别名为启动入口
* 在设置图标不可见的方法中,我们传递的是 PackageManager.DONT_KILL_APP ,在设置图标可见的方法中我们传递的是 0 。传递0表示会杀死包含该组件的app,桌面图标立即会更改掉。
*/
fun setDefaultAlias() {
val packageManager = packageManager
val name2 = ComponentName(this, NewMainActivity::class.java)
packageManager.setComponentEnabledSetting(
name2,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
val name1 = ComponentName(this, MainActivity::class.java)
packageManager.setComponentEnabledSetting(
name1,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
0
)
}

/**
* 设置别名1为启动入口
* 在设置图标不可见的方法中,我们传递的是 PackageManager.DONT_KILL_APP ,在设置图标可见的方法中我们传递的是 0 。传递0表示会杀死包含该组件的app,桌面图标立即会更改掉。
*/
fun setAlias1() {
val packageManager = packageManager
val name1 = ComponentName(this, MainActivity::class.java)
packageManager.setComponentEnabledSetting(
name1,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
val name2 = ComponentName(this, NewMainActivity::class.java)
packageManager.setComponentEnabledSetting(
name2,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
0
)
}
}

4. 参考资料

本文章主要讲述如何修改ffmpeg源码,实现在每个关键帧中插入sei。

原始Bsf

BitStream Filter(码流过滤)的缩写为bsf,它的作用是,在不做码流解码的前提下,对已经编码后的比特流做特定的修改、调整。

bsf h264_metadata的调用

使用ffmpeg工具时,可以使用比特流过滤器。基本的filter调用格式如下:

1
ffmpeg -i INPUT -c:v copy -bsf:v filter1[=opt1=str1:opt2=str2][,filter2] OUTPUT

可以使用 h264_metadata比特流过滤器添加SEI。下面示例命令添加了类型为未注册的用户数据的SEI,其中uuid为”086f3693-b7b3-4f2c-9653-21492feee5b8”,payload内容为”hello”:

1
./ffmpeg  -I oceans.h264 -c:v copy -bsf:v h264_metadata=sei_user_data='086f3693-b7b3-4f2c-9653-21492feee5b8+hello' oceans.sei.h264

Bsf修改

修改文件为h264_metadata_bsf.c

查找出关键帧

from

1
2
3
4
5
6
7
8
9
has_sps = 0;
for (i = 0; i < au->nb_units; i++) {
if (au->units[i].type == H264_NAL_SPS) {
err = h264_metadata_update_sps(bsf, au->units[i].content);
if (err < 0)
goto fail;
has_sps = 1;
}
}

to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int has_idr = 0;
has_sps = 0;
for (i = 0; i < au->nb_units; i++) {
if (au->units[i].type == H264_NAL_SPS) {
err = h264_metadata_update_sps(bsf, au->units[i].content);
if (err < 0)
goto fail;
has_sps = 1;
}
if (au->units[i].type == H264_NAL_IDR_SLICE) {
has_idr = 1;
}
}

判断是关键帧

from

1
2
3
// Only insert the SEI in access units containing SPSs, and also
// unconditionally in the first access unit we ever see.
if (ctx->sei_user_data && (has_sps || !ctx->done_first_au)) {

to

1
2
3
4
// Only insert the SEI in access units containing IDRs, and also
// unconditionally in the first access unit we ever see.
if (ctx->sei_user_data && (has_idr || !ctx->done_first_au)) {

在关键帧后插入一帧SEI

当参数为“**-bsf:v h264_metadata=sei_user_data=’086f3693-b7b3-4f2c-9653-21492feee5b8+{timestamp}’**”时插入当前时间戳。

解析出的SEI信息为”ts:timestamp”精确到毫秒

from

1
2
3
4
udu->data        = udu->data_ref->data;
udu->data_length = len + 1;
memcpy(udu->data, ctx->sei_user_data + i + 1, len + 1);

to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
udu->data        = udu->data_ref->data;
udu->data_length = len + 1;
memcpy(udu->data, ctx->sei_user_data + i + 1, len + 1);

//convert to Timestamp
if(strcmp(udu->data, "{timestamp}") == 0){
char mark[] = "ts:";
long timestamp = av_gettime() / 1000;
char result[20];
sprintf(result, "%s%ld", mark, timestamp);

udu->data = result;
udu->data_length = strlen(result) + 1;
}

调试方法

使用xcode

使用方法

1
-bsf:v h264_metadata=sei_user_data='086f3693-b7b3-4f2c-9653-21492feee5b8+{timestamp}'

栗子:

1
-stream_loop -1 -i video.mp4  -c:a aac -c:v libx264 -bsf:v h264_metadata=sei_user_data='086f3693-b7b3-4f2c-9653-21492feee5b8+{timestamp}' -f flv rtmp://demo-push.cn/live/11

参考资料

Android MediaCodec编码 推流rtmp 插入SEI

该插入SEI方法,可用于开源项目yasea & rtmp-rtsp-stream-client-java

当前项封装成flv进行推流。所以要在flv中插入SEI自定义信息
当前实现的是在每个关键帧中插入SEI时间戳(也可插入其他自定义信息)

修改文件为yasea->SrsFlvMuxer.java&rtmp-rtsp-stream-client-java->SrsFlvMuxer.java

修改的代码段如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public void writeVideoSample(final ByteBuffer bb, MediaCodec.BufferInfo bi) {
if (bi.size < 4) return;

long pts = (int) (bi.presentationTimeUs / 1000);
long dts = pts;
int type = SrsCodecVideoAVCFrame.InterFrame;
SrsFlvFrameBytes frame = avc.demuxAnnexb(bb, bi, true);
int nal_unit_type = frame.data.get(0) & 0x1f;
if (nal_unit_type == SrsAvcNaluType.IDR || bi.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
type = SrsCodecVideoAVCFrame.KeyFrame;

//增加代码如下
ByteBuffer sei = ByteBuffer.wrap(muxSEI(System.currentTimeMillis()+""));
SrsFlvFrameBytes frameSei = new SrsFlvFrameBytes();
frameSei.size = sei.array().length;
frameSei.data = sei.duplicate();

ipbs.add(avc.muxNaluHeader(frameSei));
ipbs.add(frameSei);
//增加代码如上
} else if (nal_unit_type == SrsAvcNaluType.SPS || nal_unit_type == SrsAvcNaluType.PPS) {
SrsFlvFrameBytes frame_pps = avc.demuxAnnexb(bb, bi, false);
frame.size = frame.size - frame_pps.size - 4; // 4 ---> 00 00 00 01 pps
if (!frame.data.equals(h264_sps)) {
byte[] sps = new byte[frame.size];
frame.data.get(sps);
isPpsSpsSend = false;
h264_sps = ByteBuffer.wrap(sps);
}

SrsFlvFrameBytes frame_sei = avc.demuxAnnexb(bb, bi, false);
if (frame_sei.size > 0) {
if (SrsAvcNaluType.SEI == (frame_sei.data.get(0) & 0x1f)) {
frame_pps.size = frame_pps.size - frame_sei.size - 3;// 3 ---> 00 00 01 SEI
}
}

if (frame_pps.size > 0 && !frame_pps.data.equals(h264_pps)) {
byte[] pps = new byte[frame_pps.size];
frame_pps.data.get(pps);
isPpsSpsSend = false;
h264_pps = ByteBuffer.wrap(pps);
writeH264SpsPps(dts, pts);
}
return;
} else if (nal_unit_type != SrsAvcNaluType.NonIDR) {
return;
}

ipbs.add(avc.muxNaluHeader(frame));
ipbs.add(frame);


writeH264IpbFrame(ipbs, type, dts, pts);
ipbs.clear();
}

新增的muxSEI方法

该方法实现的是SEI的组装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private byte[] muxSEI(String msg) {
byte[] seiContent = msg.getBytes();
byte[] seiType = new byte[]{0x06, 0x05};
byte[] seiUuid = new byte[]{
0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04,
0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04};
byte[] seiEnd = new byte[]{(byte) 0x80};
int contentSize = 16 + seiContent.length;

//数据长度(数据长度减去255,有多少个就写多少个FF,剩下的不为0,再写一个字节)
int ffCount = 0;
while (true) {
if (contentSize >= 255) {
ffCount++;
}
if (contentSize < 255) {
break;
}
contentSize -= 255;
}

String size = Integer.toHexString(contentSize);
byte[] contentLastSize = Hex.decodeHex(size);
byte[] contentFirstSize = new byte[ffCount];
for (int i = 0; i < ffCount; i++) {
contentFirstSize[i] = (byte) 0xff;
}
byte[] sei_content_size = combineArrays(contentFirstSize, contentLastSize);

return combineArrays(seiType, sei_content_size, seiUuid, seiContent, seiEnd);
}

private byte[] combineArrays(byte[]... a) {
int massLength = 0;
for (byte[] b : a) {
massLength += b.length;
}
byte[] c = new byte[massLength];
byte[] d;
int index = 0;
for (byte[] anA : a) {
d = anA;
System.arraycopy(d, 0, c, 0 + index, d.length);
index += d.length;
}
return c;
}

h264 SEI原理

大家可参考该链接https://www.jianshu.com/p/7c6861f0d7fd

flv封装原理

可参考该链接https://blog.evanxia.com/2017/07/1378

在AndroidManifest.xml注册ACTION事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<activity
android:name="com.test.app.MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="这里的名称会对外显示"
android:launchMode="singleTask"
android:screenOrientation="portrait">
//注册接收分享
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />

//接收分享的文件类型
<data android:mimeType="image/*" />
<data android:mimeType="application/msword" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
<data android:mimeType="application/vnd.ms-excel" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
<data android:mimeType="application/vnd.ms-powerpoint" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
<data android:mimeType="application/pdf" />
<data android:mimeType="text/plain" />
</intent-filter>

//注册默认打开事件,微信、QQ的其他应用打开
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />

//接收打开的文件类型
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="image/*" />
<data android:mimeType="application/msword" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
<data android:mimeType="application/vnd.ms-excel" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
<data android:mimeType="application/vnd.ms-powerpoint" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
<data android:mimeType="application/pdf" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>

在用于接收分享的Activity里面加接收代码

  1. 当APP进程在后台时,会调用Activity的onNewIntent方法
  2. 当APP进程被杀死时,会调用onCreate方法

所以在两个方法中都需要监听事件

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
receiveActionSend(intent);
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
receiveActionSend(intent);
}

receiveActionSend方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void receiveActionSend(Intent intent) {
String action = intent.getAction();
String type = intent.getType();

//判断action事件
if (type == null || (!Intent.ACTION_VIEW.equals(action) && !Intent.ACTION_SEND.equals(action))) {
return;
}

//取出文件uri
Uri uri = intent.getData();
if (uri == null) {
uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
}

//获取文件真实地址
String filePath = UriUtils.getFileFromUri(EdusohoApp.baseApp, uri);
if (TextUtils.isEmpty(filePath)) {
return;
}

//业务处理
.
.
.
}

获取真实路径getFileFromUri方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 获取真实路径
*
* @param context
*/
public static String getFileFromUri(Context context, Uri uri) {
if (uri == null) {
return null;
}
switch (uri.getScheme()) {
case ContentResolver.SCHEME_CONTENT:
//Android7.0之后的uri content:// URI
return getFilePathFromContentUri(context, uri);
case ContentResolver.SCHEME_FILE:
default:
//Android7.0之前的uri file://
return new File(uri.getPath()).getAbsolutePath();
}
}

Android7.0之后的uri content:// URI需要对微信、QQ等第三方APP做兼容

  • 在文件管理选择本应用打开时,url的值为content://media/external/file/85139
  • 在微信中选择本应用打开时,url的值为
    content://com.tencent.mm.external.fileprovider/external/tencent/MicroMsg/Download/111.doc
  • 在QQ中选择本应用打开时,url的值为
    content://com.tencent.mobileqq.fileprovider/external_files/storage/emulated/0/Tencent/QQfile_recv/

第一种为系统统一文件资源,能通过系统方法转化为绝对路径;
微信、QQ的为fileProvider,只能获取到文件流,需要先将文件copy到自己的私有目录。
方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* 从uri获取path
*
* @param uri content://media/external/file/109009
* <p>
* FileProvider适配
* content://com.tencent.mobileqq.fileprovider/external_files/storage/emulated/0/Tencent/QQfile_recv/
* content://com.tencent.mm.external.fileprovider/external/tencent/MicroMsg/Download/
*/
private static String getFilePathFromContentUri(Context context, Uri uri) {
if (null == uri) return null;
String data = null;

String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (index > -1) {
data = cursor.getString(index);
} else {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
String fileName = cursor.getString(nameIndex);
data = getPathFromInputStreamUri(context, uri, fileName);
}
}
cursor.close();
}
return data;
}

/**
* 用流拷贝文件一份到自己APP私有目录下
*
* @param context
* @param uri
* @param fileName
*/
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
InputStream inputStream = null;
String filePath = null;

if (uri.getAuthority() != null) {
try {
inputStream = context.getContentResolver().openInputStream(uri);
File file = createTemporalFileFrom(context, inputStream, fileName);
filePath = file.getPath();

} catch (Exception e) {
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
}
}
}

return filePath;
}

private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)
throws IOException {
File targetFile = null;

if (inputStream != null) {
int read;
byte[] buffer = new byte[8 * 1024];
//自己定义拷贝文件路径
targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) {
targetFile.delete();
}
OutputStream outputStream = new FileOutputStream(targetFile);

while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
outputStream.flush();

try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}

return targetFile;
}

getChildFragmentManager()

有时候,我们会在 Fragment 里面继续嵌套二级甚至三级 Fragment,即 Activity 嵌套多级 Fragment。此时在 Fragment 里管理子 Fragment 时,也需要使用到 FragmentManager。但是一定要使用 v4.fragment.getChildFragmentManager() 方法获取 FragmentManager 对象!

1.Fragment嵌套Fragment要用getChildFragmentManager。

本来里面的fragment用的还是getFragmentManager,Fragment嵌套Fragment时,里面要用getChildFragmentManager。

1
2
FragmentManager childFragmentManager = getChildFragmentManager();
ViewPager_Adapter viewPager_adapter = new ViewPager_Adapter(childFragmentManager, fragments); //FragmentPagerAdapter

2.常见错误 java.lang.IllegalStateException: Activity has been destroyed

onCreate中缺少super.onCreate(savedInstanceState) ,把它放在所有语句之前,应该解决这个问题。

应用内悬浮窗-自动贴边

无需权限!!!
无需权限!!!
无需权限!!!

效果

实现原理:获取当前activity的DecorView然后find到android.R.id.content,addView自己的view。

获取当前activity的content

1
2
3
4
5
6
7
8
9
10
11
private FrameLayout getActivityRoot(Activity activity) {
if (activity == null) {
return null;
}
try {
return (FrameLayout) activity.getWindow().getDecorView().findViewById(android.R.id.content);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

实现方法

FloatingManage

主要为悬浮窗的管理类,包括设置头像、弹出内容、显示与隐藏。

FloatRootView、FloatView

主要为悬浮窗的页面布局和拖动、点按事件的监听操作的实现。

结果

Demo已上传Github如需要可下载https://github.com/wangtaoT/FloatingView

通过productFlavor用来为app创建不同的版本,如:免费版和付费版、不同应用市场的渠道包等。
创建方式:

Gradle中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
android {
productFlavors {
nbsylzxVersion { //渠道1
applicationId "com.wangtao.nbsylzx"
manifestPlaceholders = [package_name : "com.wangtao.nbsylzx",
umeng_appkey : "5a1b7cb2f29d98",
umeng_message_secret: "46b6a40c6a749f",
baiduMap_appKey : "MnaZOYCPOIqozGoSle"]
versionCode 1
versionName "V1.0.1"
}
fhzyyVersion { //渠道2
applicationId "com.wangtao.fhzyy"
manifestPlaceholders = [package_name : "com.wangtao.fhzyy",
umeng_appkey : "59b8da5f734b",
umeng_message_secret: "1b652c87cd9d",
baiduMap_appKey : "U0pNbvVRZHwTfP"]
versionCode 2
versionName "V1.0.2"
}
}
}

通过这种方式可以配置不同的包名、友盟key、百度key等。

AndroidManfest.xml中获取key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--百度地图-->
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="${baiduMap_appKey}" />
<!--友盟-->
<meta-data
android:name="UMENG_APPKEY"
android:value="${umeng_appkey}" />
<meta-data
android:name="UMENG_MESSAGE_SECRET"
android:value="${umeng_message_secret}" />
<meta-data
android:name="UMENG_CHANNEL"
android:value="Umeng" />

通过${名称}来获取gradle中的配置

创建对应文件夹

  • 在app-src中创建与main同层级的文件夹

  • 文件夹名称与productFlavors中配置的要一样

    – main
    – nbsylzxVersion
    – fhzyyVersion

  • 对应文件夹中也可以创建java/res

    – main
    —–java
    —–res
    –nbsylzxVersion
    —–java
    —–res

main-java中的文件与变种文件夹中的.java文件不能重名
main-res中的资源文件与变种文件夹中的资源文件相同,优先使用变种文件夹资源文件

不同依赖配置

在Gradle中dependencies{}中配置依赖,渠道名称+Compile

1
2
3
4
dependencies{
nbsylzxVersionCompile project(':baiduMap')
fhzyyVersionCompile 'com.github.bumptech.glide:glide:4.0.0'
}

解决友盟多包名无法推送问题问题

如果:
productFlavors中多包名

  • applicationId:com.test.a
  • applicationId:com.test.b
  • AndroidManfest.xml中
    package=”com.test.c”

那么在Application中初始化友盟时增加以下:

1
mPushAgent.setResourcePackageName("com.test.c");

Android开发中很多时候需要使用第三方开源库,当它是用C++写的时就需要编译成SO库,本文就是编译之前需要做的准备!
配置NDK、SDK环境!
配置NDK、SDK环境!
配置NDK、SDK环境!

搭建虚拟机

我在这里选择VMware12虚拟机VMware Workstation Pro下载地址

安装Linux系统

选择Ubuntu下载地址

下载AndroidNDK及SDK

android NDK选择Linux版本。下载地址
android SDK选择高一点的Linux版本。下载地址

下载完成后在目录下会看到我们下载的ndk和sdk压缩包我们把它们解压出来,一个是.zip的另一个是.tgz的。

1
2
unzip xxx.zip
tar zxvf xxx.taz

将两个压缩文件解压到当前目录即可。

下载openjdk

1
sudo apt-get install openjdk-8-jre-headless

会自动安装

配置SDK和NDK全局环境变量

下载的linux版本的SDK缺少一点东西,需要运行命令

1
sh /android-sdk-linux/tools/android

下载最新的Android SDK Tools、Android SDK Platform-tools、Android SDK Build-tools、Android SDK Platform即可。

1
sudo gedit /etc/profile

在文件最后加上

1
2
3
4
export PATH=/你的路径/android-sdk-linux/platform-tools:$PATH
export PATH=/你的路径/android-sdk-linux/tools:$PATH
export ANDROID_NDK=/你的路径/android-ndk-r14b
export PATH=/你的路径/android-ndk-r14b:$PATH

路径可能会有所不同!


重启系统既可以生效

接下来就可以编译你的ANdroid SO库了!!!

实现小猿搜题、作业帮类似效果。
基于Google Tesseract-OCR实现,由于这是基于C++开发,Android中不能直接使用,所以本项目使用tess-two是对于Android的分支。

准备工作

1)Android Studio导入gradle依赖(快速集成)

1
2
3
4
//编译好的SO库 和 jar包
compile 'com.rmtheis:tess-two:6.1.1'
//图像裁剪
compile 'com.edmodo:cropper:1.0.1'

其实也可以自己下载https://github.com/rmtheis/tess-two 源码在Linux环境中进行编译。

2)第二种方法 自己编译源码(!!!)

  • 安装虚拟机VMware
  • 安装linux系统Ubunt
  • 安装必要工具
1
2
sudo apt-get update
sudo apt-get install git
  • 配置JDK、NDK、SDK环境(踩了一万个坑)具体教程地址
  • 下载tess-two代码
  • 开始编译(依次输入以下两行命令)
1
2
cd /tess/tess-two/jni
ndk-build
  • 使用
    将编译好的tess-two目录复制到自己项目的libraries下,将项目下的app关联 libraries。

tess-two使用

主要就是将assets中的字典文件写到手机文件目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 将assets中的文件复制出
*
* @param path
*/
public void deepFile(String path) {
String newPath = getExternalFilesDir(null) + "/";
try {
String str[] = getAssets().list(path);
if (str.length > 0) {//如果是目录
File file = new File(newPath + path);
file.mkdirs();
for (String string : str) {
path = path + "/" + string;
deepFile(path);
path = path.substring(0, path.lastIndexOf('/'));//回到原来的path
}
} else {//如果是文件
InputStream is = getAssets().open(path);
FileOutputStream fos = new FileOutputStream(new File(newPath + path));
byte[] buffer = new byte[1024];
int count = 0;
while (true) {
count++;
int len = is.read(buffer);
if (len == -1) {
break;
}
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

点击跳转到拍照界面

1
2
3
4
5
6
7
8
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_camera:
Intent intent = new Intent(this, TakePhoteActivity.class);
startActivity(intent);
break;
}
}
  • TakePhoteActivity.java 主要执行拍照、裁剪操作。

以下是拍照成功后回掉接口,拍照成功后显示剪裁界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 拍照成功后回调
* 存储图片并显示截图界面
*
* @param data
*/
@Override
public void onCameraStopped(byte[] data) {
Log.i("TAG", "==onCameraStopped==");
// 创建图像
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken).toString() + ".jpg";
// 存储图像(PATH目录)
Uri source = insertImage(getContentResolver(), filename, dateTaken, PATH, filename, bitmap, data);
//准备截图
try {
mCropImageView.setImageBitmap(MediaStore.Images.Media.getBitmap(this.getContentResolver(), source));
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
showCropperLayout();
}

剪裁操作,完成后跳转到识别界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private View.OnClickListener cropcper = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_closecropper:
showTakePhotoLayout();
break;
case R.id.btn_startcropper:
//获取截图并旋转90度
Bitmap cropperBitmap = mCropImageView.getCroppedImage();
Bitmap bitmap = Utils.rotate(cropperBitmap, -90);
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken).toString() + ".jpg";
Uri uri = insertImage(getContentResolver(), filename, dateTaken, PATH, filename, bitmap, null);
Intent intent = new Intent(TakePhoteActivity.this, ShowCropperedActivity.class);
intent.setData(uri);
intent.putExtra("path", PATH + filename);
intent.putExtra("width", bitmap.getWidth());
intent.putExtra("height", bitmap.getHeight());
// intent.putExtra("cropperImage", bitmap);
startActivity(intent);
bitmap.recycle();
finish();
break;
}
}
};
  • ShowCropperedActivity.java 主要是识别操作

初始化识别器

1
2
3
4
5
6
7
8
9
10
11
12
13
//sd卡路径
private static String LANGUAGE_PATH = "";
//识别语言
private static final String LANGUAGE = "chi_sim";//chi_sim | eng
private TessBaseAPI baseApi = new TessBaseAPI();
@Override
protected void onCreate(Bundle savedInstanceState) {
LANGUAGE_PATH = getExternalFilesDir("") + "/";

baseApi.init(LANGUAGE_PATH, LANGUAGE);
//设置设别模式
baseApi.setPageSegMode(TessBaseAPI.PageSegMode.PSM_AUTO);
}

将图片灰度化处理,去除杂色可以提高准确度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 灰度化处理
*
* @param bitmap3
* @return
*/
public Bitmap convertGray(Bitmap bitmap3) {
colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
Paint paint = new Paint();
paint.setColorFilter(filter);
Bitmap result = Bitmap.createBitmap(bitmap3.getWidth(), bitmap3.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
canvas.drawBitmap(bitmap3, 0, 0, paint);
return result;
}

开始识别

1
2
3
4
5
6
//传入图片
baseApi.setImage(bitmap);
//获取识别后的结果
String result = baseApi.getUTF8Text();
//结束识别
baseApi.end();

结果

Demo已上传Github如需要可下载https://github.com/wangtaoT/AndroidOCR

0%