最近看了一個Android CTS的問題,最后發現是Google CTS package里的一個case 亂用了一個 Android Non-public attrs而引起的,寫下這篇文章記錄一下。
轉載請標明來處:http://www.lxweimin.com/p/2d54c41c0dfa
一、環境
OS: Android 7.0
CTS version: android-cts-7.0_r1
重現步驟:
run cts -m CtsServicesHostTestCases -t android.server.cts.ActivityManagerPinnedStackTests#testAlwaysFocusablePipActivity
--logcat-on-failure
觀察到的現象:
Test failing with error 'junit.framework.AssertionFailedError:
Pinned stack must be the focused stack. expected:<4> but was:<0>'
二、testAlwaysFocusablePipActivity
在CTS code里找到 ActivityManagerPinnedStackTests.java 里的testAlwaysFocusablePipActivity 方法, 這個Case主要是執行adb shell命令,然后 dumpsys 一些信息來檢查是否滿足預期。 整理如下:
adb shell am start -n android.server.app/.AlwaysFocusablePipActivity
adb shell am stack move-top-activity-to-pinned-stack 1 0 0 500 500
adb shell dumpsys activity activities
測試步驟主要分為三步:
- 啟動 AlwaysFocusablePipActivity
- 將 AlwaysFocusablePipActivity 從stack 1 移動到 PINNED stack (stack id 4).
- dump activity的activities信息, 檢查 mFocusedStack 是否 stack id為4.
最后的期望的 stack id 為4
mFocusedStack=ActivityStack{7d928bf stackId=4, 2 tasks}
其中 adb shell am stack move-top-activity-to-pinned-stack 1 0 0 500 500 這句命令,目的就是將 AlwaysFocusablePipActivity 從stack 1 移動到 PINNED stack (stack id 4), 然后將 PINNED Stack設置為 Focused 的stack.
代碼跟蹤
moveTopActivityToPinnedStack
moveTopStackActivityToPinnedStackLocked
moveActivityToPinnedStackLocked
moveTaskToStackLocked
moveTaskToStackUncheckedLocked
moveToFrontAndResumeStateIfNeeded
moveToFront
setFocusStackUnchecked
接著看下 setFocusStackUnchecked
void setFocusStackUnchecked(String reason, ActivityStack focusCandidate) {
if (!focusCandidate.isFocusable()) {
// The focus candidate isn't focusable. Move focus to the top stack that is focusable.
focusCandidate = focusCandidate.getNextFocusableStackLocked();
}
}
boolean isFocusable() {
if (StackId.canReceiveKeys(mStackId)) {
return true;
}
// The stack isn't focusable. See if its top activity is focusable to force focus on the
// stack.
final ActivityRecord r = topRunningActivityLocked();
return r != null && r.isFocusable();
}
public static boolean canReceiveKeys(int stackId) {
return stackId != PINNED_STACK_ID;
}
boolean isFocusable() {
return StackId.canReceiveKeys(task.stack.mStackId) || isAlwaysFocusable();
}
boolean isAlwaysFocusable() {
return (info.flags & FLAG_ALWAYS_FOCUSABLE) != 0;
}
前面在移動 Activity Stack 的時候都是 ok 的,但是當要把當前 PINNED stack設置為 Focused stack 的時候卻出現問題了,
在設置一個 stack 為 focused stack 的時候,AMS會去檢查這個 stack 是否是 可 focusable 的!其中 PINNED STACK 是不能接收Key的,所以只有topRunningActivity 是可 Focusable 的時候,那么 PINNED stack 才能Focusable.
從 isAlwaysFocusable() 函數可以看出來,只有當 PINNED STACK 里的 Activity的 flag 設置了FLAG_ALWAYS_FOCUSABLE 才能focuse.
而 FLAG_ALWAYS_FOCUSABLE 又是個什么標志呢?
三、FLAG_ALWAYS_FOCUSABLE標志
FLAG_ALWAYS_FOCUSABLE 這個標志是在 PKMS 去解析AndroidManifest.xml里設置的。
if (sa.getBoolean(R.styleable.AndroidManifestActivity_alwaysFocusable, false)) {
a.info.flags |= FLAG_ALWAYS_FOCUSABLE;
}
那么問題就轉換成要為 AlwaysFocusablePipActivity 設置 alwaysFocusable 屬性了。找到 AlwaysFocusablePipActivity 所在的 AndroidManifest.xml 里的描述,
<activity android:name=".AlwaysFocusablePipActivity"
android:theme="@style/Theme.Transparent"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
androidprv:alwaysFocusable="true"
android:exported="true"
android:taskAffinity="nobody.but.AlwaysFocusablePipActivity"/>
很明顯 alwaysFocusable 已經被設置為 true (這里的 androidprv只是一個命名空間,無實際用處), 但是PKMS 竟然沒有把它給解析出來。 那這肯定就是 PKMS 的問題了? 不然,請繼續查看
四、PKMS解析 alwaysFocusable 屬性
明明已經設置了 alwaysFocusable 屬性了,為什么 PKMS 解析不出來呢???? 這么奇怪的問題。
從上一節中PKMS 獲得該屬性的方法得知,無非就是從 TypedArray 里去獲得屬性值,這還能有錯?莫非是AssetManager沒有把該值解析出來,趕緊 debug AssetManager.
TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestActivity);
R.styleable.AndroidManifestActivity 定義在 framework-res里的attrs_manifest.xml中
<declare-styleable name="AndroidManifestActivity" parent="AndroidManifestApplication">
<!-- Required name of the class implementing the activity, deriving from
{@link android.app.Activity}. This is a fully
qualified class name (for example, com.mycompany.myapp.MyActivity); as a
short-hand if the first character of the class
is a period then it is appended to your package name. -->
<attr name="name" />
<attr name="theme" />
<attr name="label" />
<attr name="description" />
<attr name="icon" />
<attr name="banner" />
...
<attr name="alwaysFocusable" format="boolean" />
<attr name="enableVrMode" />
</declare-styleable>
R.styleable.AndroidManifestActivity 是一組int型數組, 數組元素是一個一個屬性Id 值,而該值是通過 aapt編譯出來的ID, 具體可以從 public_resources.xml里查看,該xml文件里面包含了所有的整個系統的resource id.
out/debug/target/common/obj/APPS/framework-res_intermediates/public_resources.xml
- 本地編譯的屬性值
<public type="attr" name="theme" id="0x01010000" />
<public type="attr" name="label" id="0x01010001" />
<public type="attr" name="icon" id="0x01010002" />
<public type="attr" name="name" id="0x01010003" />
<!-- Declared at frameworks/base/core/res/res/values/attrs_manifest.xml:1882 -->
<public type="^attr-private" name="systemUserOnly" id="0x011600c4" />
<!-- Declared at frameworks/base/core/res/res/values/attrs_manifest.xml:1899 -->
<public type="^attr-private" name="alwaysFocusable" id="0x011600c5" />
obtainAttributes 最終會調用 frameworks/base/core/jni/android_util_AssetManager.cpp
的 android_content_AssetManager_retrieveAttributes 函數, 而該函數獲得對應屬性值就是通過該屬性的 id 值去編譯出來的 xml文件獲得。
而且很奇怪的一點,用本地編出來的CTS package去測試,結果就PASS了,而AOSP的CTS package 確是失敗的。 這時通過加log調試, 發現 AOSP 編出來的CTS 里的 alwaysFocusable 的屬性值與本地編出來的屬性是不一樣的。
- AOSP 屬性值
<!-- Declared at frameworks/base/core/res/res/values/attrs_manifest.xml:1899 -->
<public type="^attr-private" name="alwaysFocusable" id="0x011600c3" />
從這里基本就可以知道為什么 alwaysFocusable 不能被解析出來了。
五、結論
為什么本地與AOSP編譯出來的 alwaysFocusable 的 ID 屬性值是不一樣的呢?
從 type="^attr-private 與 type="attr" 大概可以猜出來,alwaysFocusable 應該是 私有的 屬性,非public的屬性。
定義成 public 的屬性不管是在什么平臺下編出來,其 ID 值應該是一樣的。而非public 的屬性就不一定了,因為vendor可能自己會定義一些屬性,這樣 aapt 在解析這些屬性, 并給他們賦值時, 可能就會不一樣。
而public的屬性,作為 AOSP 提供給上層 app 開發者,它們的值必須一樣,否則就會導致app 在不同的廠商不兼容的情況。
public的屬性值可以從 frameworks/base/core/res/res/values/public.xml 里查看
因此,從這里可以看出來這個是 Google 的問題, 因為Google 用了非public 的屬性值編譯出來的CTS package 來測試不同的 vendor, 這樣做是不正確的。