android系统预装App2

目前 5.0 之后项目预置方式通用步骤为:

建立 apk 文件夹;
置目标 apk 到该文件夹下;
解压缩 apk 查看是否包含 lib/文件夹(apk 项目是否包含 lib 库文件);
在该文件夹下编写 Android.mk 脚本 ;

理论上 apk 文件夹可以建立在项目内任意目录,编译系统会自动搜索并根据其内 Android.mk (编译脚本) 来进行编译。
编译系统采用的是递归搜索,在搜索到父文件目录的 Android.mk 脚本后递归便被终止。因此一般可以将需要预置的 apk 文件夹放到一个总文件夹内,并在该文件夹根目录另外写一个 Android.mk (管理脚本) ,以便对所有预置 apk 进行管理。对于 管理脚本 的编写将在文末解释。预置目录如下例:

Apps/
-/Android.mk 管理脚本
-/Test1
—-/Android.mk 编译脚本
—-/Test1.apk
—-/lib/_
-/Test2
—-/Android.mk 编译脚本
—-/Test2.apk
—-/lib/_

编译脚本 如下例:

LOCALPATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := Test
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_TAGS := optional
LOCAL_BUILT_MODULE_STEM := package.apk
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_SRC_FILES := Test
*.apk

include $(BUILD_PREBUILT)1234567891011

其中,LOCALMODULE := Test 表示了一个预置的 apk 在编译中的唯一标识,同时编译后该 apk 会以此命名;LOCAL_SRC_FILES := Test_.apk 表示了当前要预置的 apk 的文件名,”Test__.apk”匹配任意以”Test_”开头的 apk 文件。

对于 apk 预置路径,在 Android.mk 中可以通过以下方式指名:

a) 默认预置 apk 到 system/app/目录(普通系统 apk,不可卸载),如前文 Android.mk 脚本编写之后即可;
b) 预置 apk 到 system/priv-app/目录(系统核心 apk,不可卸载),在前文 Android.mk 脚本中添加并配置变量:

LOCAL_PRIVILEGED_MODULE := true1

c) 预置 apk 到 data/app/目录并且卸载后不需要再会恢复,在前文 Android.mk 脚本中配置变量:

LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)1

d) 预置 apk 到 data/app/目录并且卸载后恢复出厂可以恢复,在前文 Android.mk 简本中配置变量:

LOCAL_MODULE_PATH := $(TARGET_OUT)/vendor/operator/app1

对于包含 lib 库文件的 apk,还需要根据预置目标路径,在 mk 脚本中作不同的处理:

情况一:对于预置到 data/app/目录下的 apk,包括可恢复和不可恢复(即上一段 c) 和 d)), 一般 lib 库文件可以不用手动添加,apk 在首次运行时,会自动将自身的 lib 库抽取安装到自身的根目录;
情况二:对于预置到 system/app/ 和 system/priv-app 目录下的 apk(即上一段 a) 和 b)),因为在 android 系统中,system 分区是不允许应用执行写操作的,因此需要在 Android.mk 脚本中进行配置,手动添加 lib 库文件到编译环境,以便 lib 库文件在编译之后拷贝到对应编译后的 apk 目录下,否则 apk 执行时会因找不到 lib 库而报错;

<一>、首先根据 情况二 所述,添加 lib 库文件到编译环境

方法一 不从 apk 中解压 lib 库而直接添加

如下例,在 Android.mk 中添加并配置变量(注意路径对应):

LOCAL_PREBUILT_JNI_LIBS =
@lib/armeabi-v7a/libcryptox.so
@lib/armeabi-v7a/libfb.so 123

注意前面的 @符号,@标识符会将 apk 中的 so 抽离出来,拷贝到对应编译后的 apk 目录;

方法二 手动解压 lib 文件到当前 apk 的编译目录并添加

先解压当前 apk 内的 lib 文件夹到当前 apk 编译目录,同方法一在 Android.mk 中添加并配置变量(注意路径对应),如下例:

LOCAL_PREBUILT_JNI_LIBS =
lib/armeabi-v7a/libcryptox.so
lib/armeabi-v7a/libfb.so 123

若当前 apk 包含的 lib 库文件数量比较多时,上述代码可以通过修改为如下代码进行优化,优化的思路是用递归搜索来替代手工对 lib 库文件进行添加:

###清空临时变量 JNI_LIBS
JNI_LIBS := ###当前目录递归搜索
$(foreach FILE,$(shell find $(LOCAL_PATH)/lib/ -name *.so), $(eval JNI_LIBS += $(FILE)))
###获取搜索文件目录集(相对目录)
LOCAL_PREBUILT_JNI_LIBS := $(subst $(LOCAL_PATH),,$(JNI_LIBS))123456

<二>、然后需要注意当前 Android 环境是否符合 apk 运行条件(64 位和 32 位)并配置 apk 运行环境

之所以要配置 apk 运行环境,是因为包含 lib 库的 apk 在添加 lib 库到编译环境之后,在 Android 环境和 apk 运行条件不符的情况下,需要在编译环境中指定环境。举例说明:

目前一般的 apk 运行环境为 32 位 Android 系统环境,当在 64 位 Android 系统中预置带有 lib 库的 apk 时,手动添加 lib 库文件到编译环境后,默认情况下编译环境会在编译后 apk 目录建立 64 位的环境的 lib 库路径 /lib/arm64,虽然编译过程未报错,但之后在执行该 apk 时,会出现 apk 因找不到 lib 库而报错

因此,需要在 Android.mk 中对当前 apk 编译环境进行配置,配置的方法常见的也有两种:

<1>指定编译目标为 32 位 或 64 位

在 Android 目标中添加并配置变量:

LOCAL_MULTILIB := ###可选值 /32/64/first/both 12

不同的值含义如下:

“both”: build both 32-bit and 64-bit.
“32”: build only 32-bit.> * “64”: build only 64-bit.
“first”: build for only the first arch (32-bit in 32-bit devices and 64-bit in 64-bit devices).
“”: the default; the build system decides what arch to build based on the module class and other LOCAL_ variables, such as LOCAL_MODULE_TARGET_ARCH, LOCAL_32_BIT_ONLY, etc.

此处,default 值会根据当前已有的其他相关值方式来进行编译

<2>指定目标 lib 库的 类型
在 Android.mk 中添加并配置变量:

LOCAL_MODULE_TARGET_ARCH := ###可选值 arm/arm x86/arm6412

此处, LOCAL_MODULE_TARGET_ARCH 的值只能是当前编译环境所支持的类型,如果需要配置当前系统不支持类型,则需要配置如下另一个变量

LOCAL_MODULE_UNSUPPORTED_TARGET_ARCH := ###可选值 arm/arm x86/arm6412

与 LOCAL_MODULE_TARGET_ARCH 相反, LOCAL_MODULE_UNSUPPORTED_TARGET_ARCH 的值只能是当前编译环境所不支持的类型,否则编译将不会生效

如下是一个完整的 Android.mk 脚本示例,其中 apk 运行环境为 32 位,系统为 64 位:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := UCBrowser
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_TAGS := optional
LOCAL_BUILT_MODULE_STEM := package.apk
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_SRC_FILES := HK_UCBrowser*.apk
LOCAL_PRIVILEGED_MODULE := true
LOCAL_MULTILIB := 32
JNI_LIBS :=
$(foreach FILE,$(shell find $(LOCAL_PATH)/lib/ -name *.so), $(eval JNI_LIBS += $(FILE)))
LOCAL_PREBUILT_JNI_LIBS := $(subst $(LOCAL_PATH),,$(JNI_LIBS))
include $(BUILD_PREBUILT)123456789101112131415

最后编写管理脚本集中管理预置 apk

预置三方 apk 一般根据客户项目的不同而有所差异,因此如前文所述,可以将需要预置的 apk 文件夹放到一个总文件夹内,该文件夹可以采用与客户项目有关的命名用以区分不同客户项目预置,并在该文件夹根目录另外写一个 Android.mk (管理脚本), 这样便可以根据客户项目对预置 apk 进行管理。以下是据此更新后的预置目录,具体可以根据原理自行调整:

Customer1/
-/Android.mk 管理脚本
-/Test1
—-/Android.mk 编译脚本
—-/Test1.apk
—-/lib/_
-/Test2
—-/Android.mk 编译脚本
—-/Test2.apk
—-/lib/_
Customer2/
-/Android.mk 管理脚本
-/Test3
—-/Android.mk 编译脚本
—-/Test1.apk
—-/lib/_
-/Test4
—-/Android.mk 编译脚本
—-/Test2.apk
—-/lib/_

对于 管理脚本 的编写,如下例:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
###此处也可以在此添加一个宏变量来控制是否执行以下代码
###if($(strip $(XXX_CUSTOMER1_APP)),yes))
SUB_ANDROID_MK := $(shell find $(LOCAL_PATH)/apps/ -name Android.mk)
$(foreach sub_mk,$(SUB_ANDROID_MK), $(eval include $(sub_mk)))
PRODUCT_PACKAGES +=
Test1
Test2
###endif12345678910

其中 $(shell find $(LOCAL_PATH)/apps/ -name Android.mk) 返回一个遍历搜索子目录 Android.mk 的路径集合,此处也可以采用手动添加指定路径的方式来控制具体预置的 apk 是否编译 :

###替换
###SUB_ANDROID_MK := $(shell find $(LOCAL_PATH)/apps/ -name Android.mk)
###$(foreach sub_mk,$(SUB_ANDROID_MK), $(eval include $(sub_mk))) ###为
include Customer1/Test1/Android.mk
include Customer1/Test2/Android.mk123456

同时,PRODUCT_PACKAGES 变量指定将哪些编译完成的 apk 打包到项目最终编译生成的 Android 系统镜像文件中,如上代码中,其值恰恰就是预置 apk 的编译脚本中定义的 LOCAL_MODULE (即编译后该 apk 的名称)的值。

原文:https://blog.csdn.net/a462533587/article/details/46380795

android系统预装App

  1. ./vendor/firefly/apps/ 目录下新建 FDeviceTest 文件夹
  2. 将 Apk 放到FDeviceTest
  3. 解压 apk,把 lib 文件夹放到 FDeviceTest 中
  4. 编写 Android.mk 文件,如下:
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
###############################################################################
# RKDeviceTest
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := FDeviceTest
LOCAL_MODULE_CLASS := APPS
#optional 该模块所有编译版本下都编译
#user 该模块只在user编译版本下才编译
#eng 该模块只在eng编译版本下才编译
#tests 该模块只在tests编译版本下才编译
LOCAL_MODULE_TAGS := optional
#编译链接后的目标文件的文件名
LOCAL_BUILT_MODULE_STEM := package.apk
LOCAL_DEX_PREOPT := false
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
#LOCAL_PROPRIETARY_MODULE 控制生成路径到system/vendor/lib,否则就是system/lib
#设置true,则LOCAL_PRIVILEGED_MODULE 优先级高,如果设置为false 则LOCAL_MODULE_PATH优先级高
LOCAL_PRIVILEGED_MODULE := true
#testkey 普通apk,默认情况下使用,默认为使用apk自己的签名
#platform 使用平台签名
#shared 使用共享签名,该apk需要和home/contacts进行共享数据
#media 使用媒体签名,该apk是media/download系统中的一环
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_OVERRIDES_PACKAGES := DeviceTest
#构建系统生成模块时所用的源文件列表
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
#LOCAL_REQUIRED_MODULES :=
ifeq ($(strip $(TARGET_ARCH)), arm)
#只适用编译第三方app,且app依赖了so库 *
LOCAL_PREBUILT_JNI_LIBS := \
lib/arm/libdrm_devicetest.so \
lib/arm/libserial_port.so \
lib/arm/libgti_android.so \
lib/arm/libgti_detect.so \
lib/arm/libopencv_java3.so \
lib/arm/librknn_api.so \
lib/arm/librknn-jni.so
else ifeq ($(strip $(TARGET_ARCH)), arm64)
LOCAL_PREBUILT_JNI_LIBS := \
lib/arm64/libdrm_devicetest.so \
lib/arm64/libserial_port.so \
lib/arm64/libgti_android.so \
lib/arm64/libgti_detect.so \
lib/arm64/libopencv_java3.so \
lib/arm64/librknn_api.so \
lib/arm64/librknn-jni.so
endif
include $(BUILD_PREBUILT)
  1. 父目录的 apps.mk 中添加应用名
1
2
3
4
5
6
PRODUCT_PACKAGES += \
FDeviceTest \
FireflyDemo \
rk_ssd_demo \
GooglePinyin \
rk_openpose_demo_rga
  1. /system/app 和 /system/priv-app 有什么区别?
    /system/priv-app 是进程可以保持始终运行,并且能拿到最多的权限;坏处是无法正常升级,因为一被 kill 马上又被拉起来,并且升级完成后,再起来的还是旧版本的 service。
    所以,我们的应用被预装到终端手机 ROM 中时,为了保活,并且尽量减少终端厂商的工作量,如果能解决升级的问题,对于终端厂商来说就只需要把应用 push 到   下就可以了。没有找到解决升级的办法,最终采用的方案往往是 push 到,系统通过一个 service(如 phone)来 bind 我们的 service,一旦 disconnect 之后再来 bind,实现保活。

android的mk文件输出日志

  1. 警告方式:
    #(warning $(param))

  2. 错误方式:
    #(error $(param))

  3. 信息方式:
    #(info $(param))

示例说明

第三方App系统签名

刚入职的时候,固件中 app 需要在源码中编译,不是很理解,为什么不使用 IDE 编译,然后 copy 到 /system/app/*,咨询导师,得到反馈 app 需要使用系统的权限 在 AndroidManifest.xml 中声明了系统全下申明了系统权限android:sharedUserId="android.uid.system"

随着工作时间延长,慢慢理解了 apk 签名机制,为了解决上述疑问,出现了 2 套解决方案:

单独签名解决方案
找到平台签名文件“platform.pk8”和“platform.x509.pem”
文件位置 android/build/target/product/security/
签名工具“signapk.jar”
位置:android/prebuilts/sdk/tools/lib
签名证书“platform.pk8 ”“platform.x509.pem ”,签名工具“signapk.jar ”放置在同一个文件夹;
执行命令

java -jar signapk.jar platform.x509.pem platform.pk8 Demo.apk signedDemo.apk

或者直接在 Ubuntu 编译环境执行

java -jar out/host/linux-x86/framework/signapk.jar build/target/product/security/platform.x509.pem build/target/product/security/platform.pk8 input.apk output.apk

IDE 中添加源码平台生成证书 platform.keystore
生成平台 platform.keystore 文件:
编译平台签名文件“platform.pk8”和“platform.x509.pem”
文件位置:android/build/target/product/security/
把 pkcs8 格式的私钥转化成 pkcs12 格式:
openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out shared.priv.pem -nocrypt
把 x509.pem 公钥转换成 pkcs12 格式:
openssl pkcs12 -export -in platform.x509.pem -inkey shared.priv.pem -out shared.pk12 -name androiddebugkey
密码都是:jfz123456
生成 platform.keystore
keytool -importkeystore -deststorepass jfz123456 -destkeypass jfz123456 -destkeystore platform.keystore -srckeystore shared.pk12 -srcstoretype PKCS12 -srcstorepass jfz123456 -alias androiddebugkey
Eclipse 添加证书
将生成的 platform.keystore 导入 eclipse 在 eclipse 下 Windows/preferences/Android/build 中设置“Custom debug keystore”为刚才生成的 platform.keystore 即可

调试 apk 时直接点击 Debug As —> Android Application 即使用系统签名签名了该 apk

Android Studio 添加证书

android的listview加入EditText

在日常开发中,ListView 是我们常用的控件,也是遇到坑比较多的一个控件。在之前的项目中,有这样的一个布局需求,在 ListView 的 item 中包含有 EditText,第一个问题就是焦点问题,会发现 edittext 获取不到焦点。

1.焦点问题
比如我们有如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
activity_main.xml

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

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>

####MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends Activity {
ListView mListView;
MyAdapter mMyAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listView);
mMyAdapter = new MyAdapter(this);
mListView.setAdapter(mMyAdapter);
}}

####MyAdapter.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
public class MyAdapter extends BaseAdapter {
private Context mContext;
public MyAdapter(Context context) {
this.mContext = context;
}
@Override
public int getCount() {
return 20;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EditText editText;
if (convertView == null) {
editText = new EditText(mContext);
convertView = editText;
} else {
editText = (EditText) convertView;
}
System.out.println("current pos:" + position);
return convertView;
}}

当你运行上述简单的代码后发现 EditText 是无法获取焦点的,导致无法输入任何东东,那么原因何在呢?

####其实,是 listview 先于子 item 抢占了焦点,那么我们首先想到的就是让 listview 失去焦点,让子 item 获取焦点(当然,listview 的 onitem 相关监听事件会失效)。
mListView.setFocusable(false);

这是再运行发现键盘弹出了,可是 editText 获取到焦点然后又失去了,需要你手动再次点击才能获取到,然后才能输入。
而且当你输入完毕,关闭软键盘,发现输入的东西不见了,自动清空。这又产生了两个问题。

第一个问题是 listview 每次调用 getview 都会使 EditText 失去焦点,第二个问题归结于下面要讲的 listview 的 item 复用产生的问题。

第一种方式行不通,查询相关资料发现,可以通过给 listview 的 item 的根布局设置 descendantFocusability 属性。

1
2
3
4
#####android:descendantFocusability 属性有三个值:
beforeDescendants:viewgroup 会优先其子类控件而获取到焦点
afterDescendants:viewgroup 只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup 会覆盖子类控件而直接获得焦点

####那么我们修改 adapter 中的 getView 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EditText editText;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_edittext, parent, false);
editText = (EditText) convertView.findViewById(R.id.editText);
convertView.setTag(editText);
} else {
editText = (EditText) convertView.getTag();
}
System.out.println("current pos:" + position);
return convertView;
}

####list_edittext.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="beforeDescendants"
android:orientation="vertical">
<EditText android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

发现还是无效果,其实我们少了一句关键的代码,就是给相应的 activity 设置 windowSoftInputMode= adjustPan 即可。。

##终上所述,我认为的解决方案就是给 ListView 或者 ListView 的 item 的根布局添加 android:descendantFocusability="beforeDescendants",然后设置相应的 activity 的 windowSoftInputMode 属性为 adjustPan 。

##2.数据问题
解决完焦点问题后,另一个问题就是 edittext 的数据问题了。当我们在当前屏幕的 edittext 中输入东东后,往下滑,发现下面的 edittext 自动输入了我们输入过得东东,这明显是我们不愿意看到的。

其实这是由于 getView 方法的复用 view 导致的,加入你在 position=0 的 edittext 中输入了内容,当你往下滑时,当 position 为 0 的 view 完全消失时,该 view 会被加入到 mActiveViews[]中,当下方的 item 检测到由可用的 view,则从该数组中取出,所以下方的 edittext 的内容会跟上面你输入的一样,其实就是同一个 edittext。关于 listview 源码级解析详见链接

#####解决方案——保存 edittext 的内容

修改 adapter 代码:

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
//新增一个数组用于保存 edittext 的内容
private SparseArray<String> mStringSparseArray;

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
EditTextHolder editTextHolder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_edittext, parent, false);

editTextHolder = new EditTextHolder();
editTextHolder.mEditText = (EditText) convertView.findViewById(R.id.editText);

editTextHolder.mMyTextWatcher = new MyTextWatcher(position, mStringSparseArray);
//给edittext设置watcher
editTextHolder.mEditText.addTextChangedListener(editTextHolder.mMyTextWatcher);

convertView.setTag(editTextHolder);

} else {
editTextHolder = (EditTextHolder) convertView.getTag();
//由于复用了 edittext,导致他的 watcher 里的 position 还是之前的 positiono,所以需要通知
//watcher 更新 positon,才能保存正确的 positon 的值
editTextHolder.updatePosition(position);
}
System.out.println(position);
editTextHolder.mEditText.setText(mStringSparseArray.get(position));
return convertView;
}
static class EditTextHolder {
EditText mEditText;
MyTextWatcher mMyTextWatcher;
public void updatePosition(int position) {
mMyTextWatcher.updatePosition(position);
}
}

static class MyTextWatcher implements TextWatcher {
private int position;
private SparseArray<String> sparseArray;
//更新 postion
public void updatePosition(int position) {
this.position = position;
}
public MyTextWatcher(int position, SparseArray<String> sparseArray) {
this.position = position;
this.sparseArray = sparseArray;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) { }
@Override
public void afterTextChanged(Editable s) {
//保存 edittext 的值
sparseArray.put(position, s.toString());
}
}

运行代码,发现 edittext 数据错乱问题解决,此方法同样适用于 checkbox 错乱等问题。

android的listview解决事件穿透问题

bug 重现:通过 mListView.setOnItemClickListener 设置 item 点击事件,正常情况下没有问题,但当 item 里面嵌套了抢焦点的控件(比如 Button ,CheckBox 等),那么点击 item 的时候,Button 等抢焦点的控件会抢先反应,就会导致点击 item 时没有反应。

解决办法:想要 item 有自己的焦点,Button 等控件有自己的焦点的话,需要在 item 的根控件里面设置 android:descendantFocusability="blocksDescendants",这个属性值表示子有子的焦点,父有父的焦点。

java 代码

1
2
3
4
5
6
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.i(TAG, "onItemClick: ------");
}
});

item 布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal">

<!--标题-->
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="标题" />

<!--删除按钮-->
<Button
android:id="@+id/bt_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"/>

</LinearLayout>

属性 android:descendantFocusability 的值有三种:

1
2
3
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点,也就是各有各的焦点

树莓派android系统,vnc远程访问

A VNC Server for Android allows remotely accessing the device’s screen. In this post, I will show you how to run a VNC server on Android and access its screen remotely.

Let us clear one thing up front. We are not going to be discussing VNC clients for Android. We are going to be looking at the other side of Virtual Network Computing (VNC), which is running an actual VNC server on Android device.

Information on how to remotely access Android device’s homescreen or interface is sparse. Most of the information out there seem to look at the reverse scenario - accessing a remote server on Android device using VNC Client app. Read: Setup VNC Server on Ubuntu: Complete Ubuntu Remote Desktop Guide
Setting up a VNC server for Android just as simple. In this post, I will show you how to setup an Android VNC server without root.

WHAT IS A VNC SERVER?

VNC stands for Virtual Network Computing. It is a way of sharing the graphical desktop of a system to a remote system. In addition to the graphical interface, input events (keyboard, mouse, etc.), audio, and clipboard can also be shared with the remote system.

Xubuntu Desktop From Remote Server On Tightvnc Viewer In Windows

This is a super cool way to access remote systems and works as if you are actually in front of the remote workstation. With my recent move to Proxmox, VNC setup on Ubuntu Server has as been very handy for me. [Read: Ultimate Docker Home Server with Traefik 2, LE, and OAuth / Authelia [2020]]

In this post, we are going to try to access my Android phone’s interface via VNC from a Windows laptop.

What can you do with VNC Server on Android?

Well, this is the million dollar question isn’t it?

In my case, I have Android tablets showing Home Assistant dashboards for home automation. [Read: My Smart Home setup – All gadgets and apps I use in my automated home]

Sometimes I want to check what is showing on the dashboard, which having to be in front of it.

SmartHomeBeginner brings in-depth tutorials easy enough to understand even for beginners. This takes considerable amount of work. If this post helps you, please consider supporting us as a token of appreciation:

  • Feeling generous? Buy me a coffee (or two).
  • May be another day? Shop on Amazon using our links. Your prices won’t change but we get a small commission.
  • Don’t feel like spending? You can still show your support by sharing this post, linking to it in forums, or even commenting below.
    That is just one application. If you have other applications you can think, please share with the rest of the us in the comments section.

VNC SERVER FOR ANDROID WITHOUT ROOT

While there are numerous VNC clients for Android, there aren’t many VNC Server Android apps. In fact, when I did my research I could only find one that worked well: droidVNC-NG.

Droidvnc-Ng App - Vnc Server For Android

The best part is, droidVNC-NG app is free. Go ahead, head to the Play store and install droidVNC-NG and let us get started with setting up an Android VNC server. Alternatively, if you have droidVNC-NG VNC server Android apk, install it and proceed.

Is VNC Server free?
It depends on the server app. Some are paid. But there are free ones too (eg. droidVNC-NG for Android). Once a VNC Server is running, there are several free client apps to access the server.

  1. VNC Server Port and Password
    When you open the app, you should see an Android VNC server interface that looks like what is shown in the image below.
    Droidvnc App - Set The Port And Vnc Password

First, provide a port number for VNC server. Typically it is 5900. But you could change it to whatever (typically 59xx). In this Android VNC server guide, I am going to use 5902.

Next, provide a password for the VNC connection. Pick a strong password. In this tutorial, I am picking test (NOT a strong password).

We are not going to be discussing advanced configurations such as VNC through SSH tunnel, which provides an encrypted connection. Therefore, setting up a strong password is a bare minimum for security.

  1. Accessibility Permissions for Android VNC Server
    Next, we need to enable Accessibility permissions for the Android VNC server. This allows remote control of the Android device. So click on Screen Capturing and grant accessibility permissions to droidVNC-NG app, as shown below.

    Enable Android Accessibility Permissions
  2. Grant View and Control Permissions
    We also need to grant view and control permissions to the VNC server for Android. Click on Input to grant this permission in the screens that follow (shown below).

    Enable View And Control Permissions
  3. Grant File Access Permissions to VNC Server on Android
    Finally, click on File Access on the droidVNC-NG app configuration screen. Allow the app to access files on your android device, as shown below.

    Enable File Access Permissions
  4. Start VNC Server on Android
    Finally, hit the start button to start VNC server for Android. You will be presented with the warning screen shown below.

    Sensitive Information Casting Warning!!!

Carefully read and understand the implications prior to continuing. Once started, the Android VNC server should be listening for connections on the port you chose (5902 in this how-to).

Start Android Virtual Network Computing Server

In addition to the port number, you will also need the IP address of the Android device. As shown above, in this guide it is 192.168.1.120, which is the LAN IP address of my Android device.

If you are trying to access an Android system from outside your local network, then remember to setup port-forwarding on your router/gateway.

CONNECT TO ANDROID VNC SERVER

Now that our VNC Server on Android is running, let us see how to access the screen remotely. There are multiple ways to do this.

One of my favorite ways is to use Guacamole, which offers a HTML5 browser based access to VNC servers.

But for simplicity, in this guide, we are going to use TightVNC client for Windows. Using this VNC client app, we will view the Android device from my Windows system.

  1. Provide VNC Server details on the Client app
    Open the VNC client and provide the IP address and port number. In this case, they are 192.168.1.20:5902.
    Connect To Android Vnc Server Using Vnc Client

Wait!!! But the image above shows 2, and not 5902 for port.

As explained previously, VNC ports are usually in the range 5900 to 5999. For VNC ports, it is customary to provide only the incremental number from 5900. Therefore, for port 5902, the increment is 2. The TightVNC client app automatically trims the port number down to just the incremental number.

  1. Provide VNC Password
    Next provide the VNC password that you chose while setting up the VNC Server for Android.
    Enter Vnc Server Password

SmartHomeBeginner brings in-depth tutorials easy enough to understand even for beginners. This takes considerable amount of work. If this post helps you, please consider supporting us as a token of appreciation.
3. Access Android VNC Server on Windows
You should now be able to remotely access and interact with your Android device. You could work on it and control it using your mouse (and keyboard) - mouse-click to select, click and drag to move screen or scroll, etc.

Android Interface Via Vnc Client On Windows

So there you have it - the full graphical interface of Android that can be remotely controlled from practically any platform.

FINAL THOUGHTS ON RUNNING A VNC SERVER ON ANDROID

While this is all fun to do, I want to share my experience in using VNC Server for Android without root. Simply put, the performance was not great and it was not a smooth experience controlling the Android device using a mouse. It was choppy and slow. And this was on my Pixel 3, which has decent specifications at this point (March 2021).

On older Android devices, the experience might be even worse. Nevertheless, the droidVNC-NG app works great, is really one of a kind at this point, and free to use.

There are alternatives such as the VNC Server app from XDA Developers. This app allows both wifi and USB access. Wired access may provide a smoother experience but I have not tried this yet. If you have tried this, I would appreciate if you share your experience in the comments.

Other than that, it was a fun and easy project to setup a VNC Server for Android and access it remotely.

树莓派android系统,无屏远程访问

当树莓派首次安装完Android系统,需要借助屏幕设置wifi密码(暂时没有其他好方案)。设置完成后,能够正常上网,后续操作可以在无屏的方式下,远程访问树莓派。
一般远程访问有多种方案,这里介绍两种方式:

  • vysor投屏
  • vnc投屏

准备工具

  • vysor
  • vnc viewer
  • droidVNC-NG
  • adb
  • mac book & pc

打开Android开发者设置

参考《树莓派安装Android操作系统

方式一:vysor投屏

vysor是一款手机投屏软件,通过PC控制手机。通过官网https://www.vysor.io/ 下载桌面版软件或chrome插件,安装完成后,启动软件。

  • 按照下图,连接adb server:

  • 链接成功后,如下图
  • 点击上图“播放”按钮,开始远程投屏

注意:

  1. vysor可以通过USB或wifi连接(需要收费)。
  2. USB是通过ADB方式连接,同一时间只能一个连接,如Android studio开发联调和vysor投屏会冲突。

方式二:vnc投屏

参考《树莓派android系统,vnc远程访问

android修改包名方法

在 Studio 3.0之后, 可直接通过 Androidmenifest 修改部分包名。

修改流程如下:

  • 进入Androidmanifest.xml 文件,找到 package 名称,选中需要修改的部分。
    比如原包名为
    com.kch8.android
    如果需要修改中间的 kch8 ,那么我们就选中 kch8 ,
  • 右键 -> Refactor -> Rename , (Mac 快捷键为 fn + shift+F6)
  • 然后选择 Rename package , 输入要修改目标的名称 ,直接点击 Refactor , 左下方继续点击 Do Refactor
  • 包名修改成功后,选择build -> clean project,然后关闭Studio
  • 删除工程根目录的.idea文件夹
  • 通过Studio重新开启工程和运行 工程

通过上述操作,修改包名后,同时热更新也生效!

greendao缓存源码分析

GreenDao是Android中使用比较广泛的一个orm数据库,以高效和便捷著称。在项目开发过程中遇到过好几次特别奇葩的问题,最后排查下来,发现还是由于不熟悉它的缓存机制引起的。下面是自己稍微阅读了下它的源码后做的记录,避免以后发现类似的问题。

缓存机制相关源码

DaoMaster

DaoMaster是GreenDao的入口,它的继承自AbstractDaoMaster,有三个重要的参数,分别是实例、版本和Dao的信息。

1
2
3
4
5
6
7
8
//数据库示例
protected final SQLiteDatabase db;

//数据库版本
protected final int schemaVersion;

//dao和daoconfig的配置
protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;

DaoMaster中还有两个重要的方法:createAllTables和dropAllTables,和一个抽象的OpenHelper类,该类继承自系统的SQLiteOpenHelper类,主要用于数据库创建的时候初始化所有数据表。

创建DaoMaster需要传入SQLiteDatabase的实例,一般如下创建:

1
mDaoMaster = new DaoMaster(helper.getWritableDatabase())

跟踪代码可知数据库的初始化和升降级都是在调用helper.getWritableDatabase()时执行的。相关代码在SQLiteOpenHelper类中。

在getWritableDatabase方法中会调用getDatabaseLocked方法

1
2
3
4
5
public SQLiteDatabase getWritableDatabase() {
synchronized (this) {
return getDatabaseLocked(true);
}
}

getDatabaseLocked方法如下

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
private SQLiteDatabase getDatabaseLocked(boolean writable) {
// 首先方法接收一个是否可读的参数
if (mDatabase != null) {
if (!mDatabase.isOpen()) {
//数据库没有打开,关闭并且置空
mDatabase.close().
mDatabase = null;
} else if (!writable || !mDatabase.isReadOnly()) {
//只读或者数据库已经是读写状态了,则直接返回实例
return mDatabase;
}
}

if (mIsInitializing) {
throw new IllegalStateException("getDatabase called recursively");
}

SQLiteDatabase db = mDatabase;
try {
mIsInitializing = true;

if (db != null) {
if (writable && db.isReadOnly()) {
//只读状态的时候打开读写
db.reopenReadWrite();
}
} else if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
//创建数据库实例
。。。代码省略。。。

//调用子类的onConfigure方法
onConfigure(db);

final int version = db.getVersion();
if (version != mNewVersion) {
if (db.isReadOnly()) {
throw new SQLiteException("Can't upgrade read-only database from version " +
db.getVersion() + " to " + mNewVersion + ": " + mName);
}

db.beginTransaction();
try {
if (version == 0) {
// 如果版本为0的时候初始化数据库,调用子类的onCreate方法。
onCreate(db);
} else {
//处理升降级
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}

onOpen(db);

if (db.isReadOnly()) {
Log.w(TAG, "Opened " + mName + " in read-only mode");
}

mDatabase = db;
return db;
} finally {
mIsInitializing = false;
if (db != null && db != mDatabase) {
db.close();
}
}}
}

greendao的缓存到底是如何实现的呢?

DaoMaster构造方法中会把所有的Dao类注册到Map中,每个Dao对应一个DaoConfig配置类。

1
2
3
4
protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
DaoConfig daoConfig = new DaoConfig(db, daoClass);
daoConfigMap.put(daoClass, daoConfig);
}

DaoConfig是对数据库表的一个抽象,有数据库实例、表名、字段列表、SQL statements等类变量,最重要的是IdentityScope,它是GreenDao实现数据缓存的关键。在DaoSession类初始化的时候IdentityScope初始化,可以根据参数IdentityScopeType.Session和IdentityScopeType.None来配置是否开启缓存。

IdentityScope接口有两个实现类,分别是IdentityScopeLong和IdentityScopeObject,它们的实现类似,都是维护一个Map存放key和value,然后有一些put、get、remove、clear等方法,最主要的区别是前者的key是long,可以实现更高的读写效率,后面的key是Object。

判断主键字段类型是否是数字类型,如果是的话则使用IdentityScopeLong类型来缓存数据,否则使用IdentityScopeObject类型。

1
keyIsNumeric = type.equals(long.class) || type.equals(Long.class) || type.equals(int.class)|| type.equals(Integer.class) || type.equals(short.class) || type.equals(Short.class)|| type.equals(byte.class) || type.equals(Byte.class);
1
2
3
4
5
6
7
8
9
10
11
12
13
public void initIdentityScope(IdentityScopeType type) {
if (type == IdentityScopeType.None) {
identityScope = null;
} else if (type == IdentityScopeType.Session) {
if (keyIsNumeric) {
identityScope = new IdentityScopeLong();
} else {
identityScope = new IdentityScopeObject();
}
} else {
throw new IllegalArgumentException("Unsupported type: " + type);
}
}

缓存的使用

数据读取

以Query类中list方法为例,跟踪代码可知,最后会调用AbstractDao的loadCurrent方法,它首先会根据主键判断dentityScope中有没有对应的缓存,如何有直接返回,如果没有才会读取Cursor里面的数据。

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
final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
if (identityScopeLong != null) {
if (offset != 0) {
// Occurs with deep loads (left outer joins)
if (cursor.isNull(pkOrdinal + offset)) {
return null;
}
}
//读取主键
long key = cursor.getLong(pkOrdinal + offset);
//读取缓存
T entity = lock ? identityScopeLong.get2(key) :identityScopeLong.get2NoLock(key);
if (entity != null) {
//如果有,直接返回
return entity;
} else {
//如果没有,读取游标中的值
entity = readEntity(cursor, offset);
attachEntity(entity);
//把数据更新到缓存中
if (lock) {
identityScopeLong.put2(key, entity);
} else {
identityScopeLong.put2NoLock(key, entity);
}
return entity;
}
} else if (identityScope != null) {
K key = readKey(cursor, offset);
if (offset != 0 && key == null) {
// Occurs with deep loads (left outer joins)
return null;
}
T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key);
if (entity != null) {
return entity;
} else {
entity = readEntity(cursor, offset);
attachEntity(key, entity, lock);
return entity;
}
} else {
// Check offset, assume a value !=0 indicating a potential outer join, so check PK
if (offset != 0) {
K key = readKey(cursor, offset);
if (key == null) {
// Occurs with deep loads (left outer joins)
return null;
}
}
T entity = readEntity(cursor, offset);
attachEntity(entity);
return entity;
}
}

数据删除
我们经常使用DeleteQuery的executeDeleteWithoutDetachingEntities来条件删除数据,这时候是不清除缓存的,当用主键查询的时候,还是会返回缓存中的数据。

Deletes all matching entities without detaching them from the identity scope (aka session/cache). Note that this method may lead to stale entity objects in the session cache. Stale entities may be returned when loaded by their primary key, but not using queries.

使用对象的方式删除数据的时候,比如deleteInTx()等面向对象的方法时,会删除对应的缓存。在AbstractDao中deleteInTxInternal方法里面,会调用identityScope的remove方法。

1
2
3
if (keysToRemoveFromIdentityScope != null && identityScope != null) {
identityScope.remove(keysToRemoveFromIdentityScope);
}

数据插入

以insert方法为例,它会在插入成功之后,调用attachEntity方法,存放缓存数据。

1
2
3
4
5
6
7
8
9
10
protected final void attachEntity(K key, T entity, boolean lock) {
attachEntity(entity);
if (identityScope != null && key != null) {
if (lock) {
identityScope.put(key, entity);
} else {
identityScope.putNoLock(key, entity);
}
}
}

数据更新

数据update的时候也会调用attachEntity方法。

缓存带来的坑和脱坑方案
1.触发器引起的数据不同步
我们在项目中有这么一个需求,当改变A对象的a字段的时候,要同时改变B对象的b字段,触发器代码类似如下。

1
2
3
String sql = "create trigger 触发器名 after insert on 表B "
+ "begin update 表A set 字段A.a = NEW. 字段B.b where 字段A.b = NEW.字段B.c; end;";
db.execSQL(sql);

b是A的外键,映射到表B的b字段。

这样设置触发器之后,更新表B数据的时候,会自动把更新同步到表A,但是这样其实没有更新表A对应DAO的缓存,当查询表A的时候还是更新前的数据。

解决方案:
1.在greendao2.x版本中,可以暴露DaoSession中对应的DaoConfig
,然后调用daoConfig.clearIdentityScope();在3.x版本中可以直接调用dao类的detachAll方法,它会清除所有的缓存。 同时也可以调用Entity的refresh方法来刷新缓存。

1
2
3
4
5
public void detachAll() {
if (identityScope != null) {
identityScope.clear();
}
}

上面的方法都是通过清除缓存来保证数据的同步性,但是频繁的清除缓存就大大影响数据查询效率,不建议这么使用。

2.尽量不要使用触发器,最好使用greenDao自带的一些接口,绝大部分情况下都是能满足要求的。对于能否使用触发器,开发者做了解释。

greenDAO uses a plain SQLite database. To use triggers you have to do a regular raw SQL query on your database. greenDAO can not help you there.

2.自定义SQL带来的数据不同步问题
项目中即使使用了GreenDao,我们还是免不了使用自定义的sql语句来操作数据库,类似下面较复杂的查询功能。

1
2
3
String sql = "select *, count(distinct " + columnPkgName + ") from " + tableName + " where STATUS = 0" + " group by " + columnPkgName
+ " order by " + columnTimestamp + " desc limit " + limitCount + " offset 0;";
Cursor query = mDaoMaster.getDatabase().rawQuery(sql, new String[] {});

这种查询语句除了没有使用GreenDao的缓存,其它倒是没有什么问题。但是一旦使用update或者delete等接口时,就会引起数据的不同步,因为数据库里面的数据更新了,但是greenDao里面的缓存还是旧的。

总结:使用第三方库的时候,最好能够深入理解它的代码,不然遇到坑了都不知道怎么爬出来,像greendao这种,由于自己不合理使用导致的问题还是很多的。