diff --git a/_assets/snapshot-1686629593092.json b/_assets/snapshot-1686629593092.json
new file mode 100644
index 0000000..657824b
--- /dev/null
+++ b/_assets/snapshot-1686629593092.json
@@ -0,0 +1 @@
+{"id":1686629593092,"appId":"com.zhihu.android","activityId":"com.zhihu.android.mixshortcontainer.MixShortContainerActivity","appName":"知乎","appVersionCode":16116,"appVersionName":"9.6.0","screenHeight":2712,"screenWidth":1220,"isLandscape":false,"device":"diting","model":"22081212C","manufacturer":"Xiaomi","brand":"Redmi","sdkInt":33,"release":"13","nodes":[{"id":0,"pid":-1,"index":0,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":0,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":1,"pid":0,"index":0,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":1,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":2,"pid":1,"index":0,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":2,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":3,"pid":2,"index":0,"attr":{"id":"com.zhihu.android:id/action_bar_root","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":3,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":4,"pid":3,"index":0,"attr":{"id":"android:id/content","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":4,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":5,"pid":4,"index":0,"attr":{"id":"com.zhihu.android:id/content_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":5,"index":0,"depth":5,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":6,"pid":5,"index":0,"attr":{"id":"com.zhihu.android:id/overlay_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":6,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":7,"pid":5,"index":1,"attr":{"id":"android:id/content","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":6,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":8,"pid":5,"index":2,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":2,"depth":6,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":9,"pid":5,"index":3,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":3,"depth":6,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":10,"pid":5,"index":4,"attr":{"id":"com.zhihu.android:id/fragment_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":4,"depth":6,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":11,"pid":10,"index":0,"attr":{"id":"com.zhihu.android:id/parent_fragment_content_id","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":7,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":12,"pid":11,"index":0,"attr":{"id":"com.zhihu.android:id/rootView","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":6,"index":0,"depth":8,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":13,"pid":12,"index":0,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":9,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":14,"pid":12,"index":1,"attr":{"id":"com.zhihu.android:id/consecutiveScrollerLayout","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":1,"depth":9,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":15,"pid":12,"index":2,"attr":{"id":"com.zhihu.android:id/next_button_layout","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":9,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":16,"pid":12,"index":3,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":3,"depth":9,"left":0,"top":0,"right":1220,"bottom":88}},{"id":17,"pid":12,"index":4,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":4,"index":4,"depth":9,"left":0,"top":88,"right":1220,"bottom":220}},{"id":18,"pid":12,"index":5,"attr":{"id":"com.zhihu.android:id/overlay_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":5,"depth":9,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":19,"pid":17,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":10,"left":48,"top":94,"right":156,"bottom":202}},{"id":20,"pid":17,"index":1,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":10,"left":78,"top":124,"right":126,"bottom":172}},{"id":21,"pid":17,"index":2,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":10,"left":1064,"top":94,"right":1172,"bottom":202}},{"id":22,"pid":17,"index":3,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":3,"depth":10,"left":1094,"top":124,"right":1142,"bottom":172}},{"id":23,"pid":15,"index":0,"attr":{"id":null,"name":"androidx.appcompat.widget.LinearLayoutCompat","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":10,"left":1046,"top":1935,"right":1190,"bottom":2079}},{"id":24,"pid":23,"index":0,"attr":{"id":"com.zhihu.android:id/container_layout_view","name":"androidx.appcompat.widget.LinearLayoutCompat","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":0,"depth":11,"left":1064,"top":1952,"right":1172,"bottom":2060}},{"id":25,"pid":24,"index":0,"attr":{"id":"com.zhihu.android:id/next_arrow_view","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":12,"left":1088,"top":1976,"right":1148,"bottom":2036}},{"id":26,"pid":24,"index":1,"attr":{"id":"com.zhihu.android:id/next_text_view","name":"android.widget.TextView","text":"下一个","textLen":3,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":12,"left":1148,"top":1976,"right":1148,"bottom":2036}},{"id":27,"pid":14,"index":0,"attr":{"id":"com.zhihu.android:id/detailContainer","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":10,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":28,"pid":14,"index":1,"attr":{"id":"com.zhihu.android:id/middle_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":10,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":29,"pid":14,"index":2,"attr":{"id":"com.zhihu.android:id/listContainer","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":10,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":30,"pid":29,"index":0,"attr":{"id":"com.zhihu.android:id/root","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":11,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":31,"pid":30,"index":0,"attr":{"id":"com.zhihu.android:id/refresh","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":12,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":32,"pid":31,"index":0,"attr":{"id":"com.zhihu.android:id/recycler","name":"androidx.recyclerview.widget.RecyclerView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":20,"index":0,"depth":13,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":33,"pid":32,"index":0,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":14,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":34,"pid":32,"index":1,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":1,"depth":14,"left":0,"top":2760,"right":1220,"bottom":2712}},{"id":35,"pid":32,"index":2,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":2,"depth":14,"left":0,"top":2904,"right":1220,"bottom":2712}},{"id":36,"pid":32,"index":3,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":3,"depth":14,"left":0,"top":3594,"right":1220,"bottom":2712}},{"id":37,"pid":32,"index":4,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":4,"depth":14,"left":0,"top":3744,"right":1220,"bottom":2712}},{"id":38,"pid":32,"index":5,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":5,"depth":14,"left":0,"top":3792,"right":1220,"bottom":2712}},{"id":39,"pid":32,"index":6,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":6,"depth":14,"left":0,"top":3810,"right":1220,"bottom":2712}},{"id":40,"pid":32,"index":7,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":7,"depth":14,"left":0,"top":3858,"right":1220,"bottom":2712}},{"id":41,"pid":32,"index":8,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":8,"depth":14,"left":0,"top":4002,"right":1220,"bottom":2712}},{"id":42,"pid":32,"index":9,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":9,"depth":14,"left":0,"top":4173,"right":1220,"bottom":2712}},{"id":43,"pid":32,"index":10,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":10,"depth":14,"left":0,"top":4259,"right":1220,"bottom":2712}},{"id":44,"pid":32,"index":11,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":11,"depth":14,"left":0,"top":4409,"right":1220,"bottom":2712}},{"id":45,"pid":32,"index":12,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":12,"depth":14,"left":0,"top":4457,"right":1220,"bottom":2712}},{"id":46,"pid":32,"index":13,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":13,"depth":14,"left":0,"top":4475,"right":1220,"bottom":2712}},{"id":47,"pid":32,"index":14,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":14,"depth":14,"left":0,"top":4523,"right":1220,"bottom":2712}},{"id":48,"pid":32,"index":15,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":15,"depth":14,"left":0,"top":4667,"right":1220,"bottom":2712}},{"id":49,"pid":32,"index":16,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":16,"depth":14,"left":0,"top":4764,"right":1220,"bottom":2712}},{"id":50,"pid":32,"index":17,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":17,"depth":14,"left":0,"top":5191,"right":1220,"bottom":2712}},{"id":51,"pid":32,"index":18,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":18,"depth":14,"left":0,"top":5288,"right":1220,"bottom":2712}},{"id":52,"pid":32,"index":19,"attr":{"id":null,"name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":19,"depth":14,"left":0,"top":5355,"right":1220,"bottom":2712}},{"id":53,"pid":52,"index":0,"attr":{"id":"com.zhihu.android:id/layout_bottom_root","name":"android.widget.RelativeLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":0,"depth":15,"left":48,"top":5355,"right":1172,"bottom":2712}},{"id":54,"pid":53,"index":0,"attr":{"id":"com.zhihu.android:id/mask_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":16,"left":48,"top":5355,"right":1172,"bottom":2712}},{"id":55,"pid":53,"index":1,"attr":{"id":"com.zhihu.android:id/layout_expand","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":1,"depth":16,"left":938,"top":5355,"right":1172,"bottom":2712}},{"id":56,"pid":53,"index":2,"attr":{"id":"com.zhihu.android:id/combine_view","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":16,"left":48,"top":5355,"right":1172,"bottom":2712}},{"id":57,"pid":56,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":17,"left":48,"top":5397,"right":1172,"bottom":2712}},{"id":58,"pid":57,"index":0,"attr":{"id":"com.zhihu.android:id/voteLayout","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":18,"left":48,"top":5397,"right":465,"bottom":2712}},{"id":59,"pid":57,"index":1,"attr":{"id":"com.zhihu.android:id/rightLayout","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":1,"depth":18,"left":483,"top":5397,"right":1172,"bottom":2712}},{"id":60,"pid":59,"index":0,"attr":{"id":"com.zhihu.android:id/drawOrderLinearLayout","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":4,"index":0,"depth":19,"left":483,"top":5397,"right":1172,"bottom":2712}},{"id":61,"pid":60,"index":0,"attr":{"id":"com.zhihu.android:id/likeView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":3,"index":0,"depth":20,"left":483,"top":5409,"right":567,"bottom":2712}},{"id":62,"pid":60,"index":1,"attr":{"id":"com.zhihu.android:id/collectView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":1,"depth":20,"left":679,"top":5409,"right":763,"bottom":2712}},{"id":63,"pid":60,"index":2,"attr":{"id":"com.zhihu.android:id/commentView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":2,"depth":20,"left":874,"top":5409,"right":958,"bottom":2712}},{"id":64,"pid":60,"index":3,"attr":{"id":"com.zhihu.android:id/followWithAvatarView","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":3,"depth":20,"left":1070,"top":5397,"right":1172,"bottom":2712}},{"id":65,"pid":64,"index":0,"attr":{"id":"com.zhihu.android:id/avatar_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":21,"left":1070,"top":5397,"right":1172,"bottom":2712}},{"id":66,"pid":64,"index":1,"attr":{"id":"com.zhihu.android:id/follow","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":1,"depth":21,"left":1070,"top":5463,"right":1172,"bottom":2712}},{"id":67,"pid":66,"index":0,"attr":{"id":"com.zhihu.android:id/anim","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":22,"left":1088,"top":5463,"right":1154,"bottom":2712}},{"id":68,"pid":67,"index":0,"attr":{"id":"com.zhihu.android:id/pag_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":23,"left":1088,"top":5463,"right":1154,"bottom":2712}},{"id":69,"pid":65,"index":0,"attr":{"id":"com.zhihu.android:id/avatar","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":1070,"top":5397,"right":1172,"bottom":2712}},{"id":70,"pid":63,"index":0,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":874,"top":5409,"right":958,"bottom":2712}},{"id":71,"pid":63,"index":1,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"5","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":922,"top":5409,"right":958,"bottom":2712}},{"id":72,"pid":62,"index":0,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":679,"top":5409,"right":763,"bottom":2712}},{"id":73,"pid":62,"index":1,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"15","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":727,"top":5409,"right":763,"bottom":2712}},{"id":74,"pid":61,"index":0,"attr":{"id":"com.zhihu.android:id/animation","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":21,"left":483,"top":5409,"right":567,"bottom":2712}},{"id":75,"pid":61,"index":1,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":483,"top":5409,"right":567,"bottom":2712}},{"id":76,"pid":61,"index":2,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"16","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":21,"left":531,"top":5409,"right":567,"bottom":2712}},{"id":77,"pid":74,"index":0,"attr":{"id":"com.zhihu.android:id/pag_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":483,"top":5409,"right":567,"bottom":2712}},{"id":78,"pid":58,"index":0,"attr":{"id":"com.zhihu.android:id/voteView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":19,"left":48,"top":5397,"right":387,"bottom":2712}},{"id":79,"pid":78,"index":0,"attr":{"id":"com.zhihu.android:id/root","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":0,"depth":20,"left":48,"top":5397,"right":387,"bottom":2712}},{"id":80,"pid":79,"index":0,"attr":{"id":"com.zhihu.android:id/vote_tv","name":"android.widget.TextView","text":" 148 ","textLen":5,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":21,"left":52,"top":5397,"right":221,"bottom":2712}},{"id":81,"pid":79,"index":1,"attr":{"id":"com.zhihu.android:id/divider","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":256,"top":5424,"right":259,"bottom":2712}},{"id":82,"pid":79,"index":2,"attr":{"id":"com.zhihu.android:id/down_tv","name":"android.widget.TextView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":21,"left":293,"top":5397,"right":383,"bottom":2712}},{"id":83,"pid":55,"index":0,"attr":{"id":"com.zhihu.android:id/tv_expand","name":"android.widget.TextView","text":"展开","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":17,"left":992,"top":5355,"right":1082,"bottom":2712}},{"id":84,"pid":55,"index":1,"attr":{"id":"com.zhihu.android:id/iv_expand","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":17,"left":1082,"top":5355,"right":1142,"bottom":2712}},{"id":85,"pid":51,"index":0,"attr":{"id":"com.zhihu.android:id/holder_text","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":5288,"right":1172,"bottom":2712}},{"id":86,"pid":85,"index":0,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":16,"left":48,"top":5288,"right":1172,"bottom":2712}},{"id":87,"pid":86,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"2013年4月 某楼盘开盘动工","textLen":15,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":17,"left":48,"top":5288,"right":664,"bottom":2712}},{"id":88,"pid":50,"index":0,"attr":{"id":"com.zhihu.android:id/holder_text","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":5191,"right":1172,"bottom":2712}},{"id":89,"pid":88,"index":0,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":16,"left":48,"top":5191,"right":1172,"bottom":2712}},{"id":90,"pid":89,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"2012年 某楼盘开始认购","textLen":13,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":17,"left":48,"top":5191,"right":582,"bottom":2712}},{"id":91,"pid":49,"index":0,"attr":{"id":"com.zhihu.android:id/holder_image","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":4764,"right":1172,"bottom":2712}},{"id":92,"pid":91,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":16,"left":48,"top":4764,"right":1172,"bottom":2712}},{"id":93,"pid":92,"index":0,"attr":{"id":"com.zhihu.android:id/image_normal","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":17,"left":48,"top":4764,"right":1172,"bottom":2712}},{"id":94,"pid":92,"index":1,"attr":{"id":"com.zhihu.android:id/image_custom","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":17,"left":1172,"top":5161,"right":1172,"bottom":2712}},{"id":95,"pid":48,"index":0,"attr":{"id":"com.zhihu.android:id/holder_text","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":4667,"right":1172,"bottom":2712}},{"id":96,"pid":95,"index":0,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":16,"left":48,"top":4667,"right":1172,"bottom":2712}},{"id":97,"pid":96,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"谁说没人管的?我先放一个真实案例在这里","textLen":19,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":17,"left":48,"top":4667,"right":1017,"bottom":2712}},{"id":98,"pid":47,"index":0,"attr":{"id":"com.zhihu.android:id/author_info_view","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":4523,"right":1172,"bottom":2712}},{"id":99,"pid":98,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":6,"index":0,"depth":16,"left":48,"top":4523,"right":1172,"bottom":2712}},{"id":100,"pid":99,"index":0,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":17,"left":48,"top":4523,"right":156,"bottom":2712}},{"id":101,"pid":99,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"知名魔法少女","textLen":6,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":1,"depth":17,"left":180,"top":4523,"right":450,"bottom":2712}},{"id":102,"pid":99,"index":2,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":17,"left":462,"top":4523,"right":824,"bottom":2712}},{"id":103,"pid":99,"index":3,"attr":{"id":null,"name":"android.widget.TextView","text":"炒饭大师&冠绝男神","textLen":9,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":3,"depth":17,"left":180,"top":4586,"right":836,"bottom":2712}},{"id":104,"pid":99,"index":4,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":4,"depth":17,"left":854,"top":4532,"right":1070,"bottom":2712}},{"id":105,"pid":99,"index":5,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":5,"depth":17,"left":1100,"top":4541,"right":1172,"bottom":2712}},{"id":106,"pid":104,"index":0,"attr":{"id":"com.zhihu.android:id/btn_people_follow","name":"android.widget.TextView","text":"关注","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":18,"left":854,"top":4532,"right":1070,"bottom":2712}},{"id":107,"pid":102,"index":0,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":18,"left":462,"top":4523,"right":516,"bottom":2712}},{"id":108,"pid":46,"index":0,"attr":{"id":"com.zhihu.android:id/layout_space","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":0,"top":4475,"right":1220,"bottom":2712}},{"id":109,"pid":45,"index":0,"attr":{"id":"com.zhihu.android:id/layout_space","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":48,"top":4457,"right":1172,"bottom":2712}},{"id":110,"pid":44,"index":0,"attr":{"id":"com.zhihu.android:id/layout_space","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":0,"top":4409,"right":1220,"bottom":2712}},{"id":111,"pid":43,"index":0,"attr":{"id":"com.zhihu.android:id/layout_bottom_root","name":"android.widget.RelativeLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":4259,"right":1172,"bottom":2712}},{"id":112,"pid":111,"index":0,"attr":{"id":"com.zhihu.android:id/combine_view","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":16,"left":48,"top":4259,"right":1172,"bottom":2712}},{"id":113,"pid":112,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":17,"left":48,"top":4301,"right":1172,"bottom":2712}},{"id":114,"pid":113,"index":0,"attr":{"id":"com.zhihu.android:id/voteLayout","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":18,"left":48,"top":4301,"right":465,"bottom":2712}},{"id":115,"pid":113,"index":1,"attr":{"id":"com.zhihu.android:id/rightLayout","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":1,"depth":18,"left":483,"top":4301,"right":1172,"bottom":2712}},{"id":116,"pid":115,"index":0,"attr":{"id":"com.zhihu.android:id/drawOrderLinearLayout","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":4,"index":0,"depth":19,"left":483,"top":4301,"right":1172,"bottom":2712}},{"id":117,"pid":116,"index":0,"attr":{"id":"com.zhihu.android:id/likeView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":3,"index":0,"depth":20,"left":483,"top":4313,"right":567,"bottom":2712}},{"id":118,"pid":116,"index":1,"attr":{"id":"com.zhihu.android:id/collectView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":1,"depth":20,"left":679,"top":4313,"right":763,"bottom":2712}},{"id":119,"pid":116,"index":2,"attr":{"id":"com.zhihu.android:id/commentView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":2,"depth":20,"left":874,"top":4313,"right":958,"bottom":2712}},{"id":120,"pid":116,"index":3,"attr":{"id":"com.zhihu.android:id/followWithAvatarView","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":3,"depth":20,"left":1070,"top":4301,"right":1172,"bottom":2712}},{"id":121,"pid":120,"index":0,"attr":{"id":"com.zhihu.android:id/avatar_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":21,"left":1070,"top":4301,"right":1172,"bottom":2712}},{"id":122,"pid":120,"index":1,"attr":{"id":"com.zhihu.android:id/follow","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":1,"depth":21,"left":1070,"top":4367,"right":1172,"bottom":2712}},{"id":123,"pid":122,"index":0,"attr":{"id":"com.zhihu.android:id/anim","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":22,"left":1088,"top":4367,"right":1154,"bottom":2712}},{"id":124,"pid":123,"index":0,"attr":{"id":"com.zhihu.android:id/pag_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":23,"left":1088,"top":4367,"right":1154,"bottom":2712}},{"id":125,"pid":121,"index":0,"attr":{"id":"com.zhihu.android:id/avatar","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":1070,"top":4301,"right":1172,"bottom":2712}},{"id":126,"pid":119,"index":0,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":874,"top":4313,"right":958,"bottom":2712}},{"id":127,"pid":119,"index":1,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"3","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":922,"top":4313,"right":958,"bottom":2712}},{"id":128,"pid":118,"index":0,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":679,"top":4313,"right":763,"bottom":2712}},{"id":129,"pid":118,"index":1,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"1","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":727,"top":4313,"right":763,"bottom":2712}},{"id":130,"pid":117,"index":0,"attr":{"id":"com.zhihu.android:id/animation","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":21,"left":483,"top":4313,"right":567,"bottom":2712}},{"id":131,"pid":117,"index":1,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":483,"top":4313,"right":567,"bottom":2712}},{"id":132,"pid":117,"index":2,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"26","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":21,"left":531,"top":4313,"right":567,"bottom":2712}},{"id":133,"pid":130,"index":0,"attr":{"id":"com.zhihu.android:id/pag_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":483,"top":4313,"right":567,"bottom":2712}},{"id":134,"pid":114,"index":0,"attr":{"id":"com.zhihu.android:id/voteView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":19,"left":48,"top":4301,"right":387,"bottom":2712}},{"id":135,"pid":134,"index":0,"attr":{"id":"com.zhihu.android:id/root","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":0,"depth":20,"left":48,"top":4301,"right":387,"bottom":2712}},{"id":136,"pid":135,"index":0,"attr":{"id":"com.zhihu.android:id/vote_tv","name":"android.widget.TextView","text":" 303 ","textLen":5,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":21,"left":51,"top":4301,"right":226,"bottom":2712}},{"id":137,"pid":135,"index":1,"attr":{"id":"com.zhihu.android:id/divider","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":259,"top":4328,"right":262,"bottom":2712}},{"id":138,"pid":135,"index":2,"attr":{"id":"com.zhihu.android:id/down_tv","name":"android.widget.TextView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":21,"left":294,"top":4301,"right":384,"bottom":2712}},{"id":139,"pid":42,"index":0,"attr":{"id":"com.zhihu.android:id/end_info_view","name":"android.widget.TextView","text":"05-04・江苏・访问原页面 ‣","textLen":17,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":48,"top":4173,"right":1172,"bottom":2712}},{"id":140,"pid":41,"index":0,"attr":{"id":"com.zhihu.android:id/holder_text","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":4002,"right":1172,"bottom":2712}},{"id":141,"pid":140,"index":0,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":16,"left":48,"top":4002,"right":1172,"bottom":2712}},{"id":142,"pid":141,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"这片土地上,有个组织管一切,烂尾房能没有人管?","textLen":23,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":17,"left":48,"top":4002,"right":1172,"bottom":2712}},{"id":143,"pid":40,"index":0,"attr":{"id":"com.zhihu.android:id/author_info_view","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":3858,"right":1172,"bottom":2712}},{"id":144,"pid":143,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":6,"index":0,"depth":16,"left":48,"top":3858,"right":1172,"bottom":2712}},{"id":145,"pid":144,"index":0,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":17,"left":48,"top":3858,"right":156,"bottom":2712}},{"id":146,"pid":144,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"闻啼鸟","textLen":3,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":1,"depth":17,"left":180,"top":3858,"right":315,"bottom":2712}},{"id":147,"pid":144,"index":2,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":17,"left":327,"top":3858,"right":824,"bottom":2712}},{"id":148,"pid":144,"index":3,"attr":{"id":null,"name":"android.widget.TextView","text":"1","textLen":1,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":3,"depth":17,"left":180,"top":3921,"right":836,"bottom":2712}},{"id":149,"pid":144,"index":4,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":4,"depth":17,"left":854,"top":3867,"right":1070,"bottom":2712}},{"id":150,"pid":144,"index":5,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":5,"depth":17,"left":1100,"top":3876,"right":1172,"bottom":2712}},{"id":151,"pid":149,"index":0,"attr":{"id":"com.zhihu.android:id/btn_people_follow","name":"android.widget.TextView","text":"关注","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":18,"left":854,"top":3867,"right":1070,"bottom":2712}},{"id":152,"pid":147,"index":0,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":18,"left":327,"top":3858,"right":381,"bottom":2712}},{"id":153,"pid":39,"index":0,"attr":{"id":"com.zhihu.android:id/layout_space","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":0,"top":3810,"right":1220,"bottom":2712}},{"id":154,"pid":38,"index":0,"attr":{"id":"com.zhihu.android:id/layout_space","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":48,"top":3792,"right":1172,"bottom":2712}},{"id":155,"pid":37,"index":0,"attr":{"id":"com.zhihu.android:id/layout_space","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":0,"top":3744,"right":1220,"bottom":2712}},{"id":156,"pid":36,"index":0,"attr":{"id":"com.zhihu.android:id/layout_bottom_root","name":"android.widget.RelativeLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":0,"depth":15,"left":48,"top":3594,"right":1172,"bottom":2712}},{"id":157,"pid":156,"index":0,"attr":{"id":"com.zhihu.android:id/mask_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":16,"left":48,"top":3594,"right":1172,"bottom":2712}},{"id":158,"pid":156,"index":1,"attr":{"id":"com.zhihu.android:id/layout_expand","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":1,"depth":16,"left":938,"top":3594,"right":1172,"bottom":2712}},{"id":159,"pid":156,"index":2,"attr":{"id":"com.zhihu.android:id/combine_view","name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":16,"left":48,"top":3594,"right":1172,"bottom":2712}},{"id":160,"pid":159,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":17,"left":48,"top":3636,"right":1172,"bottom":2712}},{"id":161,"pid":160,"index":0,"attr":{"id":"com.zhihu.android:id/voteLayout","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":18,"left":48,"top":3636,"right":465,"bottom":2712}},{"id":162,"pid":160,"index":1,"attr":{"id":"com.zhihu.android:id/rightLayout","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":1,"depth":18,"left":483,"top":3636,"right":1172,"bottom":2712}},{"id":163,"pid":162,"index":0,"attr":{"id":"com.zhihu.android:id/drawOrderLinearLayout","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":4,"index":0,"depth":19,"left":483,"top":3636,"right":1172,"bottom":2712}},{"id":164,"pid":163,"index":0,"attr":{"id":"com.zhihu.android:id/likeView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":3,"index":0,"depth":20,"left":483,"top":3648,"right":567,"bottom":2712}},{"id":165,"pid":163,"index":1,"attr":{"id":"com.zhihu.android:id/collectView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":1,"depth":20,"left":679,"top":3648,"right":763,"bottom":2712}},{"id":166,"pid":163,"index":2,"attr":{"id":"com.zhihu.android:id/commentView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":2,"depth":20,"left":874,"top":3648,"right":958,"bottom":2712}},{"id":167,"pid":163,"index":3,"attr":{"id":"com.zhihu.android:id/followWithAvatarView","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":3,"depth":20,"left":1070,"top":3636,"right":1172,"bottom":2712}},{"id":168,"pid":167,"index":0,"attr":{"id":"com.zhihu.android:id/avatar_container","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":21,"left":1070,"top":3636,"right":1172,"bottom":2712}},{"id":169,"pid":167,"index":1,"attr":{"id":"com.zhihu.android:id/follow","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":1,"depth":21,"left":1070,"top":3702,"right":1172,"bottom":2712}},{"id":170,"pid":169,"index":0,"attr":{"id":"com.zhihu.android:id/anim","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":22,"left":1088,"top":3702,"right":1154,"bottom":2712}},{"id":171,"pid":170,"index":0,"attr":{"id":"com.zhihu.android:id/pag_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":23,"left":1088,"top":3702,"right":1154,"bottom":2712}},{"id":172,"pid":168,"index":0,"attr":{"id":"com.zhihu.android:id/avatar","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":1070,"top":3636,"right":1172,"bottom":2712}},{"id":173,"pid":166,"index":0,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":874,"top":3648,"right":958,"bottom":2712}},{"id":174,"pid":166,"index":1,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"9","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":922,"top":3648,"right":958,"bottom":2712}},{"id":175,"pid":165,"index":0,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":679,"top":3648,"right":763,"bottom":2712}},{"id":176,"pid":164,"index":0,"attr":{"id":"com.zhihu.android:id/animation","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":21,"left":483,"top":3648,"right":567,"bottom":2712}},{"id":177,"pid":164,"index":1,"attr":{"id":"com.zhihu.android:id/statusImg","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":483,"top":3648,"right":567,"bottom":2712}},{"id":178,"pid":164,"index":2,"attr":{"id":"com.zhihu.android:id/statusTv","name":"android.widget.TextView","text":"8","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":21,"left":531,"top":3648,"right":567,"bottom":2712}},{"id":179,"pid":176,"index":0,"attr":{"id":"com.zhihu.android:id/pag_view","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":483,"top":3648,"right":567,"bottom":2712}},{"id":180,"pid":161,"index":0,"attr":{"id":"com.zhihu.android:id/voteView","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":19,"left":48,"top":3636,"right":387,"bottom":2712}},{"id":181,"pid":180,"index":0,"attr":{"id":"com.zhihu.android:id/root","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":0,"depth":20,"left":48,"top":3636,"right":387,"bottom":2712}},{"id":182,"pid":181,"index":0,"attr":{"id":"com.zhihu.android:id/vote_tv","name":"android.widget.TextView","text":" 15 ","textLen":4,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":21,"left":58,"top":3636,"right":205,"bottom":2712}},{"id":183,"pid":181,"index":1,"attr":{"id":"com.zhihu.android:id/divider","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":245,"top":3663,"right":248,"bottom":2712}},{"id":184,"pid":181,"index":2,"attr":{"id":"com.zhihu.android:id/down_tv","name":"android.widget.TextView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":21,"left":287,"top":3636,"right":377,"bottom":2712}},{"id":185,"pid":158,"index":0,"attr":{"id":"com.zhihu.android:id/tv_expand","name":"android.widget.TextView","text":"展开","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":17,"left":992,"top":3594,"right":1082,"bottom":2712}},{"id":186,"pid":158,"index":1,"attr":{"id":"com.zhihu.android:id/iv_expand","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":17,"left":1082,"top":3594,"right":1142,"bottom":2712}},{"id":187,"pid":35,"index":0,"attr":{"id":"com.zhihu.android:id/holder_image","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":2904,"right":1172,"bottom":2712}},{"id":188,"pid":187,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":16,"left":48,"top":2904,"right":1172,"bottom":2712}},{"id":189,"pid":188,"index":0,"attr":{"id":"com.zhihu.android:id/image_normal","name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":17,"left":48,"top":2904,"right":1172,"bottom":2712}},{"id":190,"pid":188,"index":1,"attr":{"id":"com.zhihu.android:id/image_custom","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":17,"left":1172,"top":5401,"right":1172,"bottom":2712}},{"id":191,"pid":34,"index":0,"attr":{"id":"com.zhihu.android:id/author_info_view","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":48,"top":2760,"right":1172,"bottom":2712}},{"id":192,"pid":191,"index":0,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":6,"index":0,"depth":16,"left":48,"top":2760,"right":1172,"bottom":2712}},{"id":193,"pid":192,"index":0,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":17,"left":48,"top":2760,"right":156,"bottom":2712}},{"id":194,"pid":192,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"人生苍茫多少泪","textLen":7,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":1,"depth":17,"left":180,"top":2760,"right":495,"bottom":2712}},{"id":195,"pid":192,"index":2,"attr":{"id":null,"name":"android.widget.LinearLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":17,"left":507,"top":2760,"right":824,"bottom":2712}},{"id":196,"pid":192,"index":3,"attr":{"id":null,"name":"android.widget.TextView","text":"单身不婚\n吾心吾行澄如明镜,所作所为皆为正义","textLen":22,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":3,"depth":17,"left":180,"top":2823,"right":836,"bottom":2712}},{"id":197,"pid":192,"index":4,"attr":{"id":null,"name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":4,"depth":17,"left":854,"top":2769,"right":1070,"bottom":2712}},{"id":198,"pid":192,"index":5,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":5,"depth":17,"left":1100,"top":2778,"right":1172,"bottom":2712}},{"id":199,"pid":197,"index":0,"attr":{"id":"com.zhihu.android:id/btn_people_follow","name":"android.widget.TextView","text":"关注","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":18,"left":854,"top":2769,"right":1070,"bottom":2712}},{"id":200,"pid":195,"index":0,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":18,"left":507,"top":2760,"right":561,"bottom":2712}},{"id":201,"pid":33,"index":0,"attr":{"id":"com.zhihu.android:id/layout_space","name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":15,"left":0,"top":2712,"right":1220,"bottom":2712}},{"id":202,"pid":27,"index":0,"attr":{"id":"com.zhihu.android:id/root_view","name":"android.view.ViewGroup","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":11,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":203,"pid":202,"index":0,"attr":{"id":"com.zhihu.android:id/view_content","name":"android.widget.FrameLayout","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":12,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":204,"pid":203,"index":0,"attr":{"id":null,"name":"android.webkit.WebView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":13,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":205,"pid":204,"index":0,"attr":{"id":null,"name":"android.webkit.WebView","text":"知乎 - 知乎","textLen":7,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":14,"left":0,"top":0,"right":1221,"bottom":2712}},{"id":206,"pid":205,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":15,"left":0,"top":0,"right":1221,"bottom":2712}},{"id":207,"pid":206,"index":0,"attr":{"id":"root","name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":15,"left":0,"top":0,"right":1221,"bottom":2712}},{"id":208,"pid":207,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":16,"left":0,"top":0,"right":1221,"bottom":2712}},{"id":209,"pid":208,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":29,"index":0,"depth":17,"left":0,"top":0,"right":1221,"bottom":2712}},{"id":210,"pid":209,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":18,"left":0,"top":0,"right":1221,"bottom":-1137}},{"id":211,"pid":209,"index":1,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":1,"depth":18,"left":0,"top":0,"right":1221,"bottom":-879}},{"id":212,"pid":209,"index":2,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":2,"depth":18,"left":0,"top":0,"right":1221,"bottom":-687}},{"id":213,"pid":209,"index":3,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":3,"depth":18,"left":0,"top":0,"right":1221,"bottom":600}},{"id":214,"pid":209,"index":4,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":3,"index":4,"depth":18,"left":141,"top":657,"right":327,"bottom":753}},{"id":215,"pid":209,"index":5,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":3,"index":5,"depth":18,"left":366,"top":657,"right":582,"bottom":753}},{"id":216,"pid":209,"index":6,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":3,"index":6,"depth":18,"left":621,"top":657,"right":795,"bottom":753}},{"id":217,"pid":209,"index":7,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":3,"index":7,"depth":18,"left":834,"top":657,"right":1080,"bottom":753}},{"id":218,"pid":209,"index":8,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":8,"index":8,"depth":18,"left":48,"top":810,"right":1173,"bottom":1908}},{"id":219,"pid":209,"index":9,"attr":{"id":null,"name":"android.widget.TextView","text":"评论 19","textLen":5,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":9,"depth":18,"left":48,"top":1977,"right":1173,"bottom":2055}},{"id":220,"pid":209,"index":10,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":10,"depth":18,"left":48,"top":2109,"right":138,"bottom":2202}},{"id":221,"pid":209,"index":11,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":11,"depth":18,"left":168,"top":2100,"right":1173,"bottom":2211}},{"id":222,"pid":209,"index":12,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":12,"depth":18,"left":48,"top":2256,"right":138,"bottom":2349}},{"id":223,"pid":209,"index":13,"attr":{"id":null,"name":"android.widget.TextView","text":"杨驴","textLen":2,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":13,"depth":18,"left":168,"top":2256,"right":255,"bottom":2307}},{"id":224,"pid":209,"index":14,"attr":{"id":null,"name":"android.widget.Button","text":"乐于交流","textLen":4,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":14,"depth":18,"left":270,"top":2259,"right":315,"bottom":2304}},{"id":225,"pid":209,"index":15,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":15,"depth":18,"left":168,"top":2322,"right":1173,"bottom":2388}},{"id":226,"pid":209,"index":16,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":16,"depth":18,"left":168,"top":2403,"right":891,"bottom":2466}},{"id":227,"pid":209,"index":17,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":17,"depth":18,"left":948,"top":2403,"right":1011,"bottom":2466}},{"id":228,"pid":209,"index":18,"attr":{"id":null,"name":"android.widget.Button","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":18,"depth":18,"left":1056,"top":2403,"right":1119,"bottom":2466}},{"id":229,"pid":209,"index":19,"attr":{"id":null,"name":"android.widget.TextView","text":"36","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":19,"depth":18,"left":1128,"top":2409,"right":1173,"bottom":2460}},{"id":230,"pid":209,"index":20,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":20,"depth":18,"left":48,"top":2511,"right":138,"bottom":2604}},{"id":231,"pid":209,"index":21,"attr":{"id":null,"name":"android.widget.TextView","text":"老木木","textLen":3,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":21,"depth":18,"left":168,"top":2511,"right":297,"bottom":2562}},{"id":232,"pid":209,"index":22,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":22,"depth":18,"left":168,"top":2577,"right":1173,"bottom":2643}},{"id":233,"pid":209,"index":23,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":23,"depth":18,"left":168,"top":2658,"right":897,"bottom":2712}},{"id":234,"pid":209,"index":24,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":24,"depth":18,"left":954,"top":2658,"right":1017,"bottom":2712}},{"id":235,"pid":209,"index":25,"attr":{"id":null,"name":"android.widget.Button","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":25,"depth":18,"left":1062,"top":2658,"right":1125,"bottom":2712}},{"id":236,"pid":209,"index":26,"attr":{"id":null,"name":"android.widget.TextView","text":"15","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":26,"depth":18,"left":1134,"top":2664,"right":1173,"bottom":2712}},{"id":237,"pid":209,"index":27,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":27,"depth":18,"left":48,"top":2778,"right":1173,"bottom":2712}},{"id":238,"pid":209,"index":28,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":5,"index":28,"depth":18,"left":0,"top":2568,"right":1221,"bottom":2712}},{"id":239,"pid":238,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":19,"left":48,"top":2586,"right":375,"bottom":2697}},{"id":240,"pid":238,"index":1,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":1,"depth":19,"left":618,"top":2580,"right":762,"bottom":2703}},{"id":241,"pid":238,"index":2,"attr":{"id":null,"name":"android.widget.Button","text":"收藏,总收藏数4 ","textLen":9,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":19,"left":762,"top":2580,"right":906,"bottom":2703}},{"id":242,"pid":238,"index":3,"attr":{"id":null,"name":"android.widget.Button","text":"评论,总评论数19 ","textLen":10,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":3,"depth":19,"left":906,"top":2580,"right":1050,"bottom":2703}},{"id":243,"pid":238,"index":4,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":4,"depth":19,"left":1050,"top":2580,"right":1194,"bottom":2703}},{"id":244,"pid":243,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":20,"left":1056,"top":2655,"right":1194,"bottom":2706}},{"id":245,"pid":244,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":21,"left":1092,"top":2655,"right":1158,"bottom":2700}},{"id":246,"pid":245,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":22,"left":1107,"top":2658,"right":1143,"bottom":2694}},{"id":247,"pid":245,"index":1,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":1,"depth":22,"left":1113,"top":2664,"right":1137,"bottom":2691}},{"id":248,"pid":247,"index":0,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":23,"left":1113,"top":2664,"right":1137,"bottom":2691}},{"id":249,"pid":246,"index":0,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":23,"left":1107,"top":2658,"right":1143,"bottom":2694}},{"id":250,"pid":240,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":20,"left":648,"top":2598,"right":732,"bottom":2685}},{"id":251,"pid":240,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"5","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":20,"left":708,"top":2583,"right":726,"bottom":2622}},{"id":252,"pid":250,"index":0,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":648,"top":2598,"right":732,"bottom":2685}},{"id":253,"pid":239,"index":0,"attr":{"id":null,"name":"android.widget.Button","text":"赞同,总赞同数114 ","textLen":11,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":20,"left":48,"top":2586,"right":270,"bottom":2697}},{"id":254,"pid":239,"index":1,"attr":{"id":null,"name":"android.widget.Button","text":"反对","textLen":2,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":1,"depth":20,"left":273,"top":2586,"right":375,"bottom":2697}},{"id":255,"pid":237,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"查看全部评论 ","descLen":8,"isClickable":true,"childCount":3,"index":0,"depth":19,"left":459,"top":2778,"right":762,"bottom":2712}},{"id":256,"pid":255,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"查看全部评论","textLen":6,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":459,"top":2781,"right":696,"bottom":2712}},{"id":257,"pid":255,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":20,"left":693,"top":2781,"right":693,"bottom":2712}},{"id":258,"pid":255,"index":2,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":20,"left":705,"top":2778,"right":762,"bottom":2712}},{"id":259,"pid":234,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"","descLen":1,"isClickable":true,"childCount":2,"index":0,"depth":19,"left":954,"top":2658,"right":1017,"bottom":2712}},{"id":260,"pid":259,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":954,"top":2664,"right":954,"bottom":2712}},{"id":261,"pid":259,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":20,"left":954,"top":2658,"right":1017,"bottom":2712}},{"id":262,"pid":233,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"20 小时前","textLen":6,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":168,"top":2670,"right":330,"bottom":2712}},{"id":263,"pid":233,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":" · ","textLen":3,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":19,"left":327,"top":2670,"right":369,"bottom":2712}},{"id":264,"pid":233,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"IP 属地江苏","textLen":7,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":19,"left":378,"top":2670,"right":567,"bottom":2712}},{"id":265,"pid":232,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"蒸馍","descLen":2,"isClickable":true,"childCount":1,"index":0,"depth":19,"left":168,"top":2577,"right":1173,"bottom":2643}},{"id":266,"pid":265,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"蒸馍","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":168,"top":2580,"right":261,"bottom":2640}},{"id":267,"pid":230,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"v2-0db7ac91b9ed4df5de6b0756e297c085_l","descLen":37,"isClickable":true,"childCount":1,"index":0,"depth":19,"left":48,"top":2511,"right":138,"bottom":2619}},{"id":268,"pid":267,"index":0,"attr":{"id":null,"name":"android.widget.Image","text":"v2-0db7ac91b9ed4df5de6b0756e297c085_l","textLen":37,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":48,"top":2511,"right":138,"bottom":2604}},{"id":269,"pid":227,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"","descLen":1,"isClickable":true,"childCount":2,"index":0,"depth":19,"left":948,"top":2403,"right":1011,"bottom":2466}},{"id":270,"pid":269,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":948,"top":2409,"right":948,"bottom":2460}},{"id":271,"pid":269,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":20,"left":948,"top":2403,"right":1011,"bottom":2466}},{"id":272,"pid":226,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"4 小时前","textLen":5,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":168,"top":2415,"right":309,"bottom":2466}},{"id":273,"pid":226,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":" · ","textLen":3,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":19,"left":306,"top":2415,"right":348,"bottom":2466}},{"id":274,"pid":226,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"IP 属地江苏","textLen":7,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":19,"left":357,"top":2415,"right":546,"bottom":2466}},{"id":275,"pid":225,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"“您姓福么”“我姓曾”“曾某,你不福么”","descLen":20,"isClickable":true,"childCount":3,"index":0,"depth":19,"left":168,"top":2322,"right":1173,"bottom":2388}},{"id":276,"pid":275,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"“您姓福么”","textLen":6,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":168,"top":2325,"right":384,"bottom":2385}},{"id":277,"pid":275,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"“我姓曾”","textLen":5,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":20,"left":381,"top":2325,"right":555,"bottom":2385}},{"id":278,"pid":275,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"“曾某,你不福么”","textLen":9,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":20,"left":552,"top":2325,"right":903,"bottom":2385}},{"id":279,"pid":222,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"v2-abed1a8c04700ba7d72b45195223e0ff_l","descLen":37,"isClickable":true,"childCount":1,"index":0,"depth":19,"left":48,"top":2256,"right":138,"bottom":2364}},{"id":280,"pid":279,"index":0,"attr":{"id":null,"name":"android.widget.Image","text":"v2-abed1a8c04700ba7d72b45195223e0ff_l","textLen":37,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":48,"top":2256,"right":138,"bottom":2349}},{"id":281,"pid":221,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"写评论 icon icon icon","descLen":18,"isClickable":true,"childCount":4,"index":0,"depth":19,"left":216,"top":2100,"right":1161,"bottom":2211}},{"id":282,"pid":281,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"写评论","textLen":3,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":216,"top":2127,"right":345,"bottom":2184}},{"id":283,"pid":281,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"icon","textLen":4,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":20,"left":933,"top":2124,"right":993,"bottom":2187}},{"id":284,"pid":281,"index":2,"attr":{"id":null,"name":"android.widget.Image","text":"icon","textLen":4,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":20,"left":1005,"top":2124,"right":1065,"bottom":2187}},{"id":285,"pid":281,"index":3,"attr":{"id":null,"name":"android.widget.Image","text":"icon","textLen":4,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":3,"depth":20,"left":1077,"top":2124,"right":1137,"bottom":2187}},{"id":286,"pid":220,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":"v2-703154510fb1cf6120684b762932940a_l","descLen":37,"isClickable":true,"childCount":1,"index":0,"depth":19,"left":48,"top":2109,"right":138,"bottom":2217}},{"id":287,"pid":286,"index":0,"attr":{"id":null,"name":"android.widget.Image","text":"v2-703154510fb1cf6120684b762932940a_l","textLen":37,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":48,"top":2109,"right":138,"bottom":2202}},{"id":288,"pid":218,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"相关搜索","textLen":4,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":48,"top":810,"right":1173,"bottom":891}},{"id":289,"pid":218,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"烂尾楼为什么没人管","textLen":9,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":1,"depth":19,"left":48,"top":924,"right":600,"bottom":1038}},{"id":290,"pid":218,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"汉奸团建","textLen":4,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":2,"depth":19,"left":621,"top":924,"right":1173,"bottom":1038}},{"id":291,"pid":218,"index":3,"attr":{"id":null,"name":"android.widget.TextView","text":"我也是十四分之一","textLen":8,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":3,"depth":19,"left":48,"top":1059,"right":600,"bottom":1173}},{"id":292,"pid":218,"index":4,"attr":{"id":null,"name":"android.widget.TextView","text":"房管局团购房烂尾","textLen":8,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":4,"depth":19,"left":621,"top":1059,"right":1173,"bottom":1173}},{"id":293,"pid":218,"index":5,"attr":{"id":null,"name":"android.widget.TextView","text":"法院团购房烂尾","textLen":7,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":5,"depth":19,"left":48,"top":1194,"right":600,"bottom":1308}},{"id":294,"pid":218,"index":6,"attr":{"id":null,"name":"android.widget.TextView","text":"烂尾楼是怎么回事","textLen":8,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":6,"depth":19,"left":621,"top":1194,"right":1173,"bottom":1308}},{"id":295,"pid":218,"index":7,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":7,"depth":19,"left":48,"top":1377,"right":1173,"bottom":1908}},{"id":296,"pid":295,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":4,"index":0,"depth":20,"left":48,"top":1425,"right":1173,"bottom":1860}},{"id":297,"pid":296,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"2023年百元档、千元档最全电动剃须刀深度测评:电动剃须刀的水究竟有多深?如何才能避开买电动剃须刀的那些坑?看这一篇就够了!","textLen":62,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":84,"top":1461,"right":1137,"bottom":1590}},{"id":298,"pid":296,"index":1,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":1,"depth":21,"left":84,"top":1608,"right":1137,"bottom":1737}},{"id":299,"pid":296,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"住乌托邦的猪","textLen":6,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":21,"left":84,"top":1767,"right":339,"bottom":1827}},{"id":300,"pid":296,"index":3,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":3,"depth":21,"left":1095,"top":1773,"right":1125,"bottom":1824}},{"id":301,"pid":300,"index":0,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":1095,"top":1785,"right":1125,"bottom":1818}},{"id":302,"pid":298,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"作为剃须刀10多年的骨灰级用户,我用过的电动剃须刀不下十几款,如果你想买电动剃须刀,看这篇就够了! 电动剃须刀真的...","textLen":60,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":22,"left":84,"top":1608,"right":1137,"bottom":1737}},{"id":303,"pid":217,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":858,"top":672,"right":858,"bottom":738}},{"id":304,"pid":217,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":19,"left":858,"top":675,"right":921,"bottom":738}},{"id":305,"pid":217,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"图片分享","textLen":4,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":19,"left":927,"top":678,"right":1050,"bottom":735}},{"id":306,"pid":216,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":645,"top":672,"right":645,"bottom":738}},{"id":307,"pid":216,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":19,"left":645,"top":675,"right":708,"bottom":738}},{"id":308,"pid":216,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"QQ","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":19,"left":714,"top":678,"right":765,"bottom":735}},{"id":309,"pid":215,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":390,"top":672,"right":390,"bottom":738}},{"id":310,"pid":215,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":19,"left":390,"top":675,"right":453,"bottom":738}},{"id":311,"pid":215,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"朋友圈","textLen":3,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":19,"left":459,"top":678,"right":552,"bottom":735}},{"id":312,"pid":214,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":165,"top":672,"right":165,"bottom":738}},{"id":313,"pid":214,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":19,"left":165,"top":675,"right":228,"bottom":738}},{"id":314,"pid":214,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"微信","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":19,"left":234,"top":678,"right":297,"bottom":735}},{"id":315,"pid":213,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":19,"left":48,"top":0,"right":1173,"bottom":600}},{"id":316,"pid":315,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":20,"left":48,"top":0,"right":1173,"bottom":510}},{"id":317,"pid":315,"index":1,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":1,"depth":20,"left":48,"top":531,"right":939,"bottom":600}},{"id":318,"pid":317,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"编辑于 2023-06-13 09:09","textLen":20,"desc":null,"descLen":null,"isClickable":true,"childCount":0,"index":0,"depth":21,"left":48,"top":537,"right":504,"bottom":594}},{"id":319,"pid":317,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"・IP 属地广东","textLen":8,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":21,"left":501,"top":537,"right":744,"bottom":594}},{"id":320,"pid":317,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"・禁止转载","textLen":5,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":21,"left":741,"top":537,"right":939,"bottom":594}},{"id":321,"pid":316,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":7,"index":0,"depth":21,"left":48,"top":0,"right":1173,"bottom":510}},{"id":322,"pid":321,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"怎么没人管,我隔壁工位新来的同事,就遇上了。","textLen":22,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":48,"top":0,"right":1173,"bottom":-585}},{"id":323,"pid":321,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"她属于那种不认命的,会创群闹事的,来公司报道的第二天,我就听到有警察给她打电话,意思就是要抓她,把她说哭了啥的。。。","textLen":58,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":48,"top":0,"right":1173,"bottom":-288}},{"id":324,"pid":321,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"今天又听到她和他老公聊,说警察打过好几次电话给她,搞得她有点心力交瘁。","textLen":35,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":48,"top":0,"right":1173,"bottom":-66}},{"id":325,"pid":321,"index":3,"attr":{"id":null,"name":"android.widget.TextView","text":"两个孩子,房子收不到,还背着贷款,真恐怖。","textLen":21,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":48,"top":0,"right":1173,"bottom":78}},{"id":326,"pid":321,"index":4,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":3,"index":4,"depth":22,"left":48,"top":141,"right":1173,"bottom":222}},{"id":327,"pid":321,"index":5,"attr":{"id":null,"name":"android.widget.TextView","text":"\n","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":48,"top":246,"right":1173,"bottom":327}},{"id":328,"pid":321,"index":6,"attr":{"id":null,"name":"android.widget.TextView","text":"不是吧,你们说了啥,好多评论都被删了。我刚刚看到一个评论也没说,现在翻开了就被删除了。","textLen":43,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":21,"left":48,"top":354,"right":1173,"bottom":510}},{"id":329,"pid":326,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"管的不是","textLen":4,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":23,"left":48,"top":147,"right":243,"bottom":216}},{"id":330,"pid":326,"index":1,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":1,"depth":23,"left":240,"top":147,"right":417,"bottom":216}},{"id":331,"pid":326,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":",是闹事的人咯","textLen":7,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":2,"depth":23,"left":414,"top":147,"right":753,"bottom":216}},{"id":332,"pid":330,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":0,"depth":24,"left":240,"top":147,"right":417,"bottom":216}},{"id":333,"pid":332,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"烂尾楼","textLen":3,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":25,"left":240,"top":147,"right":387,"bottom":216}},{"id":334,"pid":332,"index":1,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":25,"left":384,"top":153,"right":417,"bottom":186}},{"id":335,"pid":212,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":2,"index":0,"depth":19,"left":48,"top":0,"right":1149,"bottom":-711}},{"id":336,"pid":335,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":1,"index":0,"depth":19,"left":48,"top":0,"right":156,"bottom":-711}},{"id":337,"pid":335,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"匿名用户","textLen":4,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":19,"left":186,"top":0,"right":369,"bottom":-735}},{"id":338,"pid":336,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":48,"top":0,"right":156,"bottom":-711}},{"id":339,"pid":211,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":2,"index":0,"depth":19,"left":0,"top":0,"right":1221,"bottom":-915}},{"id":340,"pid":339,"index":0,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":1,"index":0,"depth":20,"left":48,"top":0,"right":1173,"bottom":-1011}},{"id":341,"pid":339,"index":1,"attr":{"id":null,"name":"android.view.View","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":true,"childCount":6,"index":1,"depth":20,"left":48,"top":0,"right":1173,"bottom":-939}},{"id":342,"pid":341,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"知乎","textLen":2,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":48,"top":0,"right":129,"bottom":-936}},{"id":343,"pid":341,"index":1,"attr":{"id":null,"name":"android.widget.TextView","text":"·","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":132,"top":0,"right":153,"bottom":-936}},{"id":344,"pid":341,"index":2,"attr":{"id":null,"name":"android.widget.TextView","text":"1,548 个回答","textLen":9,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":156,"top":0,"right":387,"bottom":-936}},{"id":345,"pid":341,"index":3,"attr":{"id":null,"name":"android.widget.TextView","text":"·","textLen":1,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":390,"top":0,"right":414,"bottom":-936}},{"id":346,"pid":341,"index":4,"attr":{"id":null,"name":"android.widget.TextView","text":"3288 关注","textLen":7,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":417,"top":0,"right":603,"bottom":-936}},{"id":347,"pid":341,"index":5,"attr":{"id":null,"name":"android.widget.Image","text":"","textLen":0,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":600,"top":0,"right":651,"bottom":-939}},{"id":348,"pid":340,"index":0,"attr":{"id":null,"name":"android.widget.TextView","text":"烂尾楼真的就没人管吗?","textLen":11,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":20,"left":48,"top":0,"right":1173,"bottom":-1011}},{"id":349,"pid":8,"index":0,"attr":{"id":null,"name":"android.view.View","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":0,"depth":7,"left":0,"top":0,"right":1220,"bottom":2712}},{"id":350,"pid":8,"index":1,"attr":{"id":null,"name":"android.widget.ImageView","text":null,"textLen":null,"desc":null,"descLen":null,"isClickable":false,"childCount":0,"index":1,"depth":7,"left":0,"top":0,"right":1220,"bottom":2712}}]}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8f5956f..fa56206 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,12 +1,17 @@
+import com.android.build.gradle.internal.cxx.json.jsonStringOf
+import java.text.SimpleDateFormat
+import java.util.Locale
+
plugins {
id("com.android.application")
- id("kotlin-android")
id("kotlin-parcelize")
- id("kotlin-kapt")
- id("org.jetbrains.kotlin.plugin.serialization")
- id("org.jetbrains.kotlin.android")
+ kotlin("android")
+ kotlin("plugin.serialization")
+ id("com.google.devtools.ksp")
+ id("dev.rikka.tools.refine")
}
+
@Suppress("UnstableApiUsage")
android {
namespace = "li.songe.gkd"
@@ -26,12 +31,17 @@ android {
useSupportLibrary = true
}
- kapt {
- arguments {
-// room 依赖每次构建的产物来执行自动迁移
- arg("room.schemaLocation", "$projectDir/schemas")
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += mapOf(
+ "room.schemaLocation" to "$projectDir/schemas",
+ "room.incremental" to "true"
+ )
}
}
+ val nowTime = System.currentTimeMillis()
+ buildConfigField("Long", "BUILD_TIME", jsonStringOf(nowTime) + "L")
+ buildConfigField("String", "BUILD_DATE", jsonStringOf(SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZ", Locale.SIMPLIFIED_CHINESE).format(nowTime)))
}
lint {
@@ -47,18 +57,8 @@ android {
}
}
- kotlin {
- sourceSets.debug {
- kotlin.srcDir("build/generated/ksp/debug/kotlin")
- }
- sourceSets.release {
- kotlin.srcDir("build/generated/ksp/release/kotlin")
- }
- }
-
buildTypes {
release {
- manifestPlaceholders += mapOf()
isMinifyEnabled = false
setProguardFiles(
listOf(
@@ -67,31 +67,31 @@ android {
)
)
signingConfig = signingConfigs.getByName("release")
- manifestPlaceholders["appName"] = "搞快点"
+ manifestPlaceholders["appName"] = "GKD"
}
debug {
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("release")
- manifestPlaceholders["appName"] = "搞快点-dev"
+ manifestPlaceholders["appName"] = "GKD-debug"
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
- freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
+ jvmTarget = JavaVersion.VERSION_17.majorVersion
+ freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
buildFeatures {
buildConfig = true
compose = true
}
composeOptions {
- kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
+ kotlinCompilerExtensionVersion = libs.versions.compose.compilerVersion.get()
}
- packagingOptions {
+ packaging {
resources {
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
excludes += "META-INF/INDEX.LIST"
@@ -106,16 +106,20 @@ android {
exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-debug")
}
}
+
+// ksp
+ sourceSets.configureEach {
+ kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin/")
+ }
}
-dependencies {
- implementation(project(mapOf("path" to ":selector_core")))
- implementation(project(mapOf("path" to ":router")))
+dependencies {
+
+ implementation(project(mapOf("path" to ":selector")))
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
- implementation(libs.androidx.localbroadcastmanager)
implementation(libs.compose.ui)
implementation(libs.compose.material)
@@ -128,15 +132,18 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso)
+
+ compileOnly(project(mapOf("path" to ":hidden_api")))
implementation(libs.rikka.shizuku.api)
implementation(libs.rikka.shizuku.provider)
+ implementation(libs.lsposed.hiddenapibypass)
implementation(libs.tencent.bugly)
implementation(libs.tencent.mmkv)
implementation(libs.androidx.room.runtime)
- kapt(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
+ ksp(libs.androidx.room.compiler)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
@@ -144,12 +151,13 @@ dependencies {
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.client.core)
- implementation(libs.ktor.client.cio)
+ implementation(libs.ktor.client.android)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.google.accompanist.drawablepainter)
implementation(libs.google.accompanist.placeholder.material)
+ implementation(libs.google.accompanist.systemuicontroller)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.collections.immutable)
@@ -160,4 +168,9 @@ dependencies {
implementation(libs.others.zxing.android.embedded)
implementation(libs.others.floating.bubble.view)
+ implementation(libs.destinations.core)
+ implementation(libs.destinations.animations)
+ ksp(libs.destinations.ksp)
+
+
}
\ No newline at end of file
diff --git a/app/schemas/li.songe.gkd.db.AppDatabase/1.json b/app/schemas/li.songe.gkd.db.AppDatabase/1.json
index 8fc2672..003d601 100644
--- a/app/schemas/li.songe.gkd.db.AppDatabase/1.json
+++ b/app/schemas/li.songe.gkd.db.AppDatabase/1.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
- "identityHash": "2083d8585fffd897fde3733958e356f8",
+ "identityHash": "f3feda76127233f3416d7570fca1615f",
"entities": [
{
"tableName": "subs_item",
@@ -149,12 +149,116 @@
},
"indices": [],
"foreignKeys": []
+ },
+ {
+ "tableName": "snapshot",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `device` TEXT NOT NULL, `model` TEXT NOT NULL, `manufacturer` TEXT NOT NULL, `brand` TEXT NOT NULL, `sdk_int` INTEGER NOT NULL, `release` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "appId",
+ "columnName": "app_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activityId",
+ "columnName": "activity_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "appName",
+ "columnName": "app_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "appVersionCode",
+ "columnName": "app_version_code",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "appVersionName",
+ "columnName": "app_version_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "screenHeight",
+ "columnName": "screen_height",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "screenWidth",
+ "columnName": "screen_width",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLandscape",
+ "columnName": "is_landscape",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "device",
+ "columnName": "device",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "model",
+ "columnName": "model",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manufacturer",
+ "columnName": "manufacturer",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "brand",
+ "columnName": "brand",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sdkInt",
+ "columnName": "sdk_int",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "release",
+ "columnName": "release",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2083d8585fffd897fde3733958e356f8')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f3feda76127233f3416d7570fca1615f')"
]
}
}
\ No newline at end of file
diff --git a/app/schemas/li.songe.gkd.db.SnapshotDb/1.json b/app/schemas/li.songe.gkd.db.SnapshotDb/1.json
new file mode 100644
index 0000000..71d0906
--- /dev/null
+++ b/app/schemas/li.songe.gkd.db.SnapshotDb/1.json
@@ -0,0 +1,124 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "296a7b78252c48246f24767e66441c22",
+ "entities": [
+ {
+ "tableName": "snapshot",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `device` TEXT NOT NULL, `model` TEXT NOT NULL, `manufacturer` TEXT NOT NULL, `brand` TEXT NOT NULL, `sdk_int` INTEGER NOT NULL, `release` TEXT NOT NULL, `_1` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "appId",
+ "columnName": "app_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activityId",
+ "columnName": "activity_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "appName",
+ "columnName": "app_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "appVersionCode",
+ "columnName": "app_version_code",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "appVersionName",
+ "columnName": "app_version_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "screenHeight",
+ "columnName": "screen_height",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "screenWidth",
+ "columnName": "screen_width",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLandscape",
+ "columnName": "is_landscape",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "device",
+ "columnName": "device",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "model",
+ "columnName": "model",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manufacturer",
+ "columnName": "manufacturer",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "brand",
+ "columnName": "brand",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sdkInt",
+ "columnName": "sdk_int",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "release",
+ "columnName": "release",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodes",
+ "columnName": "_1",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '296a7b78252c48246f24767e66441c22')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/li.songe.gkd.db.SubsConfigDb/1.json b/app/schemas/li.songe.gkd.db.SubsConfigDb/1.json
new file mode 100644
index 0000000..288b7ed
--- /dev/null
+++ b/app/schemas/li.songe.gkd.db.SubsConfigDb/1.json
@@ -0,0 +1,70 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "5ad1f90d8f2852410fde46463bf24322",
+ "entities": [
+ {
+ "tableName": "subs_config",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mtime",
+ "columnName": "mtime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enable",
+ "columnName": "enable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subsItemId",
+ "columnName": "subs_item_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "appId",
+ "columnName": "app_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupKey",
+ "columnName": "group_key",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ad1f90d8f2852410fde46463bf24322')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/li.songe.gkd.db.SubsItemDb/1.json b/app/schemas/li.songe.gkd.db.SubsItemDb/1.json
new file mode 100644
index 0000000..bf4e812
--- /dev/null
+++ b/app/schemas/li.songe.gkd.db.SubsItemDb/1.json
@@ -0,0 +1,88 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "b51332e64931ac0cef5774cb5df5b703",
+ "entities": [
+ {
+ "tableName": "subs_item",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `version` INTEGER NOT NULL, `update_url` TEXT NOT NULL, `support_url` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mtime",
+ "columnName": "mtime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enable",
+ "columnName": "enable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enableUpdate",
+ "columnName": "enable_update",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updateUrl",
+ "columnName": "update_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "supportUrl",
+ "columnName": "support_url",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b51332e64931ac0cef5774cb5df5b703')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/li.songe.gkd.db.TriggerLogDb/1.json b/app/schemas/li.songe.gkd.db.TriggerLogDb/1.json
new file mode 100644
index 0000000..fafbd90
--- /dev/null
+++ b/app/schemas/li.songe.gkd.db.TriggerLogDb/1.json
@@ -0,0 +1,34 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "e565cbca157f8ba6cecb6e7cd7cc6304",
+ "entities": [
+ {
+ "tableName": "trigger_log",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e565cbca157f8ba6cecb6e7cd7cc6304')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 04cde12..ec63e29 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -17,16 +17,21 @@
-
+
+ android:theme="@style/AppTheme">
+ android:exported="true">
+ android:exported="false"
+ android:process=":remote" />
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/App.kt b/app/src/main/java/li/songe/gkd/App.kt
index bf146f1..ae64c8b 100644
--- a/app/src/main/java/li/songe/gkd/App.kt
+++ b/app/src/main/java/li/songe/gkd/App.kt
@@ -1,22 +1,35 @@
package li.songe.gkd
import android.app.Application
+import android.content.Context
+import android.os.Build
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
import com.blankj.utilcode.util.LogUtils
import com.tencent.bugly.crashreport.CrashReport
import com.tencent.mmkv.MMKV
-import li.songe.gkd.util.Storage
+import li.songe.gkd.utils.Storage
+import org.lsposed.hiddenapibypass.HiddenApiBypass
+import rikka.shizuku.ShizukuProvider
class App : Application() {
companion object {
lateinit var context: Application
}
+ override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ HiddenApiBypass.addHiddenApiExemptions("L")
+ }
+ }
+
override fun onCreate() {
super.onCreate()
context = this
MMKV.initialize(this)
LogUtils.d(Storage.settings)
- if (!Storage.settings.enableConsoleLogOut){
+ if (!Storage.settings.enableConsoleLogOut) {
LogUtils.d("关闭日志控制台输出")
}
LogUtils.getConfig().apply {
@@ -24,6 +37,7 @@ class App : Application() {
saveDays = 30
LogUtils.getConfig().setConsoleSwitch(Storage.settings.enableConsoleLogOut)
}
+ ShizukuProvider.enableMultiProcessSupport(true)
CrashReport.initCrashReport(applicationContext, "d0ce46b353", false)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/MainActivity.kt b/app/src/main/java/li/songe/gkd/MainActivity.kt
index e82a300..9addac9 100644
--- a/app/src/main/java/li/songe/gkd/MainActivity.kt
+++ b/app/src/main/java/li/songe/gkd/MainActivity.kt
@@ -1,18 +1,24 @@
package li.songe.gkd
-import androidx.activity.compose.BackHandler
+import android.os.Build
+import android.view.WindowManager
import androidx.activity.compose.setContent
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
import androidx.compose.runtime.CompositionLocalProvider
-import com.blankj.utilcode.util.LogUtils
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.navigation.compose.rememberNavController
+import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.dylanc.activityresult.launcher.StartActivityLauncher
+import com.ramcosta.composedestinations.DestinationsNavHost
import li.songe.gkd.composition.CompositionActivity
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
-import li.songe.gkd.ui.home.HomePage
-import li.songe.gkd.ui.theme.MainTheme
-import li.songe.gkd.util.Ext.LocalLauncher
-import li.songe.gkd.util.Storage
-import li.songe.gkd.util.UseHook
-import li.songe.router.RouterHost
+import li.songe.gkd.ui.NavGraphs
+import li.songe.gkd.ui.theme.AppTheme
+import li.songe.gkd.utils.LocalLauncher
+import li.songe.gkd.utils.LocalNavController
+import li.songe.gkd.utils.StackCacheProvider
+import li.songe.gkd.utils.Storage
class MainActivity : CompositionActivity({
@@ -20,30 +26,63 @@ class MainActivity : CompositionActivity({
val launcher = StartActivityLauncher(this)
onFinish { fs ->
- LogUtils.d(Storage.settings)
if (Storage.settings.excludeFromRecents) {
finishAndRemoveTask() // 会让miui桌面回退动画失效
} else {
fs()
}
}
- onConfigurationChanged { newConfig ->
- LogUtils.d(newConfig)
- UseHook.update(newConfig)
+
+// https://juejin.cn/post/7169147194400833572
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
+// TextView[a==1||b==1||a==1||(a==1&&b==true)]
+// lifecycleScope.launchTry {
+// delay(1000)
+// WindowCompat.setDecorFitsSystemWindows(window, false)
+// val insetsController = WindowCompat.getInsetsController(window, window.decorView)
+// insetsController.hide(WindowInsetsCompat.Type.statusBars())
+// }
+
+// var shizukuIsOK = false
+// val receivedListener: () -> Unit = {
+// shizukuIsOK = true
+// }
+// Shizuku.addBinderReceivedListenerSticky(receivedListener)
+// onDestroy {
+// Shizuku.removeBinderReceivedListener(receivedListener)
+// }
+// lifecycleScope.launchWhile {
+// if (shizukuIsOK) {
+// val top = activityTaskManager.getTasks(1, false, true)?.firstOrNull()
+// if (top!=null) {
+// LogUtils.d(top.topActivity?.packageName, top.topActivity?.className, top.topActivity?.shortClassName)
+// }
+// }
+// delay(5000)
+// }
+
setContent {
- BackHandler {
- finish()
- }
- CompositionLocalProvider(LocalLauncher provides launcher) {
- MainTheme(false) {
- RouterHost(HomePage)
+ val navController = rememberNavController()
+ AppTheme(false) {
+ CompositionLocalProvider(
+ LocalLauncher provides launcher,
+ LocalNavController provides navController
+ ) {
+ StackCacheProvider(navController = navController) {
+ DestinationsNavHost(
+ navGraph = NavGraphs.root,
+ navController = navController,
+ )
+ }
}
}
}
-
})
+
diff --git a/app/src/main/java/li/songe/gkd/accessibility/AbExt.kt b/app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
new file mode 100644
index 0000000..7d66b45
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
@@ -0,0 +1,128 @@
+package li.songe.gkd.accessibility
+
+import android.accessibilityservice.AccessibilityService
+import android.accessibilityservice.GestureDescription
+import android.graphics.Path
+import android.graphics.Rect
+import android.view.accessibility.AccessibilityNodeInfo
+import li.songe.selector.Transform
+import li.songe.selector.Selector
+
+fun AccessibilityNodeInfo.getIndex(): Int {
+ parent?.forEachIndexed { index, accessibilityNodeInfo ->
+ if (accessibilityNodeInfo == this) {
+ return index
+ }
+ }
+ return 0
+}
+
+inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) {
+ var index = 0
+ val childCount = this.childCount
+ while (index < childCount) {
+ val child: AccessibilityNodeInfo? = getChild(index)
+ action(index, child)
+ index += 1
+ }
+}
+
+fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
+ this.isClickable -> {
+ this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
+ "self"
+ }
+
+ else -> {
+ val react = Rect()
+ this.getBoundsInScreen(react)
+ val x = react.left + 50f / 100f * (react.right - react.left)
+ val y = react.top + 50f / 100f * (react.bottom - react.top)
+ if (x >= 0 && y >= 0) {
+ val gestureDescription = GestureDescription.Builder()
+ val path = Path()
+ path.moveTo(x, y)
+ gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
+ service.dispatchGesture(gestureDescription.build(), null, null)
+ "(50%, 50%)"
+ } else {
+ "($x, $y) no click"
+ }
+ }
+}
+
+fun AccessibilityNodeInfo.getDepth(): Int {
+ var p: AccessibilityNodeInfo? = this
+ var depth = 0
+ while (true) {
+ val p2 = p?.parent
+ if (p2 != null) {
+ p = p2
+ depth++
+ } else {
+ break
+ }
+ }
+ return depth
+}
+
+
+fun AccessibilityNodeInfo.querySelector(selector: Selector) =
+ abTransform.querySelector(this, selector)
+
+fun AccessibilityNodeInfo.querySelectorAll(selector: Selector) =
+ abTransform.querySelectorAll(this, selector)
+
+// 不可以在 多线程/不同协程作用域 里同时使用
+private val tempRect = Rect()
+private fun AccessibilityNodeInfo.getTempRect(): Rect {
+ getBoundsInScreen(tempRect)
+ return tempRect
+}
+
+val abTransform = Transform(
+ getAttr = { node, name ->
+ when (name) {
+ "id" -> node.viewIdResourceName
+ "name" -> node.className
+ "text" -> node.text
+ "textLen" -> node.text?.length
+ "desc" -> node.contentDescription
+ "descLen" -> node.contentDescription?.length
+ "childCount" -> node.childCount
+
+ "isEnabled" -> node.isEnabled
+ "isClickable" -> node.isClickable
+ "isChecked" -> node.isChecked
+ "isCheckable" -> node.isCheckable
+ "isFocused" -> node.isFocused
+ "isFocusable" -> node.isFocusable
+ "isVisibleToUser" -> node.isVisibleToUser
+
+ "left" -> node.getTempRect().left
+ "top" -> node.getTempRect().top
+ "right" -> node.getTempRect().right
+ "bottom" -> node.getTempRect().bottom
+
+ "width" -> node.getTempRect().width()
+ "height" -> node.getTempRect().height()
+
+ "index" -> node.getIndex()
+ "depth" -> node.getDepth()
+ else -> null
+ }
+ },
+ getName = { node -> node.className },
+ getChildren = { node ->
+ sequence {
+ repeat(node.childCount) { i ->
+ yield(node.getChild(i))
+ }
+ }
+ },
+ getChild = { node, index -> node.getChild(index) },
+ getParent = { node -> node.parent }
+)
+
+
+
diff --git a/app/src/main/java/li/songe/gkd/accessibility/GkdAbService.kt b/app/src/main/java/li/songe/gkd/accessibility/GkdAbService.kt
index 3934919..c00a438 100644
--- a/app/src/main/java/li/songe/gkd/accessibility/GkdAbService.kt
+++ b/app/src/main/java/li/songe/gkd/accessibility/GkdAbService.kt
@@ -1,31 +1,40 @@
package li.songe.gkd.accessibility
+import android.graphics.Bitmap
+import android.os.Build
+import android.view.Display
import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.NetworkUtils
import com.blankj.utilcode.util.ScreenUtils
import com.blankj.utilcode.util.ServiceUtils
+import com.blankj.utilcode.util.ToastUtils
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
+import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.withContext
import li.songe.gkd.composition.CompositionAbService
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.CompositionExt.useScope
+import li.songe.gkd.data.NodeInfo
+import li.songe.gkd.data.Rule
import li.songe.gkd.data.RuleManager
import li.songe.gkd.data.SubscriptionRaw
-import li.songe.gkd.db.table.SubsItem
-import li.songe.gkd.db.util.RoomX
-import li.songe.gkd.debug.NodeSnapshot
-import li.songe.gkd.selector.click
-import li.songe.gkd.selector.querySelectorAll
-import li.songe.gkd.util.Ext.buildRuleManager
-import li.songe.gkd.util.Ext.getActivityIdByShizuku
-import li.songe.gkd.util.Ext.getSubsFileLastModified
-import li.songe.gkd.util.Ext.launchWhile
-import li.songe.gkd.util.Singleton
-import li.songe.gkd.util.Storage
-import li.songe.selector_core.Selector
-import java.io.File
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.debug.SnapshotExt
+import li.songe.gkd.shizuku.activityTaskManager
+import li.songe.gkd.shizuku.shizukuIsSafeOK
+import li.songe.gkd.utils.Singleton
+import li.songe.gkd.utils.Storage
+import li.songe.gkd.utils.launchTry
+import li.songe.gkd.utils.launchWhile
+import li.songe.gkd.utils.launchWhileTry
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
class GkdAbService : CompositionAbService({
useLifeCycleLog()
@@ -35,7 +44,11 @@ class GkdAbService : CompositionAbService({
val scope = useScope()
service = context
- onDestroy { service = null }
+ onDestroy {
+ service = null
+ currentAppId = null
+ currentActivityId = null
+ }
KeepAliveService.start(context)
onDestroy {
@@ -46,100 +59,98 @@ class GkdAbService : CompositionAbService({
onServiceConnected { serviceConnected = true }
onInterrupt { serviceConnected = false }
- onAccessibilityEvent { event ->
- val activityId = event?.className?.toString() ?: return@onAccessibilityEvent
- val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
- when (event.eventType) {
+ onAccessibilityEvent { event -> // 根据事件获取 activityId, 概率不准确
+ when (event?.eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOWS_CHANGED -> {
-// 在桌面和应用之间来回切换, 大概率导致识别失败
- if (!activityId.startsWith("android.") &&
- !activityId.startsWith("androidx.") &&
- !activityId.startsWith("com.android.")
- ) {
- if ((activityId == "com.miui.home.launcher.Launcher" && rootAppId != "com.miui.home")) {
-// 小米手机 上滑手势, 导致 活动名 不属于包名
-// 另外 微信扫码登录第三方网站 也会导致失败
- } else {
- if (activityId != nodeSnapshot.activityId) {
- nodeSnapshot = nodeSnapshot.copy(
- activityId = activityId
- )
- }
+ val activityId = event.className?.toString() ?: return@onAccessibilityEvent
+ if (activityId == "com.miui.home.launcher.Launcher") { // 小米桌面 bug
+ val appId =
+ rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
+ if (appId != "com.miui.home") {
+ return@onAccessibilityEvent
}
}
+
+ if (activityId.startsWith("android.") ||
+ activityId.startsWith("androidx.") ||
+ activityId.startsWith("com.android.")
+ ) {
+ return@onAccessibilityEvent
+ }
+ currentActivityId = activityId
}
else -> {}
}
}
- scope.launchWhile {
- delay(300)
- val activityId = getActivityIdByShizuku() ?: return@launchWhile
- if (activityId != nodeSnapshot.activityId) {
- nodeSnapshot = nodeSnapshot.copy(
- activityId = activityId
- )
+ onAccessibilityEvent { event -> // 小米手机监听截屏保存快照
+ if (!Storage.settings.enableCaptureSystemScreenshot) return@onAccessibilityEvent
+ if (event?.packageName == null || event.className == null) return@onAccessibilityEvent
+ if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED && event.packageName.contentEquals(
+ "com.miui.screenshot"
+ ) && event.className!!.startsWith("android.") // android.widget.RelativeLayout
+ ) {
+ scope.launchTry {
+ val snapshot = SnapshotExt.captureSnapshot()
+ ToastUtils.showShort("保存快照成功")
+ LogUtils.d("截屏:保存快照", snapshot.id)
+ }
}
}
- var subsFileLastModified = 0L
- scope.launchWhile { // 根据本地文件最新写入时间 决定 是否 更新数据
- val t = getSubsFileLastModified()
- if (t > subsFileLastModified) {
- subsFileLastModified = t
- ruleManager = buildRuleManager()
- LogUtils.d("读取本地规则")
- }
- delay(10_000)
- }
-
- scope.launchWhile {
- delay(50)
+ scope.launchWhile { // 屏幕无障碍信息轮询
+ delay(200)
if (!serviceConnected) return@launchWhile
if (!Storage.settings.enableService || ScreenUtils.isScreenLock()) return@launchWhile
- nodeSnapshot = nodeSnapshot.copy(
- root = rootInActiveWindow,
- )
- val shot = nodeSnapshot
- if (shot.root == null) return@launchWhile
- for (rule in ruleManager.match(shot.appId, shot.activityId)) {
- val target = rule.query(shot.root) ?: continue
- val clickResult = target.click(context)
- ruleManager.trigger(rule)
- LogUtils.d(
- *rule.matches.toTypedArray(),
- NodeSnapshot.abNodeToNode(target),
- clickResult
- )
+ currentAppId = rootInActiveWindow?.packageName?.toString()
+ var tempRules = rules
+ var i = 0
+ while (i < tempRules.size) {
+ val rule = tempRules[i]
+ i++
+ if (!ruleManager.ruleIsAvailable(rule)) continue
+ val frozenNode = rootInActiveWindow
+ val target = rule.query(frozenNode)
+ if (target != null) {
+ val clickResult = target.click(context)
+ ruleManager.trigger(rule)
+ LogUtils.d(
+ *rule.matches.toTypedArray(), NodeInfo.abNodeToNode(target), clickResult
+ )
+ }
+ delay(50)
+ currentAppId = rootInActiveWindow?.packageName?.toString()
+ if (tempRules != rules) {
+ tempRules = rules
+ i = 0
+ }
}
- delay(150)
}
- scope.launchWhile {
+ scope.launchWhile { // 自动从网络更新订阅文件
delay(5000)
- RoomX.select().map { subsItem ->
- if (!NetworkUtils.isAvailable()) return@map
+ if (!NetworkUtils.isAvailable()) return@launchWhile
+ DbSet.subsItemDao.query().first().forEach { subsItem ->
try {
val text = Singleton.client.get(subsItem.updateUrl).bodyAsText()
val subscriptionRaw = SubscriptionRaw.parse5(text)
if (subscriptionRaw.version <= subsItem.version) {
- return@map
+ return@forEach
}
val newItem = subsItem.copy(
- updateUrl = subscriptionRaw.updateUrl
- ?: subsItem.updateUrl,
+ updateUrl = subscriptionRaw.updateUrl ?: subsItem.updateUrl,
name = subscriptionRaw.name,
mtime = System.currentTimeMillis()
)
- RoomX.update(newItem)
- File(newItem.filePath).writeText(
+ newItem.subsFile.writeText(
SubscriptionRaw.stringify(
subscriptionRaw
)
)
- LogUtils.d("更新订阅文件:${subsItem.name}")
+ DbSet.subsItemDao.update(newItem)
+ LogUtils.d("更新磁盘订阅文件:${subsItem.name}")
} catch (e: Exception) {
e.printStackTrace()
}
@@ -147,29 +158,96 @@ class GkdAbService : CompositionAbService({
delay(30 * 60_000)
}
-}) {
- private var nodeSnapshot = NodeSnapshot()
- set(value) {
- if (field.appId != value.appId || field.activityId != value.activityId) {
- LogUtils.d(
- value.appId,
- value.activityId,
- *ruleManager.match(value.appId, value.activityId).toList().toTypedArray()
- )
+ scope.launchTry {
+ DbSet.subsItemDao.query().flowOn(IO).collect {
+ val subscriptionRawArray = withContext(IO) {
+ it.filter { s -> s.enable }
+ .mapNotNull { s -> s.subscriptionRaw }
}
- field = value
+ ruleManager = RuleManager(*subscriptionRawArray.toTypedArray())
}
+ }
- private var ruleManager = RuleManager()
+ scope.launchWhileTry(interval = 400) {
+ if (shizukuIsSafeOK()) {
+ val topActivity =
+ activityTaskManager.getTasks(1, false, true)?.firstOrNull()?.topActivity
+ if (topActivity != null) {
+ currentAppId = topActivity.packageName
+ currentActivityId = topActivity.className
+ }
+ }
+ }
+
+}) {
companion object {
+ private var service: GkdAbService? = null
fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java)
- fun currentNodeSnapshot() = service?.nodeSnapshot
- fun match(selector: String) {
- val rootAbNode = service?.rootInActiveWindow ?: return
- val list = rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
+
+ private var ruleManager = RuleManager()
+ set(value) {
+ field = value
+ rules = value.match(currentAppId, currentActivityId).toList()
+ }
+ private var rules = listOf()
+ set(value) {
+ field = value
+ LogUtils.d(
+ "currentAppId: $currentAppId",
+ "currentActivityId: $currentActivityId",
+ *value.toTypedArray()
+ )
+ }
+
+ var currentActivityId: String? = null
+ set(value) {
+ val oldValue = field
+ field = value
+ if (value != oldValue) {
+ rules = ruleManager.match(currentAppId, value).toList()
+ }
+ }
+ private var currentAppId: String? = null
+ set(value) {
+ val oldValue = field
+ field = value
+ if (value != oldValue) {
+ rules = ruleManager.match(value, currentActivityId).toList()
+ }
+ }
+ val currentAbNode: AccessibilityNodeInfo?
+ get() {
+ return service?.rootInActiveWindow
+ }
+
+ suspend fun currentScreenshot() = service?.run {
+ suspendCoroutine {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ takeScreenshot(Display.DEFAULT_DISPLAY,
+ application.mainExecutor,
+ object : TakeScreenshotCallback {
+ override fun onSuccess(screenshot: ScreenshotResult) {
+ it.resume(
+ Bitmap.wrapHardwareBuffer(
+ screenshot.hardwareBuffer, screenshot.colorSpace
+ )
+ )
+ }
+
+ override fun onFailure(errorCode: Int) = it.resume(null)
+ })
+ } else {
+ it.resume(null)
+ }
+ }
}
- private var service: GkdAbService? = null
+// fun match(selector: String) {
+// val rootAbNode = service?.rootInActiveWindow ?: return
+// val list =
+// rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
+// }
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/accessibility/KeepAliveService.kt b/app/src/main/java/li/songe/gkd/accessibility/KeepAliveService.kt
index 589e92a..9ce80aa 100644
--- a/app/src/main/java/li/songe/gkd/accessibility/KeepAliveService.kt
+++ b/app/src/main/java/li/songe/gkd/accessibility/KeepAliveService.kt
@@ -6,9 +6,8 @@ import kotlinx.coroutines.delay
import li.songe.gkd.App
import li.songe.gkd.composition.CompositionService
import li.songe.gkd.composition.CompositionExt.useScope
-import li.songe.gkd.util.Ext.createNotificationChannel
-import li.songe.gkd.util.Ext.launchWhile
-
+import li.songe.gkd.utils.launchWhile
+import li.songe.gkd.utils.Ext.createNotificationChannel
class KeepAliveService : CompositionService({
createNotificationChannel(this)
diff --git a/app/src/main/java/li/songe/gkd/accessibility/NodeSnapshot.kt b/app/src/main/java/li/songe/gkd/accessibility/NodeSnapshot.kt
deleted file mode 100644
index 838d5d2..0000000
--- a/app/src/main/java/li/songe/gkd/accessibility/NodeSnapshot.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package li.songe.gkd.accessibility
-
-import android.view.accessibility.AccessibilityNodeInfo
-
-data class NodeSnapshot(
- val root: AccessibilityNodeInfo? = null,
- val activityId: String? = null,
-) {
- val appId by lazy { root?.packageName?.toString() }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/accessibility/ShizukuService.kt b/app/src/main/java/li/songe/gkd/accessibility/ShizukuService.kt
new file mode 100644
index 0000000..3d891ca
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/accessibility/ShizukuService.kt
@@ -0,0 +1,7 @@
+package li.songe.gkd.accessibility
+
+import li.songe.gkd.composition.CompositionService
+
+class ShizukuService: CompositionService({
+
+})
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt b/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt
index 7d29e36..0ed6486 100644
--- a/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt
+++ b/app/src/main/java/li/songe/gkd/composition/CompositionExt.kt
@@ -6,14 +6,13 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
-import li.songe.gkd.util.Singleton
+import li.songe.gkd.utils.Singleton
import kotlin.coroutines.CoroutineContext
object CompositionExt {
@@ -40,15 +39,14 @@ object CompositionExt {
}
val filter = IntentFilter(packageName)
- val broadcastManager = LocalBroadcastManager.getInstance(this)
- broadcastManager.registerReceiver(receiver, filter)
+ registerReceiver(receiver, filter)
val sendMessage: (InvokeMessage) -> Unit = { message ->
- broadcastManager.sendBroadcast(Intent(packageName).apply {
+ sendBroadcast(Intent(packageName).apply {
putExtra("__invoke", Singleton.json.encodeToString(message))
})
}
onDestroy {
- broadcastManager.unregisterReceiver(receiver)
+ unregisterReceiver(receiver)
}
val setter: ((InvokeMessage) -> Unit) -> Unit = { onMessage = it }
return (setter to sendMessage)
diff --git a/app/src/main/java/li/songe/gkd/data/AppInfo.kt b/app/src/main/java/li/songe/gkd/data/AppInfo.kt
new file mode 100644
index 0000000..e0a48a2
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/data/AppInfo.kt
@@ -0,0 +1,35 @@
+package li.songe.gkd.data
+
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import li.songe.gkd.App
+import li.songe.gkd.utils.Ext.getApplicationInfoExt
+
+data class AppInfo(
+ val id: String,
+ val name: String? = null,
+ val icon: Drawable? = null,
+ val installed: Boolean = true
+)
+
+private val appInfoCache = mutableMapOf()
+
+fun getAppInfo(id: String): AppInfo {
+ appInfoCache[id]?.let { return it }
+ val packageManager = App.context.packageManager
+ val info = try {
+// 需要权限
+ val rawInfo = App.context.packageManager.getApplicationInfoExt(
+ id, PackageManager.GET_META_DATA
+ )
+ AppInfo(
+ id = id,
+ name = packageManager.getApplicationLabel(rawInfo).toString(),
+ icon = packageManager.getApplicationIcon(rawInfo),
+ )
+ } catch (e: Exception) {
+ return AppInfo(id = id, installed = false)
+ }
+ appInfoCache[id] = info
+ return info
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/debug/AttrSnapshot.kt b/app/src/main/java/li/songe/gkd/data/AttrInfo.kt
similarity index 87%
rename from app/src/main/java/li/songe/gkd/debug/AttrSnapshot.kt
rename to app/src/main/java/li/songe/gkd/data/AttrInfo.kt
index 967a94b..55808d1 100644
--- a/app/src/main/java/li/songe/gkd/debug/AttrSnapshot.kt
+++ b/app/src/main/java/li/songe/gkd/data/AttrInfo.kt
@@ -1,13 +1,13 @@
-package li.songe.gkd.debug
+package li.songe.gkd.data
import android.graphics.Rect
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
-import li.songe.gkd.selector.getDepth
-import li.songe.gkd.selector.getIndex
+import li.songe.gkd.accessibility.getDepth
+import li.songe.gkd.accessibility.getIndex
@Serializable
-data class AttrSnapshot(
+data class AttrInfo(
val id: String? = null,
val name: String? = null,
val text: String? = null,
@@ -30,9 +30,9 @@ data class AttrSnapshot(
private val rect = Rect()
fun info2data(
nodeInfo: AccessibilityNodeInfo,
- ): AttrSnapshot {
+ ): AttrInfo {
nodeInfo.getBoundsInScreen(rect)
- return AttrSnapshot(
+ return AttrInfo(
id = nodeInfo.viewIdResourceName,
name = nodeInfo.className?.toString(),
text = nodeInfo.text?.toString(),
diff --git a/app/src/main/java/li/songe/gkd/debug/DeviceSnapshot.kt b/app/src/main/java/li/songe/gkd/data/DeviceInfo.kt
similarity index 55%
rename from app/src/main/java/li/songe/gkd/debug/DeviceSnapshot.kt
rename to app/src/main/java/li/songe/gkd/data/DeviceInfo.kt
index d1181b0..310e7c8 100644
--- a/app/src/main/java/li/songe/gkd/debug/DeviceSnapshot.kt
+++ b/app/src/main/java/li/songe/gkd/data/DeviceInfo.kt
@@ -1,25 +1,18 @@
-package li.songe.gkd.debug
+package li.songe.gkd.data
import android.os.Build
-import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
-data class DeviceSnapshot(
- @SerialName("device")
+data class DeviceInfo(
val device: String = Build.DEVICE,
- @SerialName("model")
val model: String = Build.MODEL,
- @SerialName("manufacturer")
val manufacturer: String = Build.MANUFACTURER,
- @SerialName("brand")
val brand: String = Build.BRAND,
- @SerialName("sdkInt")
val sdkInt: Int = Build.VERSION.SDK_INT,
- @SerialName("release")
val release: String = Build.VERSION.RELEASE,
){
companion object{
- val instance by lazy { DeviceSnapshot() }
+ val instance by lazy { DeviceInfo() }
}
}
diff --git a/app/src/main/java/li/songe/gkd/debug/NodeSnapshot.kt b/app/src/main/java/li/songe/gkd/data/NodeInfo.kt
similarity index 68%
rename from app/src/main/java/li/songe/gkd/debug/NodeSnapshot.kt
rename to app/src/main/java/li/songe/gkd/data/NodeInfo.kt
index 64cbc44..9418ddb 100644
--- a/app/src/main/java/li/songe/gkd/debug/NodeSnapshot.kt
+++ b/app/src/main/java/li/songe/gkd/data/NodeInfo.kt
@@ -1,24 +1,17 @@
-package li.songe.gkd.debug
+package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
-import li.songe.gkd.selector.forEachIndexed
+import li.songe.gkd.accessibility.forEachIndexed
import java.util.ArrayDeque
-
-/**
- * api/node 返回列表
- */
-
@Serializable
-data class NodeSnapshot(
- val id: Int,
- val pid: Int,
- val index: Int,
+data class NodeInfo(
+ val id: Int, val pid: Int, val index: Int,
/**
* null: when getChild(i) return null
*/
- val attr: AttrSnapshot?
+ val attr: AttrInfo?
) {
companion object {
fun abNodeToNode(
@@ -26,22 +19,17 @@ data class NodeSnapshot(
id: Int = 0,
pid: Int = -1,
index: Int = 0,
- ): NodeSnapshot {
- return NodeSnapshot(
- id,
- pid,
- index,
- nodeInfo?.let { AttrSnapshot.info2data(nodeInfo) }
- )
+ ): NodeInfo {
+ return NodeInfo(id, pid, index, nodeInfo?.let { AttrInfo.info2data(nodeInfo) })
}
- fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List {
+ fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List {
if (nodeInfo == null) {
return emptyList()
}
val stack = ArrayDeque>()
stack.push(0 to nodeInfo)
- val list = mutableListOf()
+ val list = mutableListOf()
list.add(abNodeToNode(nodeInfo, index = 0))
while (stack.isNotEmpty()) {
val top = stack.pop()
diff --git a/app/src/main/java/li/songe/gkd/debug/RpcError.kt b/app/src/main/java/li/songe/gkd/data/RpcError.kt
similarity index 84%
rename from app/src/main/java/li/songe/gkd/debug/RpcError.kt
rename to app/src/main/java/li/songe/gkd/data/RpcError.kt
index 1a9a9d9..e83492b 100644
--- a/app/src/main/java/li/songe/gkd/debug/RpcError.kt
+++ b/app/src/main/java/li/songe/gkd/data/RpcError.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.debug
+package li.songe.gkd.data
import kotlinx.serialization.Serializable
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
data class RpcError(
override val message: String = "unknown error",
val code: Int = 0,
- val X_Rpc_Result: Boolean = true
+ val X_Rpc_Result:String = "error"
) : Exception(message) {
companion object {
const val HeaderKey = "X_Rpc_Result"
diff --git a/app/src/main/java/li/songe/gkd/data/Rule.kt b/app/src/main/java/li/songe/gkd/data/Rule.kt
index fa5f1a3..a86ff8f 100644
--- a/app/src/main/java/li/songe/gkd/data/Rule.kt
+++ b/app/src/main/java/li/songe/gkd/data/Rule.kt
@@ -1,8 +1,8 @@
package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo
-import li.songe.gkd.selector.querySelector
-import li.songe.selector_core.Selector
+import li.songe.gkd.accessibility.querySelector
+import li.songe.selector.Selector
data class Rule(
/**
diff --git a/app/src/main/java/li/songe/gkd/data/RuleManager.kt b/app/src/main/java/li/songe/gkd/data/RuleManager.kt
index 8498f07..ccf6e30 100644
--- a/app/src/main/java/li/songe/gkd/data/RuleManager.kt
+++ b/app/src/main/java/li/songe/gkd/data/RuleManager.kt
@@ -1,6 +1,6 @@
package li.songe.gkd.data
-import li.songe.selector_core.Selector
+import li.songe.selector.Selector
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
@@ -88,24 +88,31 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
}
}
+
fun match(appId: String? = null, activityId: String? = null) = sequence {
if (appId == null) return@sequence
val rules = appToRulesMap[appId] ?: return@sequence
+ if (activityId == null) {
+ yieldAll(rules)
+ return@sequence
+ }
rules.forEach { rule ->
- if (!rule.active) return@forEach // 处于冷却时间
+ if (rule.excludeActivityIds.any { activityId.startsWith(it) }) return@forEach // 是被排除的 界面 id
- if (rule.excludeActivityIds.contains(activityId)) return@forEach // 是被排除的 界面 id
-
- if (rule.preRules.isNotEmpty()) { // 需要提前触发某个规则
- val record = triggerLogQueue.lastOrNull() ?: return@forEach
- if (!rule.preRules.any { it == record.rule }) return@forEach // 上一个触发的规则不在当前需要触发的列表
- }
-
- if (activityId == null || rule.matchAnyActivity // 全匹配
- || rule.activityIds.contains(activityId) // 在匹配列表
+ if (rule.matchAnyActivity || rule.activityIds.any { activityId.startsWith(it) } // 在匹配列表
) {
yield(rule)
}
}
}
+
+ fun ruleIsAvailable(rule: Rule): Boolean {
+ if (!rule.active) return false // 处于冷却时间
+ if (rule.preKeys.isNotEmpty()) { // 需要提前触发某个规则
+ if (rule.preRules.isEmpty()) return false // 声明了 preKeys 但是没有在当前列表找到
+ val record = triggerLogQueue.lastOrNull() ?: return false
+ if (!rule.preRules.any { it == record.rule }) return false // 上一个触发的规则不在当前需要触发的列表
+ }
+ return true
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/data/Snapshot.kt b/app/src/main/java/li/songe/gkd/data/Snapshot.kt
new file mode 100644
index 0000000..089b087
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/data/Snapshot.kt
@@ -0,0 +1,82 @@
+package li.songe.gkd.data
+
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.TypeConverters
+import androidx.room.Update
+import com.blankj.utilcode.util.AppUtils
+import com.blankj.utilcode.util.ScreenUtils
+import kotlinx.coroutines.flow.Flow
+import kotlinx.serialization.Serializable
+import li.songe.gkd.accessibility.GkdAbService
+import li.songe.gkd.db.IgnoreConverters
+import li.songe.gkd.utils.Ext
+
+@TypeConverters(IgnoreConverters::class)
+@Entity(
+ tableName = "snapshot",
+)
+@Serializable
+data class Snapshot(
+ @PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
+ @ColumnInfo(name = "app_id") val appId: String? = null,
+ @ColumnInfo(name = "activity_id") val activityId: String? = null,
+ @ColumnInfo(name = "app_name") val appName: String? = Ext.getAppName(appId),
+ @ColumnInfo(name = "app_version_code") val appVersionCode: Int? = appId?.let {
+ AppUtils.getAppVersionCode(
+ appId
+ )
+ },
+ @ColumnInfo(name = "app_version_name") val appVersionName: String? = appId?.let {
+ AppUtils.getAppVersionName(
+ appId
+ )
+ },
+
+ @ColumnInfo(name = "screen_height") val screenHeight: Int = ScreenUtils.getScreenHeight(),
+ @ColumnInfo(name = "screen_width") val screenWidth: Int = ScreenUtils.getScreenWidth(),
+ @ColumnInfo(name = "is_landscape") val isLandscape: Boolean = ScreenUtils.isLandscape(),
+
+ @ColumnInfo(name = "device") val device: String = DeviceInfo.instance.device,
+ @ColumnInfo(name = "model") val model: String = DeviceInfo.instance.model,
+ @ColumnInfo(name = "manufacturer") val manufacturer: String = DeviceInfo.instance.manufacturer,
+ @ColumnInfo(name = "brand") val brand: String = DeviceInfo.instance.brand,
+ @ColumnInfo(name = "sdk_int") val sdkInt: Int = DeviceInfo.instance.sdkInt,
+ @ColumnInfo(name = "release") val release: String = DeviceInfo.instance.release,
+
+ @ColumnInfo(name = "_1") val nodes: List = emptyList(),
+) {
+ companion object {
+ fun current(includeNode: Boolean = true): Snapshot {
+ val currentAbNode = GkdAbService.currentAbNode
+ val appId = currentAbNode?.packageName?.toString()
+ val currentActivityId = GkdAbService.currentActivityId
+ return Snapshot(
+ appId = appId,
+ activityId = currentActivityId,
+ nodes = if (includeNode) NodeInfo.info2nodeList(currentAbNode) else emptyList()
+ )
+ }
+ }
+
+ @Dao
+ @TypeConverters(IgnoreConverters::class)
+ interface SnapshotDao {
+ @Update
+ suspend fun update(vararg objects: Snapshot): Int
+
+ @Insert
+ suspend fun insert(vararg users: Snapshot): List
+
+ @Delete
+ suspend fun delete(vararg users: Snapshot): Int
+
+ @Query("SELECT * FROM snapshot")
+ fun query(): Flow>
+ }
+}
diff --git a/app/src/main/java/li/songe/gkd/data/SubsConfig.kt b/app/src/main/java/li/songe/gkd/data/SubsConfig.kt
new file mode 100644
index 0000000..4994a5f
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/data/SubsConfig.kt
@@ -0,0 +1,64 @@
+package li.songe.gkd.data
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+import kotlinx.parcelize.Parcelize
+
+@Entity(
+ tableName = "subs_config",
+)
+@Parcelize
+data class SubsConfig(
+ @PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
+ @ColumnInfo(name = "type") val type: Int = SubsType,
+ @ColumnInfo(name = "enable") val enable: Boolean = true,
+
+ @ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
+ @ColumnInfo(name = "app_id") val appId: String = "",
+ @ColumnInfo(name = "group_key") val groupKey: Int = -1,
+) : Parcelable {
+
+ companion object {
+ const val SubsType = 0
+ const val AppType = 1
+ const val GroupType = 2
+ }
+
+ @Dao
+ interface SubsConfigDao {
+
+ @Update
+ suspend fun update(vararg objects: SubsConfig): Int
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(vararg users: SubsConfig): List
+
+ @Delete
+ suspend fun delete(vararg users: SubsConfig): Int
+
+ @Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId")
+ suspend fun deleteSubs(subsItemId: Long): Int
+
+ @Query("SELECT * FROM subs_config")
+ fun query(): Flow>
+
+ @Query("SELECT * FROM subs_config WHERE type=${SubsType}")
+ fun querySubsTypeConfig(): Flow>
+
+ @Query("SELECT * FROM subs_config WHERE type=${AppType} and subs_item_id=:subsItemId")
+ fun queryAppTypeConfig(subsItemId: Long): Flow>
+
+ @Query("SELECT * FROM subs_config WHERE type=${GroupType} and subs_item_id=:subsItemId and app_id=:appId")
+ suspend fun queryGroupTypeConfig(subsItemId: Long, appId: String): List
+ }
+
+}
diff --git a/app/src/main/java/li/songe/gkd/data/SubsItem.kt b/app/src/main/java/li/songe/gkd/data/SubsItem.kt
new file mode 100644
index 0000000..53f3c5a
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/data/SubsItem.kt
@@ -0,0 +1,79 @@
+package li.songe.gkd.data
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Update
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.utils.FolderExt
+import java.io.File
+
+@Entity(
+ tableName = "subs_item",
+)
+@Parcelize
+data class SubsItem(
+ @PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
+ @ColumnInfo(name = "mtime") val mtime: Long = System.currentTimeMillis(),
+ @ColumnInfo(name = "enable") val enable: Boolean = true,
+ @ColumnInfo(name = "enable_update") val enableUpdate: Boolean = true,
+ @ColumnInfo(name = "order") val order: Int = 0,
+
+// 订阅文件的根字段
+ @ColumnInfo(name = "name") val name: String = "",
+ @ColumnInfo(name = "author") val author: String = "",
+ @ColumnInfo(name = "version") val version: Int = 0,
+ @ColumnInfo(name = "update_url") val updateUrl: String = "",
+ @ColumnInfo(name = "support_url") val supportUrl: String = "",
+
+ ) : Parcelable {
+
+ @IgnoredOnParcel
+ val subsFile by lazy {
+ File(FolderExt.subsFolder.absolutePath.plus("/${id}.json"))
+ }
+
+ @IgnoredOnParcel
+ val subscriptionRaw by lazy {
+ try {
+ SubscriptionRaw.parse5(subsFile.readText())
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ suspend fun removeAssets() {
+ DbSet.subsItemDao.delete(this)
+ withContext(IO) {
+ subsFile.exists() && subsFile.delete()
+ }
+ DbSet.subsConfigDao.deleteSubs(id)
+ }
+
+
+ @Dao
+ interface SubsItemDao {
+ @Update
+ suspend fun update(vararg objects: SubsItem): Int
+
+ @Insert
+ suspend fun insert(vararg users: SubsItem): List
+
+ @Delete
+ suspend fun delete(vararg users: SubsItem): Int
+
+ @Query("SELECT * FROM subs_item ORDER BY `order`")
+ fun query(): Flow>
+ }
+}
diff --git a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt
index 201796f..27d6310 100644
--- a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt
+++ b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt
@@ -6,8 +6,8 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
-import li.songe.gkd.util.Singleton
-import li.songe.selector_core.Selector
+import li.songe.gkd.utils.Singleton
+import li.songe.selector.Selector
@Parcelize
@@ -35,6 +35,7 @@ data class SubscriptionRaw(
@Serializable
data class GroupRaw(
@SerialName("name") val name: String? = null,
+ @SerialName("desc") val desc: String? = null,
@SerialName("key") val key: Int? = null,
@SerialName("cd") val cd: Long? = null,
@SerialName("activityIds") val activityIds: List? = null,
@@ -78,13 +79,13 @@ data class SubscriptionRaw(
JsonNull, null -> null
is JsonArray -> element.map {
when (it) {
- is JsonObject, is JsonArray, JsonNull -> error("Element ${this::class} is not a int")
+ is JsonObject, is JsonArray, JsonNull -> error("Element $it is not a int")
is JsonPrimitive -> it.int
}
}
is JsonPrimitive -> listOf(element.int)
- else -> error("")
+ else -> error("Element $element is not a Array")
}
}
@@ -95,11 +96,11 @@ data class SubscriptionRaw(
if (p.isString) {
p.content
} else {
- error("")
+ error("Element $p is not a string")
}
}
- else -> error("")
+ else -> error("Element $p is not a string")
}
@Suppress("SameParameterValue")
@@ -110,7 +111,7 @@ data class SubscriptionRaw(
p.long
}
- else -> error("")
+ else -> error("Element $p is not a long")
}
private fun getInt(json: JsonObject? = null, key: String = ""): Int? =
@@ -120,7 +121,7 @@ data class SubscriptionRaw(
p.int
}
- else -> error("")
+ else -> error("Element $p is not a int")
}
private fun jsonToRuleRaw(rulesRawJson: JsonElement): RuleRaw {
@@ -134,12 +135,10 @@ data class SubscriptionRaw(
excludeActivityIds = getStringIArray(rulesJson, "excludeActivityIds"),
cd = getLong(rulesJson, "cd"),
matches = (getStringIArray(
- rulesJson,
- "matches"
+ rulesJson, "matches"
) ?: emptyList()).onEach { Selector.parse(it) },
excludeMatches = (getStringIArray(
- rulesJson,
- "excludeMatches"
+ rulesJson, "excludeMatches"
) ?: emptyList()).onEach { Selector.parse(it) },
key = getInt(rulesJson, "key"),
name = getString(rulesJson, "name"),
@@ -154,11 +153,11 @@ data class SubscriptionRaw(
is JsonObject -> groupsRawJson
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("rules" to groupsRawJson))
}
- return GroupRaw(
- activityIds = getStringIArray(groupsJson, "activityIds"),
+ return GroupRaw(activityIds = getStringIArray(groupsJson, "activityIds"),
excludeActivityIds = getStringIArray(groupsJson, "excludeActivityIds"),
cd = getLong(groupsJson, "cd"),
name = getString(groupsJson, "name"),
+ desc = getString(groupsJson, "desc"),
key = getInt(groupsJson, "key"),
rules = when (val rulesJson = groupsJson["rules"]) {
null, JsonNull -> emptyList()
@@ -166,13 +165,11 @@ data class SubscriptionRaw(
is JsonArray -> rulesJson
}.map {
jsonToRuleRaw(it)
- }
- )
+ })
}
private fun jsonToAppRaw(appsJson: JsonObject): AppRaw {
- return AppRaw(
- activityIds = getStringIArray(appsJson, "activityIds"),
+ return AppRaw(activityIds = getStringIArray(appsJson, "activityIds"),
excludeActivityIds = getStringIArray(appsJson, "excludeActivityIds"),
cd = getLong(appsJson, "cd"),
id = getString(appsJson, "id") ?: error(""),
@@ -182,25 +179,22 @@ data class SubscriptionRaw(
is JsonArray -> groupsJson
}).map {
jsonToGroupRaw(it)
- }
- )
+ })
}
private fun jsonToSubscriptionRaw(rootJson: JsonObject): SubscriptionRaw {
- return SubscriptionRaw(
- name = getString(rootJson, "name") ?: error(""),
+ return SubscriptionRaw(name = getString(rootJson, "name") ?: error(""),
version = getInt(rootJson, "version") ?: error(""),
author = getString(rootJson, "author"),
updateUrl = getString(rootJson, "updateUrl"),
supportUrl = getString(rootJson, "supportUrl"),
apps = rootJson["apps"]?.jsonArray?.map { jsonToAppRaw(it.jsonObject) }
- ?: emptyList()
- )
+ ?: emptyList())
}
fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source)
- fun parse(source: String): SubscriptionRaw {
+ private fun parse(source: String): SubscriptionRaw {
return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject)
}
diff --git a/app/src/main/java/li/songe/gkd/data/TriggerLog.kt b/app/src/main/java/li/songe/gkd/data/TriggerLog.kt
new file mode 100644
index 0000000..ff29092
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/data/TriggerLog.kt
@@ -0,0 +1,54 @@
+package li.songe.gkd.data
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Update
+import kotlinx.parcelize.Parcelize
+import java.nio.channels.Selector
+
+@Entity(
+ tableName = "trigger_log",
+)
+@Parcelize
+data class TriggerLog(
+ /**
+ * 此 id 与某个 snapshot id 一致, 表示 one to one
+ */
+ @PrimaryKey @ColumnInfo(name = "id") val id: Long,
+ /**
+ * 订阅文件 id
+ */
+ @ColumnInfo(name = "subs_id") val subsId: Long,
+ /**
+ * 触发的组 id
+ */
+ @ColumnInfo(name = "group_key") val groupKey: Int,
+
+ /**
+ * 触发的选择器
+ */
+ @ColumnInfo(name = "match") val match: String,
+
+ ) : Parcelable {
+ @Dao
+ interface TriggerLogDao {
+
+ @Update
+ suspend fun update(vararg objects: TriggerLog): Int
+
+ @Insert
+ suspend fun insert(vararg users: TriggerLog): List
+
+ @Delete
+ suspend fun delete(vararg users: TriggerLog): Int
+
+ @Query("SELECT * FROM trigger_log")
+ suspend fun query(): List
+ }
+}
diff --git a/app/src/main/java/li/songe/gkd/db/AppDatabase.kt b/app/src/main/java/li/songe/gkd/db/AppDatabase.kt
deleted file mode 100644
index c4d34b0..0000000
--- a/app/src/main/java/li/songe/gkd/db/AppDatabase.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package li.songe.gkd.db
-
-import androidx.room.Database
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import com.blankj.utilcode.util.PathUtils
-import li.songe.gkd.App
-import li.songe.gkd.db.table.SubsConfig
-import li.songe.gkd.db.table.SubsItem
-import java.io.File
-
-@Database(
- version = 1,
- entities = [SubsItem::class, SubsConfig::class],
- autoMigrations = [
-// AutoMigration(from = 1, to = 2),
-// AutoMigration(from = 2, to = 3),
- ],
-// 自动迁移 https://developer.android.com/training/data-storage/room/migrating-db-versions#automated
-)
-abstract class AppDatabase : RoomDatabase() {
- abstract fun subsItemRoomDao(): SubsItem.RoomDao
- abstract fun subsConfigRoomDao(): SubsConfig.RoomDao
-
- companion object {
- val db by lazy {
- File(PathUtils.getExternalAppFilesPath().plus("/db/")).apply {
- if (!exists()) {
- mkdir()
- }
- }
- val name = PathUtils.getExternalAppFilesPath().plus("/db/database.db")
- Room.databaseBuilder(
- App.context,
- AppDatabase::class.java,
- name
- )
- .fallbackToDestructiveMigration()
- .build()
- }
- }
-}
diff --git a/app/src/main/java/li/songe/gkd/db/BaseDao.kt b/app/src/main/java/li/songe/gkd/db/BaseDao.kt
deleted file mode 100644
index 91cc749..0000000
--- a/app/src/main/java/li/songe/gkd/db/BaseDao.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package li.songe.gkd.db
-
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.RawQuery
-import androidx.room.Update
-import androidx.sqlite.db.SupportSQLiteQuery
-
-interface BaseDao {
- @Insert
- suspend fun insert(vararg objects: T): List
-
- @Delete
- suspend fun delete(vararg objects: T): Int
-
- @Update
- suspend fun update(vararg objects: T): Int
-
- @RawQuery
- suspend fun query(sqLiteQuery: SupportSQLiteQuery): List
-
- @RawQuery
- suspend fun delete(sqLiteQuery: SupportSQLiteQuery): List
-
- // https://developer.android.com/training/data-storage/room/async-queries#kotlin
-// you must set observedEntities in sub interface
-// @RawQuery
-// fun queryFlow(sqLiteQuery: SupportSQLiteQuery): Flow>
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/BaseTable.kt b/app/src/main/java/li/songe/gkd/db/BaseTable.kt
deleted file mode 100644
index a6d1bc7..0000000
--- a/app/src/main/java/li/songe/gkd/db/BaseTable.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package li.songe.gkd.db
-
-interface BaseTable {
- val id: Long
- val ctime: Long
- val mtime: Long
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/DbSet.kt b/app/src/main/java/li/songe/gkd/db/DbSet.kt
new file mode 100644
index 0000000..835324d
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/db/DbSet.kt
@@ -0,0 +1,32 @@
+package li.songe.gkd.db
+
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import com.blankj.utilcode.util.PathUtils
+import li.songe.gkd.App
+import li.songe.gkd.utils.FolderExt
+import java.io.File
+
+object DbSet {
+
+
+ private fun getDb(
+ klass: Class, name: String
+ ): T {
+ return Room.databaseBuilder(
+ App.context, klass, FolderExt.dbFolder.absolutePath.plus("/${name}.db")
+ ).fallbackToDestructiveMigration()
+ .enableMultiInstanceInvalidation()
+ .build()
+ }
+
+ private val snapshotDb by lazy { getDb(SnapshotDb::class.java, "snapshot") }
+ private val subsConfigDb by lazy { getDb(SubsConfigDb::class.java, "subsConfig") }
+ private val subsItemDb by lazy { getDb(SubsItemDb::class.java, "subsItem") }
+ private val triggerLogDb by lazy { getDb(TriggerLogDb::class.java, "triggerLog") }
+
+ val subsItemDao by lazy { subsItemDb.subsItemDao() }
+ val subsConfigDao by lazy { subsConfigDb.subsConfigDao() }
+ val snapshotDao by lazy { snapshotDb.snapshotDao() }
+ val triggerLogDao by lazy { triggerLogDb.triggerLogDao() }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt b/app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
new file mode 100644
index 0000000..6dd88b2
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
@@ -0,0 +1,14 @@
+package li.songe.gkd.db
+
+import androidx.room.TypeConverter
+import li.songe.gkd.data.NodeInfo
+
+object IgnoreConverters {
+ @TypeConverter
+ @JvmStatic
+ fun listToCol(list: List): String? = null
+
+ @TypeConverter
+ @JvmStatic
+ fun colToList(value: String?): List = emptyList()
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/LogDatabase.kt b/app/src/main/java/li/songe/gkd/db/LogDatabase.kt
deleted file mode 100644
index 3ec183f..0000000
--- a/app/src/main/java/li/songe/gkd/db/LogDatabase.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package li.songe.gkd.db
-
-import androidx.room.Database
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import com.blankj.utilcode.util.PathUtils
-import li.songe.gkd.App
-import li.songe.gkd.db.table.TriggerLog
-import java.io.File
-
-@Database(
- version = 1,
- entities = [TriggerLog::class],
-)
-abstract class LogDatabase : RoomDatabase() {
- abstract fun triggerLogRoomDao(): TriggerLog.RoomDao
-
- companion object {
- val logDb by lazy {
- File(PathUtils.getExternalAppFilesPath().plus("/db/")).apply {
- if (!exists()) {
- mkdir()
- }
- }
- val name = PathUtils.getExternalAppFilesPath().plus("/db/log.db")
- Room.databaseBuilder(
- App.context,
- LogDatabase::class.java,
- name
- )
- .fallbackToDestructiveMigration()
- .build()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/SnapshotDb.kt b/app/src/main/java/li/songe/gkd/db/SnapshotDb.kt
new file mode 100644
index 0000000..38a7187
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/db/SnapshotDb.kt
@@ -0,0 +1,13 @@
+package li.songe.gkd.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import li.songe.gkd.data.Snapshot
+
+@Database(
+ version = 1,
+ entities = [Snapshot::class],
+)
+abstract class SnapshotDb: RoomDatabase() {
+ abstract fun snapshotDao(): Snapshot.SnapshotDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/SubsConfigDb.kt b/app/src/main/java/li/songe/gkd/db/SubsConfigDb.kt
new file mode 100644
index 0000000..58bfb56
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/db/SubsConfigDb.kt
@@ -0,0 +1,13 @@
+package li.songe.gkd.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import li.songe.gkd.data.SubsConfig
+
+@Database(
+ version = 1,
+ entities = [SubsConfig::class],
+)
+abstract class SubsConfigDb: RoomDatabase() {
+ abstract fun subsConfigDao(): SubsConfig.SubsConfigDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/SubsItemDb.kt b/app/src/main/java/li/songe/gkd/db/SubsItemDb.kt
new file mode 100644
index 0000000..0e3232b
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/db/SubsItemDb.kt
@@ -0,0 +1,13 @@
+package li.songe.gkd.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import li.songe.gkd.data.SubsItem
+
+@Database(
+ version = 1,
+ entities = [SubsItem::class],
+)
+abstract class SubsItemDb: RoomDatabase() {
+ abstract fun subsItemDao(): SubsItem.SubsItemDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt b/app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
new file mode 100644
index 0000000..5797c59
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
@@ -0,0 +1,13 @@
+package li.songe.gkd.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import li.songe.gkd.data.TriggerLog
+
+@Database(
+ version = 1,
+ entities = [TriggerLog::class],
+)
+abstract class TriggerLogDb : RoomDatabase() {
+ abstract fun triggerLogDao(): TriggerLog.TriggerLogDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/table/SubsConfig.kt b/app/src/main/java/li/songe/gkd/db/table/SubsConfig.kt
deleted file mode 100644
index f550b58..0000000
--- a/app/src/main/java/li/songe/gkd/db/table/SubsConfig.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package li.songe.gkd.db.table
-
-import android.os.Parcelable
-import androidx.room.ColumnInfo
-import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import kotlinx.parcelize.Parcelize
-import li.songe.gkd.db.BaseDao
-import li.songe.gkd.db.BaseTable
-
-@Entity(
- tableName = "subs_config",
-)
-@Parcelize
-data class SubsConfig(
- @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
- @ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
- @ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
-
- /**
- * 0 - app
- * 1 - group
- * 2 - rule
- */
- @ColumnInfo(name = "type") val type: Int = 0,
- @ColumnInfo(name = "enable") val enable: Boolean = true,
-
- @ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
- @ColumnInfo(name = "app_id") val appId: String = "",
- @ColumnInfo(name = "group_key") val groupKey: Int = -1,
- @ColumnInfo(name = "rule_key") val ruleKey: Int = -1,
-) : BaseTable, Parcelable {
-
- companion object {
- const val AppType = 0
- const val GroupType = 1
- const val RuleType = 2
- }
-
- @Dao
- interface RoomDao : BaseDao
-}
diff --git a/app/src/main/java/li/songe/gkd/db/table/SubsItem.kt b/app/src/main/java/li/songe/gkd/db/table/SubsItem.kt
deleted file mode 100644
index 91ea29d..0000000
--- a/app/src/main/java/li/songe/gkd/db/table/SubsItem.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package li.songe.gkd.db.table
-
-import android.os.Parcelable
-import androidx.room.ColumnInfo
-import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.Index
-import androidx.room.PrimaryKey
-import kotlinx.parcelize.Parcelize
-import li.songe.gkd.db.BaseDao
-import li.songe.gkd.db.BaseTable
-
-@Entity(
- tableName = "subs_item",
- indices = [Index(value = ["update_url"], unique = true)]
-)
-@Parcelize
-data class SubsItem(
- /**
- * 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段
- */
- @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
- @ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
- @ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
-
- @ColumnInfo(name = "enable") val enable: Boolean = true,
-
- /**
- * 订阅文件 name 属性
- */
- @ColumnInfo(name = "name") val name: String = "",
-
- /**
- * 订阅文件下载地址,也是更新链接
- */
- @ColumnInfo(name = "update_url") val updateUrl: String = "",
-
- /**
- * 订阅文件下载地址,也是更新链接
- */
- @ColumnInfo(name = "version") val version: Int = 0,
-
- /**
- * 订阅文件下载后存放的路径
- */
- @ColumnInfo(name = "file_path") val filePath: String = "",
-
- /**
- * 顺序
- */
- @ColumnInfo(name = "index") val index: Int = 0,
-
-
- ) : Parcelable, BaseTable {
- @Dao
- interface RoomDao : BaseDao
-}
diff --git a/app/src/main/java/li/songe/gkd/db/table/TriggerLog.kt b/app/src/main/java/li/songe/gkd/db/table/TriggerLog.kt
deleted file mode 100644
index 8b270dd..0000000
--- a/app/src/main/java/li/songe/gkd/db/table/TriggerLog.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package li.songe.gkd.db.table
-
-import android.os.Parcelable
-import androidx.room.ColumnInfo
-import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import kotlinx.parcelize.Parcelize
-import li.songe.gkd.db.BaseDao
-import li.songe.gkd.db.BaseTable
-
-@Entity(
- tableName = "trigger_log",
-)
-@Parcelize
-data class TriggerLog(
- @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
- @ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
- @ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
- @ColumnInfo(name = "app_id") val appId: String? = null,
- @ColumnInfo(name = "activity_id") val activityId: String? = null,
- @ColumnInfo(name = "selector") val selector: String = ""
-) : Parcelable, BaseTable {
- @Dao
- interface RoomDao : BaseDao
-}
diff --git a/app/src/main/java/li/songe/gkd/db/util/Expression.kt b/app/src/main/java/li/songe/gkd/db/util/Expression.kt
deleted file mode 100644
index deeb616..0000000
--- a/app/src/main/java/li/songe/gkd/db/util/Expression.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package li.songe.gkd.db.util
-
-import android.database.DatabaseUtils
-import kotlin.reflect.KClass
-
-data class Expression(
- val left: L,
- val operator: String,
- val right: R,
- val tableClass: KClass
-) {
- fun stringify(): String {
- val nameText = when (left) {
- is String -> left.toString()
- is Expression<*, *, *> -> left.stringify()
- else -> throw Exception("not support type : $left")
- }
- val valueText = when (right) {
- null -> "NULL"
- is Boolean -> (if (right) 0 else 1).toString()
- is String -> DatabaseUtils.sqlEscapeString(right.toString())
- is Byte, is UByte, is Short, is UShort, is Int, is UInt, is Long, is ULong, is Float, is Double -> right.toString()
- is List<*> -> "(" + right.joinToString(",\u0020") {
- if (it is String) {
- DatabaseUtils.sqlEscapeString(it)
- } else {
- it?.toString() ?: "NULL"
- }
- } + ")"
- is GlobString -> right.stringify()
- is LikeString -> right.stringify()
- is Expression<*, *, *> -> "(${right.stringify()})"
- else -> throw Exception("not support type : $right")
- }
- return "$nameText $operator $valueText"
- }
-
- infix fun and(other: Expression) =
- Expression(this, "AND", other, tableClass)
-
- infix fun or(other: Expression) =
- Expression(this, "OR", other, tableClass)
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/util/GlobString.kt b/app/src/main/java/li/songe/gkd/db/util/GlobString.kt
deleted file mode 100644
index be7a36b..0000000
--- a/app/src/main/java/li/songe/gkd/db/util/GlobString.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package li.songe.gkd.db.util
-
-import android.database.DatabaseUtils
-
-data class GlobString(val sqlString: String = "") {
- fun one() = GlobString("$sqlString?")
- fun any() = GlobString("$sqlString*")
- infix fun one(s: String) = GlobString("$sqlString?").str(s)
- infix fun any(s: String) = GlobString("$sqlString*").str(s)
- infix fun str(s: String) = GlobString(
- sqlString + s.replace("\\", "\\\\")
- .replace("*", "\\*")
- .replace("?", "\\?")
- )
-
- fun stringify() = "${DatabaseUtils.sqlEscapeString(sqlString)} ESCAPE '\\'"
-
- companion object {
- fun globString(value: String = "") = GlobString().str(value)
- }
-}
-
diff --git a/app/src/main/java/li/songe/gkd/db/util/LikeString.kt b/app/src/main/java/li/songe/gkd/db/util/LikeString.kt
deleted file mode 100644
index 125cf69..0000000
--- a/app/src/main/java/li/songe/gkd/db/util/LikeString.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package li.songe.gkd.db.util
-
-import android.database.DatabaseUtils
-
-
-data class LikeString (val sqlString: String = "") {
- fun one() = LikeString("$sqlString?")
- fun any() = LikeString("$sqlString*")
- infix fun one(s: String) = LikeString("${sqlString}_").str(s)
- infix fun any(s: String) = LikeString("$sqlString%").str(s)
- infix fun str(s: String) = LikeString(
- sqlString + s.replace("\\", "\\\\")
- .replace("_", "\\_")
- .replace("%", "\\%")
- )
-
- fun stringify() = "${DatabaseUtils.sqlEscapeString(sqlString)} ESCAPE '\\'"
-
- companion object {
- fun likeString(value: String = "") = LikeString().str(value)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/util/Operator.kt b/app/src/main/java/li/songe/gkd/db/util/Operator.kt
deleted file mode 100644
index 43a32eb..0000000
--- a/app/src/main/java/li/songe/gkd/db/util/Operator.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package li.songe.gkd.db.util
-
-import kotlin.reflect.KMutableProperty1
-import kotlin.reflect.KProperty1
-
-object Operator {
- infix fun Expression.and(other: Expression) =
- Expression(this, "AND", other, tableClass)
-
- infix fun Expression.or(other: Expression) =
- Expression(this, "OR", other, tableClass)
-
-
- // TODO 当同时设置 Property1 时, 代码失效
-// 还需要写 Int, Long, String, Boolean 等多种类型的重载, 这种重复性很高,工作量指数级增长的工作确实需要联合类型
- inline fun KMutableProperty1.baseOperator(
- value: V2,
- operator: String,
- ) =
- Expression(
- RoomAnnotation.getColumnName(T::class, name),
- operator,
- value,
- T::class
- )
-
- inline infix fun KProperty1.eq(value: V) =
- baseOperator(value, "==")
-
- inline infix fun KProperty1.neq(value: V) =
- baseOperator(value, "!=")
-
- inline infix fun KProperty1.less(value: V) =
- baseOperator(value, "<")
-
- inline infix fun KProperty1.lessEq(value: V) =
- baseOperator(value, "<=")
-
- inline infix fun KProperty1.greater(value: V) =
- baseOperator(value, ">")
-
- inline infix fun KProperty1.greaterEq(value: V) =
- baseOperator(value, ">=")
-
- inline infix fun KProperty1.inList(value: List) =
- baseOperator(value, "IN")
-
- inline infix fun KProperty1.glob(value: GlobString) =
- baseOperator(value, "GLOB")
-
- inline infix fun KProperty1.like(value: LikeString) =
- baseOperator(value, "LIKE")
-
- inline fun KProperty1.baseOperator(
- value: V2,
- operator: String,
- ) =
- Expression(
- RoomAnnotation.getColumnName(T::class, name),
- operator,
- value,
- T::class
- )
-
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/util/RoomAnnotation.kt b/app/src/main/java/li/songe/gkd/db/util/RoomAnnotation.kt
deleted file mode 100644
index 6407377..0000000
--- a/app/src/main/java/li/songe/gkd/db/util/RoomAnnotation.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package li.songe.gkd.db.util
-
-import li.songe.gkd.db.table.SubsConfig
-import li.songe.gkd.db.table.SubsItem
-import li.songe.gkd.db.table.TriggerLog
-import kotlin.reflect.KClass
-
-object RoomAnnotation {
-
- fun getTableName(cls: KClass<*>): String = when (cls) {
- SubsConfig::class -> "subs_config"
- SubsItem::class -> "subs_item"
- TriggerLog::class -> "trigger_log"
- else -> throw Exception("""not found className : ${cls.qualifiedName}""")
- }
-
- fun getColumnName(cls: KClass<*>, propertyName: String): String = when (cls) {
- SubsConfig::class -> when (propertyName) {
- SubsConfig::id.name -> "id"
- SubsConfig::ctime.name -> "ctime"
- SubsConfig::mtime.name -> "mtime"
- SubsConfig::type.name -> "type"
- SubsConfig::enable.name -> "enable"
- SubsConfig::subsItemId.name -> "subs_item_id"
- SubsConfig::appId.name -> "app_id"
- SubsConfig::groupKey.name -> "group_key"
- SubsConfig::ruleKey.name -> "rule_key"
- else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
- }
-
- SubsItem::class -> when (propertyName) {
- SubsItem::id.name -> "id"
- SubsItem::ctime.name -> "ctime"
- SubsItem::mtime.name -> "mtime"
- SubsItem::enable.name -> "enable"
- SubsItem::name.name -> "name"
- SubsItem::updateUrl.name -> "update_url"
- SubsItem::filePath.name -> "file_path"
- SubsItem::index.name -> "index"
- else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
- }
-
- TriggerLog::class -> when (propertyName) {
- TriggerLog::id.name -> "id"
- TriggerLog::ctime.name -> "ctime"
- TriggerLog::mtime.name -> "mtime"
- TriggerLog::appId.name -> "app_id"
- TriggerLog::activityId.name -> "activity_id"
- TriggerLog::selector.name -> "selector"
- else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
- }
-
- else -> error("""not found className : ${cls.qualifiedName}""")
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/db/util/RoomX.kt b/app/src/main/java/li/songe/gkd/db/util/RoomX.kt
deleted file mode 100644
index 9e1f37e..0000000
--- a/app/src/main/java/li/songe/gkd/db/util/RoomX.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package li.songe.gkd.db.util
-
-import androidx.sqlite.db.SimpleSQLiteQuery
-import li.songe.gkd.db.AppDatabase.Companion.db
-import li.songe.gkd.db.BaseDao
-import li.songe.gkd.db.LogDatabase.Companion.logDb
-import li.songe.gkd.db.table.*
-import kotlin.reflect.KClass
-
-
-object RoomX {
- // 把表类和具体数据库方法关联起来
- @Suppress("UNCHECKED_CAST")
- fun getBaseDao(cls: KClass) = when (cls) {
- SubsItem::class -> db.subsItemRoomDao()
- SubsConfig::class -> db.subsConfigRoomDao()
- TriggerLog::class -> logDb.triggerLogRoomDao()
- else -> error("not found class dao : ${cls::class.java.name}")
- } as BaseDao
-
-
- suspend inline fun update(vararg objects: T): Int {
- return getBaseDao(T::class).update(*objects)
- }
-
- /**
- * 插入成功后, 自动改变入参对象的 id
- */
- suspend inline fun insert(vararg objects: T): List {
- return getBaseDao(T::class).insert(*objects)
- }
-
- suspend inline fun delete(vararg objects: T) =
- getBaseDao(T::class).delete(*objects)
-
- suspend inline fun select(
- limit: Int? = null,
- offset: Int? = null,
- noinline block: (() -> Expression<*, *, T>)? = null
- ): List {
- val expression = block?.invoke()
- val tableName = RoomAnnotation.getTableName(T::class)
- val sqlString = "SELECT * FROM $tableName" + (if (expression != null) {
- " WHERE ${expression.stringify()}"
- } else {
- ""
- }) + (if (limit != null) {
- " LIMIT $limit"
- } else {
- ""
- }) + (if (offset != null) {
- " OFFSET $offset"
- } else {
- ""
- })
- val baseDao = getBaseDao(T::class)
- return baseDao.query(SimpleSQLiteQuery(sqlString))
- }
-
- suspend inline fun delete(
- limit: Int? = null,
- offset: Int? = null,
- noinline block: (() -> Expression<*, *, T>)? = null
- ): List {
- val expression = block?.invoke()
- val tableName = RoomAnnotation.getTableName(T::class)
- val sqlString = "DELETE FROM $tableName" + (if (expression != null) {
- " WHERE ${expression.stringify()}"
- } else {
- ""
- }) + (if (limit != null) {
- " LIMIT $limit"
- } else {
- ""
- }) + (if (offset != null) {
- " OFFSET $offset"
- } else {
- ""
- })
- val baseDao = getBaseDao(T::class)
- return baseDao.delete(SimpleSQLiteQuery(sqlString))
- }
-}
-
diff --git a/app/src/main/java/li/songe/gkd/debug/FloatingService.kt b/app/src/main/java/li/songe/gkd/debug/FloatingService.kt
index 22bbc1b..67ca8d8 100644
--- a/app/src/main/java/li/songe/gkd/debug/FloatingService.kt
+++ b/app/src/main/java/li/songe/gkd/debug/FloatingService.kt
@@ -8,11 +8,14 @@ import com.blankj.utilcode.util.ServiceUtils
import com.torrydo.floatingbubbleview.FloatingBubble
import li.songe.gkd.App
import li.songe.gkd.R
+import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.CompositionFbService
import li.songe.gkd.composition.CompositionExt.useMessage
import li.songe.gkd.composition.InvokeMessage
+import li.songe.gkd.utils.SafeR
class FloatingService : CompositionFbService({
+ useLifeCycleLog()
val context = this
val (onMessage, sendMessage) = useMessage(this::class.simpleName)
@@ -22,9 +25,8 @@ class FloatingService : CompositionFbService({
"removeBubbles" -> context.removeBubbles()
}
}
-
setupBubble { _, resolve ->
- val builder = FloatingBubble.Builder(this).bubble(R.drawable.capture, 40, 40)
+ val builder = FloatingBubble.Builder(this).bubble(SafeR.capture, 40, 40)
.enableCloseBubble(false)
.addFloatingBubbleListener(object : FloatingBubble.Listener {
override fun onClick() {
@@ -38,16 +40,16 @@ class FloatingService : CompositionFbService({
override fun setupNotificationBuilder(channelId: String): Notification {
return NotificationCompat.Builder(this, channelId)
.setOngoing(true)
- .setSmallIcon(R.drawable.ic_app_2)
- .setContentTitle("bubble is running")
- .setContentText("click to do nothing")
+ .setSmallIcon(SafeR.ic_launcher)
+ .setContentTitle("搞快点")
+ .setContentText("正在显示悬浮窗按钮")
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
}
- override fun channelId() = "your_channel_id"
- override fun channelName() = "your_channel_name"
+ override fun channelId() = "service-floating"
+ override fun channelName() = "悬浮窗按钮服务"
override fun notificationId() = 69
companion object{
diff --git a/app/src/main/java/li/songe/gkd/debug/HttpService.kt b/app/src/main/java/li/songe/gkd/debug/HttpService.kt
index 77ab1cd..0061564 100644
--- a/app/src/main/java/li/songe/gkd/debug/HttpService.kt
+++ b/app/src/main/java/li/songe/gkd/debug/HttpService.kt
@@ -21,16 +21,24 @@ import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import li.songe.gkd.App
import li.songe.gkd.composition.CompositionExt.useMessage
import li.songe.gkd.composition.CompositionService
import li.songe.gkd.composition.InvokeMessage
+import li.songe.gkd.data.DeviceInfo
+import li.songe.gkd.data.RpcError
+import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
-import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
-import li.songe.gkd.util.Storage
+import li.songe.gkd.utils.Ext.getIpAddressInLocalNetwork
+import li.songe.gkd.utils.Storage
+import li.songe.gkd.utils.launchTry
import java.io.File
class HttpService : CompositionService({
@@ -71,30 +79,16 @@ class HttpService : CompositionService({
routing {
route("/api") {
- get("/device") { call.respond(DeviceSnapshot.instance) }
- get("/snapshotIds") {
- call.respond(SnapshotExt.getSnapshotIds())
- }
+ get("/device") { call.respond(DeviceInfo.instance) }
get("/snapshot") {
val id = call.request.queryParameters["id"]?.toLongOrNull()
- if (id != null) {
- val fp = File(SnapshotExt.getSnapshotPath(id))
- if (!fp.exists()) {
- throw RpcError("对应快照不存在")
- }
- call.response.cacheControl(CacheControl.MaxAge(3600))
- call.respondFile(fp)
- } else {
- removeBubbles()
- delay(200)
- try {
- call.respond(captureSnapshot())
- } catch (e: Exception) {
- throw e
- } finally {
- showBubbles()
- }
+ ?: throw RpcError("miss id")
+ val fp = File(SnapshotExt.getSnapshotPath(id))
+ if (!fp.exists()) {
+ throw RpcError("对应快照不存在")
}
+ call.response.cacheControl(CacheControl.MaxAge(3600 * 24 * 7))
+ call.respondFile(fp)
}
get("/screenshot") {
val id = call.request.queryParameters["id"]?.toLongOrNull()
@@ -103,22 +97,35 @@ class HttpService : CompositionService({
if (!fp.exists()) {
throw RpcError("对应截图不存在")
}
- call.response.cacheControl(CacheControl.MaxAge(3600))
+ call.response.cacheControl(CacheControl.MaxAge(3600 * 24 * 7))
call.respondFile(fp)
}
+ get("/captureSnapshot") {
+ removeBubbles()
+ delay(200)
+ val snapshot = try {
+ captureSnapshot()
+ } finally {
+ showBubbles()
+ }
+ call.respond(snapshot)
+ }
+ get("/snapshots") {
+ call.respond(DbSet.snapshotDao.query().first())
+ }
}
}
}
- scope.launch {
+ scope.launchTry(Dispatchers.IO) {
LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
.toList().toTypedArray())
server.start(true)
}
onDestroy {
- scope.launch(Dispatchers.IO) {
- server.stop(1000, 2000)
- scope.cancel()
+ scope.launchTry(Dispatchers.IO) {
+ server.stop()
LogUtils.d("http server is stopped")
+ scope.cancel()
}
}
}) {
diff --git a/app/src/main/java/li/songe/gkd/debug/RpcErrorHeaderPlugin.kt b/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt
similarity index 93%
rename from app/src/main/java/li/songe/gkd/debug/RpcErrorHeaderPlugin.kt
rename to app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt
index e4ea7dd..28cc9ad 100644
--- a/app/src/main/java/li/songe/gkd/debug/RpcErrorHeaderPlugin.kt
+++ b/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt
@@ -8,6 +8,7 @@ import io.ktor.server.application.hooks.CallFailed
import io.ktor.server.request.uri
import io.ktor.server.response.header
import io.ktor.server.response.respond
+import li.songe.gkd.data.RpcError
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
onCall { call ->
@@ -36,6 +37,7 @@ val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin"
}
onCallRespond { call, _ ->
call.response.header("Access-Control-Expose-Headers", "*")
+ call.response.header("Access-Control-Allow-Private-Network", "true")
val status = call.response.status() ?: HttpStatusCode.OK
if (status == HttpStatusCode.OK &&
!call.response.headers.contains(
diff --git a/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt b/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt
index e12d79d..9d749eb 100644
--- a/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt
+++ b/app/src/main/java/li/songe/gkd/debug/ScreenshotService.kt
@@ -6,11 +6,13 @@ import android.content.Intent
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ServiceUtils
import li.songe.gkd.App
+import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
import li.songe.gkd.composition.CompositionService
-import li.songe.gkd.util.Ext
-import li.songe.gkd.util.ScreenshotUtil
+import li.songe.gkd.utils.Ext
+import li.songe.gkd.utils.ScreenshotUtil
class ScreenshotService : CompositionService({
+ useLifeCycleLog()
Ext.createNotificationChannel(this, 110)
onStartCommand { intent, _, _ ->
diff --git a/app/src/main/java/li/songe/gkd/debug/Snapshot.kt b/app/src/main/java/li/songe/gkd/debug/Snapshot.kt
deleted file mode 100644
index e23f256..0000000
--- a/app/src/main/java/li/songe/gkd/debug/Snapshot.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package li.songe.gkd.debug
-
-import com.blankj.utilcode.util.ScreenUtils
-import kotlinx.serialization.Serializable
-import li.songe.gkd.accessibility.GkdAbService
-import li.songe.gkd.util.Ext
-
-@Serializable
-data class Snapshot(
- val id: Long = System.currentTimeMillis(),
- val device: DeviceSnapshot? = DeviceSnapshot.instance,
- val screenHeight: Int = ScreenUtils.getScreenHeight(),
- val screenWidth: Int = ScreenUtils.getScreenWidth(),
- val appId: String? = null,
- val appName: String? = null,
- val activityId: String? = null,
- val nodes: List? = null,
-) {
- companion object {
- fun current(): Snapshot {
- val shot = GkdAbService.currentNodeSnapshot()
- return Snapshot(
- appId = shot?.appId,
- appName = if (shot?.appId != null) {
- Ext.getAppName(shot.appId)
- } else null,
- activityId = shot?.activityId,
- nodes = NodeSnapshot.info2nodeList(shot?.root),
- )
- }
- }
-}
diff --git a/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt b/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
index bd9b320..c549535 100644
--- a/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
+++ b/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
@@ -2,13 +2,20 @@ package li.songe.gkd.debug
import android.graphics.Bitmap
import com.blankj.utilcode.util.LogUtils
+import com.blankj.utilcode.util.ScreenUtils
+import com.blankj.utilcode.util.ZipUtils
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.encodeToString
import li.songe.gkd.App
import li.songe.gkd.accessibility.GkdAbService
-import li.songe.gkd.util.Singleton
+import li.songe.gkd.data.RpcError
+import li.songe.gkd.data.Snapshot
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.utils.Singleton
import java.io.File
object SnapshotExt {
@@ -16,7 +23,15 @@ object SnapshotExt {
App.context.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
}
- private fun getSnapshotParentPath(snapshotId: Long) =
+ private val emptyBitmap by lazy {
+ Bitmap.createBitmap(
+ ScreenUtils.getScreenWidth(),
+ ScreenUtils.getScreenHeight(),
+ Bitmap.Config.ARGB_8888
+ )
+ }
+
+ fun getSnapshotParentPath(snapshotId: Long) =
"${snapshotDir.absolutePath}/${snapshotId}"
fun getSnapshotPath(snapshotId: Long) =
@@ -30,21 +45,51 @@ object SnapshotExt {
?.mapNotNull { f -> f.name.toLongOrNull() } ?: emptyList()
}
+ suspend fun getSnapshotZipFile(snapshotId: Long): File {
+ val file = File(getSnapshotParentPath(snapshotId) + "/${snapshotId}.zip")
+ if (!file.exists()) {
+ withContext(Dispatchers.IO) {
+ ZipUtils.zipFiles(
+ listOf(
+ getSnapshotPath(snapshotId),
+ getScreenshotPath(snapshotId)
+ ), file.absolutePath
+ )
+ }
+ }
+ return file
+ }
+
+ fun remove(id: Long) {
+ File(getSnapshotParentPath(id)).apply {
+ if (exists()) {
+ deleteRecursively()
+ }
+ }
+ }
+
suspend fun captureSnapshot(): Snapshot {
if (!GkdAbService.isRunning()) {
throw RpcError("无障碍不可用")
}
- if (!ScreenshotService.isRunning()) {
- LogUtils.d("截屏不可用,即将使用空白图片")
+
+ val snapshotDef = coroutineScope { async(Dispatchers.IO) { Snapshot.current() } }
+ val bitmapDef = coroutineScope {
+ async(Dispatchers.IO) {
+ GkdAbService.currentScreenshot() ?: withTimeoutOrNull(3_000) {
+ if (!ScreenshotService.isRunning()) {
+ return@withTimeoutOrNull null
+ }
+ ScreenshotService.screenshot()
+ } ?: emptyBitmap.apply {
+ LogUtils.d("截屏不可用,即将使用空白图片")
+ }
+ }
}
- val snapshot = Snapshot.current()
- val bitmap = withTimeoutOrNull(3_000) {
- ScreenshotService.screenshot()
- } ?: Bitmap.createBitmap(
- snapshot.screenWidth,
- snapshot.screenHeight,
- Bitmap.Config.ARGB_8888
- )
+
+ val bitmap = bitmapDef.await()
+ val snapshot = snapshotDef.await()
+
withContext(Dispatchers.IO) {
File(getSnapshotParentPath(snapshot.id)).apply { if (!exists()) mkdirs() }
val stream =
@@ -53,6 +98,7 @@ object SnapshotExt {
stream.close()
val text = Singleton.json.encodeToString(snapshot)
File(getSnapshotPath(snapshot.id)).writeText(text)
+ DbSet.snapshotDao.insert(snapshot)
}
return snapshot
}
diff --git a/app/src/main/java/li/songe/gkd/icon/AddIcon.kt b/app/src/main/java/li/songe/gkd/icon/AddIcon.kt
new file mode 100644
index 0000000..f73d831
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/icon/AddIcon.kt
@@ -0,0 +1,24 @@
+package li.songe.gkd.icon
+
+import androidx.compose.foundation.Image
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.addPathNodes
+import androidx.compose.ui.tooling.preview.Preview
+
+// @DslMarker
+// https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-react/src/jsMain/kotlin/react/ChildrenBuilder.kt
+val AddIcon = materialIcon(name = "add") {
+ addPath(
+ pathData = addPathNodes("M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"),
+ fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
+ )
+}
+
+@Preview
+@Composable
+fun PreviewIconAdd() {
+ Image(imageVector = AddIcon, contentDescription = null)
+}
diff --git a/app/src/main/java/li/songe/gkd/icon/TestDsl.kt b/app/src/main/java/li/songe/gkd/icon/TestDsl.kt
new file mode 100644
index 0000000..759c6f7
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/icon/TestDsl.kt
@@ -0,0 +1,31 @@
+package li.songe.gkd.icon
+
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.Image
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+
+@Preview
+@Composable
+fun PreviewTestDsl() {
+ val vectorString = """
+
+
+
+ """.trim()
+ val drawable = Drawable.createFromStream(vectorString.byteInputStream(), "ic_back")
+ if (drawable != null) {
+ Image(painter = rememberDrawablePainter(drawable = drawable), contentDescription = null)
+ } else {
+ Text(text = "null drawable")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/selector/AbNode.kt b/app/src/main/java/li/songe/gkd/selector/AbNode.kt
deleted file mode 100644
index de29db8..0000000
--- a/app/src/main/java/li/songe/gkd/selector/AbNode.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package li.songe.gkd.selector
-
-import android.view.accessibility.AccessibilityNodeInfo
-import li.songe.selector_core.NodeExt
-
-@JvmInline
-value class AbNode(val value: AccessibilityNodeInfo) : NodeExt {
- override val parent: NodeExt?
- get() = value.parent?.let { AbNode(it) }
- override val children: Sequence
- get() = sequence {
- repeat(value.childCount) { i ->
- val child = value.getChild(i)
- if (child != null) {
- yield(AbNode(child))
- } else {
- yield(null)
- }
- }
- }
-
- override fun getChild(offset: Int) = value.getChild(offset)?.let { AbNode(it) }
-
- override val name: CharSequence
- get() = value.className
-
- override fun attr(name: String): Any? = when (name) {
- "id" -> value.viewIdResourceName
- "name" -> value.className
- "text" -> value.text
- "textLen" -> value.text?.length
- "desc" -> value.contentDescription
- "descLen" -> value.contentDescription?.length
- "isClickable" -> value.isClickable
- "childCount" -> value.childCount
- "index" -> value.getIndex()
- "depth" -> value.getDepth()
- else -> null
- }
-}
-
diff --git a/app/src/main/java/li/songe/gkd/selector/AbNodeExt.kt b/app/src/main/java/li/songe/gkd/selector/AbNodeExt.kt
deleted file mode 100644
index edbac69..0000000
--- a/app/src/main/java/li/songe/gkd/selector/AbNodeExt.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package li.songe.gkd.selector
-
-import android.accessibilityservice.AccessibilityService
-import android.accessibilityservice.GestureDescription
-import android.graphics.Path
-import android.graphics.Rect
-import android.view.accessibility.AccessibilityNodeInfo
-import li.songe.selector_core.Selector
-
-fun AccessibilityNodeInfo.getIndex(): Int {
- parent?.forEachIndexed { index, accessibilityNodeInfo ->
- if (accessibilityNodeInfo == this) {
- return index
- }
- }
- return 0
-}
-
-inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) {
- var index = 0
- val childCount = this.childCount
- while (index < childCount) {
- val child: AccessibilityNodeInfo? = getChild(index)
- action(index, child)
- index += 1
- }
-}
-
-fun AccessibilityNodeInfo.querySelector(selector: Selector): AccessibilityNodeInfo? {
- val ab = AbNode(this)
- val result = (ab.querySelector(selector) as AbNode?) ?: return null
- return result.value
-}
-
-fun AccessibilityNodeInfo.querySelectorAll(selector: Selector): Sequence {
- val ab = AbNode(this)
- return ab.querySelectorAll(selector) as Sequence
-}
-
-fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
- this.isClickable -> {
- this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
- "self"
- }
-
- else -> {
- val react = Rect()
- this.getBoundsInScreen(react)
- val x = react.left + 50f / 100f * (react.right - react.left)
- val y = react.top + 50f / 100f * (react.bottom - react.top)
- if (x >= 0 && y >= 0) {
- val gestureDescription = GestureDescription.Builder()
- val path = Path()
- path.moveTo(x, y)
- gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
- service.dispatchGesture(gestureDescription.build(), null, null)
- "(50%, 50%)"
- } else {
- "($x, $y) no click"
- }
- }
-}
-
-fun AccessibilityNodeInfo.getDepth(): Int {
- var p: AccessibilityNodeInfo? = this
- var depth = 0
- while (true) {
- val p2 = p?.parent
- if (p2 != null) {
- p = p2
- depth++
- } else {
- break
- }
- }
- return depth
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/shizuku/AutoStartReceiver.kt b/app/src/main/java/li/songe/gkd/shizuku/AutoStartReceiver.kt
index f715465..5780fd0 100644
--- a/app/src/main/java/li/songe/gkd/shizuku/AutoStartReceiver.kt
+++ b/app/src/main/java/li/songe/gkd/shizuku/AutoStartReceiver.kt
@@ -11,27 +11,9 @@ class AutoStartReceiver : BroadcastReceiver() {
Shizuku.addBinderReceivedListenerSticky(oneShotBinderReceivedListener)
}
}
+
private val oneShotBinderReceivedListener = object : Shizuku.OnBinderReceivedListener {
override fun onBinderReceived() {
-// AutomatorViewModel.get().run {
-// app.openFileOutput("on_boot", Context.MODE_PRIVATE).bufferedWriter().apply {
-// write("binder received")
-// newLine()
-// write("permission granted: ${Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED}")
-// newLine()
-// write("is_binding: ${isBinding.value}")
-// newLine()
-// write("is_running: ${isRunning.value}")
-// newLine()
-// if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED &&
-// isBinding.value != true && isRunning.value != true
-// ) {
-// write("starting service...")
-// toggleService()
-// isAutoStarted.value = true
-// }
-// }
-// }
Shizuku.removeBinderReceivedListener(this)
}
}
diff --git a/app/src/main/java/li/songe/gkd/shizuku/IOUtils.kt b/app/src/main/java/li/songe/gkd/shizuku/IOUtils.kt
deleted file mode 100644
index aa1e5f4..0000000
--- a/app/src/main/java/li/songe/gkd/shizuku/IOUtils.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-package li.songe.gkd.shizuku
-
-import android.content.Context
-import android.util.Log
-import java.io.*
-import java.nio.charset.Charset
-import java.nio.charset.StandardCharsets
-import java.security.DigestInputStream
-import java.security.MessageDigest
-import java.util.zip.CRC32
-
-
-object IOUtils {
- private const val TAG = "IOUtils"
- @Throws(IOException::class)
- fun copyStream(from: InputStream, to: OutputStream) {
- val buf = ByteArray(1024 * 1024)
- var len: Int
- while (from.read(buf).also { len = it } > 0) {
- to.write(buf, 0, len)
- }
- }
-
- @Throws(IOException::class)
- fun copyFile(original: File?, destination: File?) {
- FileInputStream(original).use { inputStream ->
- FileOutputStream(destination).use { outputStream ->
- copyStream(
- inputStream,
- outputStream
- )
- }
- }
- }
-
- @Throws(IOException::class)
- fun copyFileFromAssets(context: Context, assetFileName: String?, destination: File?) {
- context.assets.open(assetFileName!!).use { inputStream ->
- FileOutputStream(destination).use { outputStream ->
- copyStream(
- inputStream,
- outputStream
- )
- }
- }
- }
-
- fun deleteRecursively(f: File) {
- if (f.isDirectory) {
- val files = f.listFiles()
- if (files != null) {
- for (child in files) deleteRecursively(child)
- }
- }
- f.delete()
- }
-
- @Throws(IOException::class)
- fun calculateFileCrc32(file: File?): Long {
- return calculateCrc32(FileInputStream(file))
- }
-
- @Throws(IOException::class)
- fun calculateBytesCrc32(bytes: ByteArray?): Long {
- return calculateCrc32(ByteArrayInputStream(bytes))
- }
-
- @Throws(IOException::class)
- fun calculateCrc32(inputStream: InputStream): Long {
- inputStream.use { `in` ->
- val crc32 = CRC32()
- val buffer = ByteArray(1024 * 1024)
- var read: Int
- while (`in`.read(buffer).also { read = it } > 0) crc32.update(buffer, 0, read)
- return crc32.value
- }
- }
-
- fun writeStreamToStringBuilder(builder: StringBuilder, inputStream: InputStream?): Thread {
- val t = Thread {
- try {
- val buf = CharArray(1024)
- var len: Int
- val reader =
- BufferedReader(InputStreamReader(inputStream))
- while (reader.read(buf).also { len = it } > 0) builder.append(buf, 0, len)
- reader.close()
- } catch (e: Exception) {
- Log.wtf(TAG, e)
- }
- }
- t.start()
- return t
- }
-
- /**
- * Read contents of input stream to a byte array and close it
- *
- * @param inputStream
- * @return contents of input stream
- * @throws IOException
- */
- @Throws(IOException::class)
- fun readStream(inputStream: InputStream): ByteArray {
- inputStream.use { `in` -> return readStreamNoClose(`in`) }
- }
-
- @Throws(IOException::class)
- fun readStream(inputStream: InputStream, charset: Charset?): String {
- return String(readStream(inputStream), charset!!)
- }
-
- /**
- * Read contents of input stream to a byte array, but don't close the stream
- *
- * @param inputStream
- * @return contents of input stream
- * @throws IOException
- */
- @Throws(IOException::class)
- fun readStreamNoClose(inputStream: InputStream): ByteArray {
- val buffer = ByteArrayOutputStream()
- copyStream(inputStream, buffer)
- return buffer.toByteArray()
- }
-
- fun closeSilently(closeable: Closeable?) {
- if (closeable == null) return
- try {
- closeable.close()
- } catch (e: Exception) {
- Log.w(TAG, String.format("Unable to close %s", closeable.javaClass.canonicalName), e)
- }
- }
-
- /**
- * Hashes stream content using passed [MessageDigest], closes the stream and returns digest bytes
- *
- * @param inputStream
- * @param messageDigest
- * @return
- * @throws IOException
- */
- @Throws(IOException::class)
- fun hashStream(inputStream: InputStream?, messageDigest: MessageDigest): ByteArray {
- DigestInputStream(inputStream, messageDigest).use { digestInputStream ->
- val buffer = ByteArray(1024 * 64)
- var read: Int
- while (digestInputStream.read(buffer).also { read = it } > 0) {
- //Do nothing
- }
- return messageDigest.digest()
- }
- }
-
- @Throws(IOException::class)
- fun hashString(s: String, messageDigest: MessageDigest): ByteArray {
- return hashStream(
- ByteArrayInputStream(s.toByteArray(StandardCharsets.UTF_8)),
- messageDigest
- )
- }
-
- @Throws(IOException::class)
- fun readFile(file: File?): ByteArray {
- FileInputStream(file).use { `in` -> return readStream(`in`) }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/shizuku/Shell.kt b/app/src/main/java/li/songe/gkd/shizuku/Shell.kt
deleted file mode 100644
index 69d93c0..0000000
--- a/app/src/main/java/li/songe/gkd/shizuku/Shell.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package li.songe.gkd.shizuku
-
-import android.annotation.SuppressLint
-import java.io.InputStream
-import java.util.*
-
-
-interface Shell {
- val isAvailable: Boolean
-
- fun exec(command: Command): Result
- fun exec(command: Command, inputPipe: InputStream): Result
- fun makeLiteral(arg: String): String
- class Command(command: String, vararg args: String) {
- private val mArgs = mutableListOf()
- fun toStringArray(): Array {
- val array = arrayOfNulls(mArgs.size)
- for (i in mArgs.indices) array[i] = mArgs[i]
- return array
- }
-
- override fun toString(): String {
- val sb = StringBuilder()
- for (i in mArgs.indices) {
- val arg = mArgs[i]
- sb.append(arg)
- if (i < mArgs.size - 1) sb.append(" ")
- }
- return sb.toString()
- }
-
- class Builder(command: String, vararg args: String) {
- private val mCommand: Command = Command(command, *args)
- fun addArg(argument: String): Builder {
- mCommand.mArgs.add(argument)
- return this
- }
-
- fun build(): Command {
- return mCommand
- }
-
- }
-
- init {
- mArgs.add(command)
- mArgs.addAll(args)
- }
- }
-
- open class Result(
- var cmd: Command,
- var exitCode: Int,
- var out: String,
- var err: String
- ) {
- val isSuccessful: Boolean
- get() = exitCode == 0
-
- @SuppressLint("DefaultLocale")
- override fun toString(): String {
- return String.format(
- "Command: %s\nExit code: %d\nOut:\n%s\n=============\nErr:\n%s",
- cmd,
- exitCode,
- out,
- err
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/shizuku/ShizukuExt.kt b/app/src/main/java/li/songe/gkd/shizuku/ShizukuExt.kt
new file mode 100644
index 0000000..1d4fc51
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/shizuku/ShizukuExt.kt
@@ -0,0 +1,12 @@
+package li.songe.gkd.shizuku
+
+import android.content.pm.PackageManager
+import rikka.shizuku.Shizuku
+
+fun shizukuIsSafeOK(): Boolean {
+ return try {
+ Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
+ } catch (_: Exception) {
+ false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/shizuku/ShizukuShell.kt b/app/src/main/java/li/songe/gkd/shizuku/ShizukuShell.kt
deleted file mode 100644
index 7cd3906..0000000
--- a/app/src/main/java/li/songe/gkd/shizuku/ShizukuShell.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package li.songe.gkd.shizuku
-
-import android.os.Build
-import android.util.Log
-import rikka.shizuku.Shizuku
-import java.io.InputStream
-
-
-/**
- * https://github.com/Aefyr/SAI/blob/master/app/src/main/java/com/aefyr/sai/shell/ShizukuShell.java
- */
-class ShizukuShell private constructor() : Shell {
- override val isAvailable: Boolean
- get() = if (!Shizuku.pingBinder()) false else try {
- exec(Shell.Command("echo", "test")).isSuccessful
- } catch (e: Exception) {
- Log.w(TAG, "Unable to access shizuku: ")
- Log.w(TAG, e)
- false
- }
-
- override fun exec(command: Shell.Command): Shell.Result {
- return execInternal(command, null)
- }
-
- override fun exec(command: Shell.Command, inputPipe: InputStream): Shell.Result {
- return execInternal(command, inputPipe)
- }
-
- override fun makeLiteral(arg: String): String {
- return "'" + arg.replace("'", "'\\''") + "'"
- }
-
- private fun execInternal(command: Shell.Command, inputPipe: InputStream?): Shell.Result {
- val stdOutSb = StringBuilder()
- val stdErrSb = StringBuilder()
- return try {
- val shCommand = Shell.Command.Builder("sh", "-c", command.toString())
- val process = Shizuku.newProcess(shCommand.build().toStringArray(), null, null)
- val stdOutD: Thread = IOUtils.writeStreamToStringBuilder(stdOutSb, process.inputStream)
- val stdErrD: Thread = IOUtils.writeStreamToStringBuilder(stdErrSb, process.errorStream)
- if (inputPipe != null) {
- try {
- process.outputStream.use { outputStream ->
- inputPipe.use { inputStream ->
- IOUtils.copyStream(
- inputStream,
- outputStream
- )
- }
- }
- } catch (e: Exception) {
- stdOutD.interrupt()
- stdErrD.interrupt()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- process.destroyForcibly()
- } else {
- process.destroy()
- }
- throw RuntimeException(e)
- }
- }
- process.waitFor()
- stdOutD.join()
- stdErrD.join()
- Shell.Result(
- command,
- process.exitValue(),
- stdOutSb.toString().trim { it <= ' ' },
- stdErrSb.toString().trim { it <= ' ' })
- } catch (e: Exception) {
- Log.w(TAG, "Unable execute command: ")
- Log.w(TAG, e)
- Shell.Result(
- command, -1, stdOutSb.toString().trim { it <= ' ' },
- """$stdErrSb
-
- SAI ShizukuShell Java exception: ${Utils.throwableToString(e)}"""
- )
- }
- }
-
- companion object {
- private const val TAG = "ShizukuShell"
- val instance by lazy { ShizukuShell() }
- }
-
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/shizuku/ShizukuSystemServiceApi.kt b/app/src/main/java/li/songe/gkd/shizuku/ShizukuSystemServiceApi.kt
new file mode 100644
index 0000000..10fa42c
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/shizuku/ShizukuSystemServiceApi.kt
@@ -0,0 +1,19 @@
+package li.songe.gkd.shizuku
+
+
+import android.app.IActivityTaskManager
+import android.content.pm.IPackageManager
+import rikka.shizuku.ShizukuBinderWrapper
+import rikka.shizuku.SystemServiceHelper
+
+val activityTaskManager: IActivityTaskManager by lazy {
+ SystemServiceHelper.getSystemService("activity_task")
+ .let(::ShizukuBinderWrapper)
+ .let(IActivityTaskManager.Stub::asInterface)
+}
+
+val iPackageManager: IPackageManager by lazy {
+ SystemServiceHelper.getSystemService("package")
+ .let(::ShizukuBinderWrapper)
+ .let(IPackageManager.Stub::asInterface)
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/shizuku/Utils.kt b/app/src/main/java/li/songe/gkd/shizuku/Utils.kt
deleted file mode 100644
index 4a27e9e..0000000
--- a/app/src/main/java/li/songe/gkd/shizuku/Utils.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package li.songe.gkd.shizuku
-
-import java.io.PrintWriter
-import java.io.StringWriter
-
-
-object Utils {
- fun throwableToString(throwable: Throwable): String {
- val sw = StringWriter(1024)
- val pw = PrintWriter(sw)
-
- throwable.printStackTrace(pw)
- pw.close()
-
- return sw.toString()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/AboutPage.kt b/app/src/main/java/li/songe/gkd/ui/AboutPage.kt
new file mode 100644
index 0000000..2c6cf29
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/AboutPage.kt
@@ -0,0 +1,86 @@
+package li.songe.gkd.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Icon
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import li.songe.gkd.BuildConfig
+import li.songe.gkd.utils.SafeR
+
+@RootNavGraph
+@Destination
+@Composable
+fun AboutPage(navigator: DestinationsNavigator) {
+// val systemUiController = rememberSystemUiController()
+// val context = LocalContext.current as ComponentActivity
+// DisposableEffect(systemUiController) {
+// val oldVisible = systemUiController.isStatusBarVisible
+// systemUiController.isStatusBarVisible = false
+// WindowCompat.setDecorFitsSystemWindows(context.window, false)
+// onDispose {
+// systemUiController.isStatusBarVisible = oldVisible
+// WindowCompat.setDecorFitsSystemWindows(context.window, true)
+// }
+// }
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ backgroundColor = Color(0xfff8f9f9),
+ navigationIcon = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(id = SafeR.ic_back),
+ contentDescription = null,
+ modifier = Modifier
+ .size(30.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false),
+ ) {
+ navigator.popBackStack()
+ }
+ )
+ }
+ },
+ title = { Text(text = "关于") }
+ )
+ },
+ content = { contentPadding ->
+ Column(
+ Modifier
+ .padding(contentPadding)
+ .padding(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(text = "版本代码: " + BuildConfig.VERSION_CODE)
+ Text(text = "版本名称: " + BuildConfig.VERSION_NAME)
+ Text(text = "构建时间: " + BuildConfig.BUILD_DATE)
+ Text(text = "构建类型: " + BuildConfig.BUILD_TYPE)
+ }
+ }
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt
index 2b6e6c8..e3987ed 100644
--- a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt
@@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material.Switch
import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -30,103 +31,83 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.fade
import com.google.accompanist.placeholder.material.placeholder
-import kotlinx.coroutines.delay
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import kotlinx.serialization.encodeToString
+import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.SubscriptionRaw
-import li.songe.gkd.db.table.SubsConfig
-import li.songe.gkd.db.util.Operator.eq
-import li.songe.gkd.db.util.RoomX
-import li.songe.gkd.ui.component.StatusBar
-import li.songe.gkd.util.ThrottleState
-import li.songe.router.LocalRoute
-import li.songe.router.LocalRouter
-import li.songe.router.Page
-
-data class AppItemPageParams(
- val subsApp: SubscriptionRaw.AppRaw,
- val subsConfig: SubsConfig,
- val appName: String,
-)
-
-val AppItemPage = Page {
-
-// https://developer.android.com/jetpack/compose/modifiers-list
-
- val router = LocalRouter.current
-
- val params = LocalRoute.current.data as AppItemPageParams
+import li.songe.gkd.data.getAppInfo
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.utils.Singleton
+import li.songe.gkd.utils.launchAsFn
+@RootNavGraph
+@Destination
+@Composable
+fun AppItemPage(
+ subsApp: SubscriptionRaw.AppRaw,
+ subsConfig: SubsConfig,
+) {
val scope = rememberCoroutineScope()
-// val context = LocalContext.current
- var subsConfigList: List? by remember { mutableStateOf(null) }
-
- val changeItemThrottle = ThrottleState.use(scope)
+ var subsConfigs: List? by remember { mutableStateOf(null) }
LaunchedEffect(Unit) {
- delay(400)
- val config = params.subsConfig
- val mutableSet =
- RoomX.select { (SubsConfig::type eq SubsConfig.GroupType) and (SubsConfig::subsItemId eq config.subsItemId) and (SubsConfig::appId eq config.appId) }
- .toMutableSet()
+ val mutableSet = DbSet.subsConfigDao.queryGroupTypeConfig(subsConfig.subsItemId, subsApp.id)
val list = mutableListOf()
- params.subsApp.groups.forEach { group ->
+ subsApp.groups.forEach { group ->
if (group.key == null) {
list.add(null)
} else {
- val item = mutableSet.find { s -> s.groupKey == group.key } ?: SubsConfig(
- subsItemId = config.subsItemId,
- appId = config.appId,
- groupKey = group.key,
- type = SubsConfig.GroupType
- )
+ val item = mutableSet.find { s -> s.groupKey == group.key }
+ ?: SubsConfig(
+ subsItemId = subsConfig.subsItemId,
+ appId = subsConfig.appId,
+ groupKey = group.key,
+ type = SubsConfig.GroupType
+ )
list.add(item)
}
}
- subsConfigList = list
+ subsConfigs = list
}
+ var showGroupItem: SubscriptionRaw.GroupRaw? by remember { mutableStateOf(null) }
+
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
- Column {
- StatusBar()
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(10.dp, 0.dp)
- ) {
- Text(
- text = params.appName,
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis,
- )
- Spacer(modifier = Modifier.width(10.dp))
- Text(
- text = params.subsApp.id,
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis,
- )
- }
- Spacer(modifier = Modifier.height(10.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp, 0.dp)
+ ) {
+ Text(
+ text = getAppInfo(subsApp.id).name ?: "-",
+ maxLines = 1,
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(
+ text = subsApp.id,
+ maxLines = 1,
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis,
+ )
}
+ Spacer(modifier = Modifier.height(10.dp))
}
- items(params.subsApp.groups.size) { i ->
- val group = params.subsApp.groups[i]
+ items(subsApp.groups.size) { i ->
+ val group = subsApp.groups[i]
Row(
modifier = Modifier
.clickable {
-// router.navigate(
-// GroupItemPage, GroupItemPage.Params(
-// group = group,
-// subsConfig = subsConfigList?.get(i),
-// appName = params.appName
-// )
-// )
+ showGroupItem = group
}
.padding(10.dp, 6.dp)
.fillMaxWidth()
@@ -149,7 +130,7 @@ val AppItemPage = Page {
.fillMaxWidth()
)
Text(
- text = group.activityIds?.joinToString() ?: "",
+ text = group.desc ?: "-",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
@@ -163,10 +144,10 @@ val AppItemPage = Page {
if (group.key != null) {
val crPx = with(LocalDensity.current) { 4.dp.toPx() }
Switch(
- checked = subsConfigList?.get(i)?.enable ?: true,
+ checked = subsConfigs?.get(i)?.enable != false,
modifier = Modifier
.placeholder(
- subsConfigList == null,
+ subsConfigs == null,
highlight = PlaceholderHighlight.fade(),
shape = GenericShape { size, _ ->
val cr = CornerRadius(crPx, crPx)
@@ -184,16 +165,12 @@ val AppItemPage = Page {
)
}
),
-// 当 onCheckedChange 是 null 时, size 是长方形, 反之是 正方形
- onCheckedChange = changeItemThrottle.invoke { enable ->
- val list = subsConfigList ?: return@invoke
- val newItem = list[i]?.copy(enable = enable) ?: return@invoke
- if (newItem.id == 0L) {
- RoomX.insert(newItem)
- } else {
- RoomX.update(newItem)
- }
- subsConfigList = list.toMutableList().apply {
+ onCheckedChange = scope.launchAsFn { enable ->
+ val subsConfigsVal = subsConfigs ?: return@launchAsFn
+ val newItem =
+ subsConfigsVal[i]?.copy(enable = enable) ?: return@launchAsFn
+ DbSet.subsConfigDao.insert(newItem)
+ subsConfigs = subsConfigsVal.toMutableList().apply {
set(i, newItem)
}
}
@@ -210,5 +187,15 @@ val AppItemPage = Page {
}
}
}
+
+
+ showGroupItem?.let { showGroupItemVal ->
+ Dialog(onDismissRequest = { showGroupItem = null }) {
+ Text(
+ text = Singleton.json.encodeToString(showGroupItemVal),
+ modifier = Modifier.width(400.dp)
+ )
+ }
+ }
}
diff --git a/app/src/main/java/li/songe/gkd/ui/DebugPage.kt b/app/src/main/java/li/songe/gkd/ui/DebugPage.kt
deleted file mode 100644
index d94da46..0000000
--- a/app/src/main/java/li/songe/gkd/ui/DebugPage.kt
+++ /dev/null
@@ -1,151 +0,0 @@
-package li.songe.gkd.ui
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.media.projection.MediaProjectionManager
-import android.net.Uri
-import android.provider.Settings
-import androidx.activity.ComponentActivity
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.selection.SelectionContainer
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Text
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import androidx.core.content.ContextCompat
-import com.blankj.utilcode.util.ToastUtils
-import com.dylanc.activityresult.launcher.launchForResult
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import li.songe.gkd.accessibility.GkdAbService
-import li.songe.gkd.debug.FloatingService
-import li.songe.gkd.debug.ScreenshotService
-import li.songe.gkd.debug.HttpService
-import li.songe.gkd.ui.component.StatusBar
-import li.songe.gkd.ui.component.TextSwitch
-import li.songe.gkd.util.Ext
-import li.songe.gkd.util.Ext.LocalLauncher
-import li.songe.gkd.util.Ext.usePollState
-import li.songe.gkd.util.Storage
-import li.songe.router.Page
-
-
-val DebugPage = Page {
- val context = LocalContext.current as ComponentActivity
- val launcher = LocalLauncher.current
- val scope = rememberCoroutineScope()
-
- val httpServerRunning by usePollState { HttpService.isRunning() }
- val screenshotRunning by usePollState { ScreenshotService.isRunning() }
- val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
- val floatingRunning by usePollState {
- FloatingService.isRunning() && Settings.canDrawOverlays(
- context
- )
- }
-
-
- val debugAvailable by remember {
- derivedStateOf { httpServerRunning }
- }
-
- val serverUrl by remember {
- derivedStateOf {
- if (debugAvailable) {
- Ext.getIpAddressInLocalNetwork()
- .map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
- .joinToString("\n")
- } else {
- null
- }
- }
- }
-
- Column(
- modifier = Modifier
- .verticalScroll(
- state = rememberScrollState()
- )
- .padding(20.dp)
- ) {
- StatusBar()
-
- Text("调试模式需要WIFI和另一台设备\n您可以一台设备开热点,另一台设备连入\n满足以上外部条件后, 本机需要开启以下服务")
-
- TextSwitch("HTTP服务(需WIFI)", httpServerRunning) {
- if (it) {
- HttpService.start()
- } else {
- HttpService.stop()
- }
- }
-
- TextSwitch("截屏服务", screenshotRunning) {
- if (it) {
- scope.launch {
- val mediaProjectionManager =
- context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
- val activityResult =
- launcher.launchForResult(mediaProjectionManager.createScreenCaptureIntent())
- if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) {
- ScreenshotService.start(intent = activityResult.data!!)
- }
- }
- } else {
- ScreenshotService.stop()
- }
- }
-
- TextSwitch("无障碍服务", gkdAccessRunning) {
- if (it) {
- scope.launch {
- ToastUtils.showShort("请先启动无障碍服务")
- delay(500)
- val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- context.startActivity(intent)
- }
- } else {
- ToastUtils.showLong("无障碍服务不可在调试模式中关闭")
- }
- }
-
- TextSwitch("悬浮窗服务", floatingRunning) {
- if (it) {
- if (Settings.canDrawOverlays(context)) {
- val intent = Intent(context, FloatingService::class.java)
- ContextCompat.startForegroundService(context, intent)
- } else {
- val intent = Intent(
- Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
- Uri.parse("package:$context.packageName")
- )
- launcher.launch(intent) { resultCode, _ ->
- if (resultCode != ComponentActivity.RESULT_OK) return@launch
- if (!Settings.canDrawOverlays(context)) return@launch
- val intent1 = Intent(context, FloatingService::class.java)
- ContextCompat.startForegroundService(context, intent1)
- }
- }
- } else {
- FloatingService.stop(context)
- }
- }
-
- if (debugAvailable && serverUrl != null) {
- Text("调试模式可用, 请使用同一局域网的另一台设备打开链接")
- SelectionContainer {
- Text("长按可复制: " + serverUrl!!)
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt b/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
new file mode 100644
index 0000000..3427c78
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
@@ -0,0 +1,50 @@
+package li.songe.gkd.ui
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import com.blankj.utilcode.util.ToastUtils
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import li.songe.gkd.utils.LaunchedEffectTry
+
+@RootNavGraph
+@Destination
+@Composable
+fun ImagePreviewPage(
+ filePath: String?
+) {
+ val context = LocalContext.current as ComponentActivity
+ val scope = rememberCoroutineScope()
+ var bitmap by remember {
+ mutableStateOf(null)
+ }
+ LaunchedEffectTry {
+ if (filePath != null) {
+ bitmap = withContext(IO) { BitmapFactory.decodeFile(filePath) }
+ } else {
+ ToastUtils.showShort("图片路径缺失")
+ }
+ }
+
+ bitmap?.let { bitmapVal ->
+ Image(
+ bitmap = bitmapVal.asImageBitmap(),
+ contentDescription = null,
+ Modifier.fillMaxWidth()
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/RecordPage.kt b/app/src/main/java/li/songe/gkd/ui/RecordPage.kt
new file mode 100644
index 0000000..80d4244
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/RecordPage.kt
@@ -0,0 +1,13 @@
+package li.songe.gkd.ui
+
+import androidx.compose.runtime.Composable
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import li.songe.gkd.db.DbSet
+
+@RootNavGraph
+@Destination
+@Composable
+fun RecordPage() {
+ DbSet.triggerLogDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt b/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
new file mode 100644
index 0000000..f2f05eb
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
@@ -0,0 +1,163 @@
+package li.songe.gkd.ui
+
+import android.content.Intent
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.core.content.FileProvider
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.withContext
+import li.songe.gkd.data.Snapshot
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.debug.SnapshotExt
+import li.songe.gkd.ui.component.StatusBar
+import li.songe.gkd.utils.launchAsFn
+import li.songe.gkd.utils.Singleton
+
+@RootNavGraph
+@Destination
+@Composable
+fun SnapshotPage() {
+ val context = LocalContext.current as ComponentActivity
+ val scope = rememberCoroutineScope()
+
+ var snapshots by remember {
+ mutableStateOf(listOf())
+ }
+ var selectedSnapshot by remember {
+ mutableStateOf(null)
+ }
+ LaunchedEffect(Unit) {
+ DbSet.snapshotDao.query().flowOn(Dispatchers.IO).collect {
+ snapshots = it.reversed()
+ }
+ }
+ LazyColumn(
+ modifier = Modifier.padding(10.dp, 0.dp, 10.dp, 0.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ item {
+ Text(text = "存在 ${snapshots.size} 条快照记录")
+ }
+ items(snapshots.size) { i ->
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(BorderStroke(1.dp, Color.Black))
+ .clickable {
+ selectedSnapshot = snapshots[i]
+ }
+ ) {
+ Row {
+ Text(
+ text = Singleton.simpleDateFormat.format(snapshots[i].id),
+ fontFamily = FontFamily.Monospace
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = snapshots[i].appName ?: "")
+ }
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = snapshots[i].appId ?: "")
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(text = snapshots[i].activityId ?: "")
+ }
+ }
+ item {
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ selectedSnapshot?.let { snapshot ->
+ Dialog(
+ onDismissRequest = { selectedSnapshot = null }
+ ) {
+ Box(
+ Modifier
+ .width(200.dp)
+ .background(Color.White)
+ .padding(8.dp)
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ val modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ Text(
+ text = "查看", modifier = Modifier
+ .clickable(onClick = scope.launchAsFn {
+// router.navigate(
+// ImagePreviewPage,
+// SnapshotExt.getScreenshotPath(snapshot.id)
+// )
+ selectedSnapshot = null
+ })
+ .then(modifier)
+ )
+ Text(
+ text = "分享", modifier = Modifier
+ .clickable(onClick = scope.launchAsFn {
+ val zipFile = SnapshotExt.getSnapshotZipFile(snapshot.id)
+ val uri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.provider",
+ zipFile
+ )
+ val intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_STREAM, uri)
+ type = "application/zip"
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(Intent.createChooser(intent, "分享zip文件"))
+ })
+ .then(modifier)
+ )
+ Text(
+ text = "删除", modifier = Modifier
+ .clickable(onClick = scope.launchAsFn {
+ DbSet.snapshotDao.delete(snapshot)
+ withContext(IO) {
+ SnapshotExt.remove(snapshot.id)
+ }
+ selectedSnapshot = null
+ })
+ .then(modifier)
+ )
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt
index b35e97a..f896535 100644
--- a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt
@@ -1,162 +1,96 @@
package li.songe.gkd.ui
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
-import androidx.core.content.res.ResourcesCompat
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.fade
import com.google.accompanist.placeholder.material.placeholder
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.ramcosta.composedestinations.navigation.navigate
import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.cancellable
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.withContext
-import li.songe.gkd.R
+import li.songe.gkd.data.SubsConfig
+import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.SubscriptionRaw
-import li.songe.gkd.db.table.SubsConfig
-import li.songe.gkd.db.table.SubsItem
-import li.songe.gkd.db.util.Operator.eq
-import li.songe.gkd.db.util.RoomX
-import li.songe.gkd.ui.component.StatusBar
+import li.songe.gkd.data.getAppInfo
+import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.SubsAppCard
import li.songe.gkd.ui.component.SubsAppCardData
-import li.songe.gkd.util.Ext.getApplicationInfoExt
-import li.songe.gkd.util.Status
-import li.songe.gkd.util.ThrottleState
-import li.songe.router.LocalRoute
-import li.songe.router.LocalRouter
-import li.songe.router.Page
-import java.io.File
-
-
-val SubsPage = Page {
- val router = LocalRouter.current
- val subsItem = LocalRoute.current.data as SubsItem
+import li.songe.gkd.ui.destinations.AppItemPageDestination
+import li.songe.gkd.utils.LaunchedEffectTry
+import li.songe.gkd.utils.LocalNavController
+import li.songe.gkd.utils.launchAsFn
+import li.songe.gkd.utils.rememberCache
+import li.songe.gkd.utils.useTask
+@RootNavGraph
+@Destination
+@Composable
+fun SubsPage(
+ subsItem: SubsItem
+) {
val scope = rememberCoroutineScope()
- val context = LocalContext.current
+ val navController = LocalNavController.current
- var sub: SubscriptionRaw? by remember { mutableStateOf(null) }
- var subStatus: Status by remember { mutableStateOf(Status.Progress()) }
- var subsAppCardDataList: List? by remember { mutableStateOf(null) }
- val placeholderList: List = remember {
- mutableListOf().apply {
- repeat(5) {
- add(
- SubsAppCardData(
- appName = "" + it,
- icon = ResourcesCompat.getDrawable(
- context.resources,
- R.drawable.ic_app_2,
- context.theme
- )!!,
- subsConfig = SubsConfig(
- subsItemId = it.toLong(),
- appId = "" + it
- )
- )
- )
- }
- }
- }
- var subsAppCardDataListStatus: Status> by remember {
- mutableStateOf(Status.Progress())
- }
+ var sub: SubscriptionRaw? by rememberCache { mutableStateOf(null) }
+ var subsAppCards: List? by rememberCache { mutableStateOf(null) }
- val changeItemThrottle = ThrottleState.use(scope)
-
- LaunchedEffect(Unit) {
- val st = System.currentTimeMillis()
- val file = File(subsItem.filePath)
- if (!(file.exists() && file.isFile)) {
- subStatus = Status.Error("在本地存储没有找到订阅文件")
- return@LaunchedEffect
- }
- val rawText = try {
- withContext(IO) { file.readText() }
- } catch (e: Exception) {
- subStatus = Status.Error("读取文件失败:$e")
- return@LaunchedEffect
- }
- val newSub = try {
- SubscriptionRaw.parse5(rawText)
- } catch (e: Exception) {
- subStatus = Status.Error("序列化失败:$e")
- return@LaunchedEffect
- }
- subStatus = Status.Success(newSub)
-
- val mutableSet =
- RoomX.select { (SubsConfig::type eq SubsConfig.AppType) and (SubsConfig::subsItemId eq subsItem.id) }
- .toMutableSet()
-
- val packageManager = context.packageManager
- val defaultIcon = ResourcesCompat.getDrawable(
- context.resources,
- R.drawable.ic_app_2,
- context.theme
- )!!
- val defaultName = "-"
- val newSubsAppCardDataList = (subStatus as Status.Success).value.apps.map { appRaw ->
- mutableSet.firstOrNull { v ->
- v.appId == appRaw.id
- }.apply {
- if (this != null) {
- mutableSet.remove(this)
+ LaunchedEffectTry(Unit) {
+ scope.launchAsFn { }
+ val newSub = if (sub === null) {
+ SubscriptionRaw.parse5(subsItem.subsFile.readText()).apply {
+ withContext(IO) {
+ apps.forEach {
+ getAppInfo(it.id)
+ }
}
- } ?: SubsConfig(
- subsItemId = subsItem.id,
- appId = appRaw.id,
- type = SubsConfig.AppType
- )
- }.map { subsConfig ->
- async(IO) {
- val info: ApplicationInfo = try {
- packageManager.getApplicationInfoExt(
- subsConfig.appId,
- PackageManager.GET_META_DATA
- )
- } catch (e: Exception) {
- return@async SubsAppCardData(
- defaultName,
- defaultIcon,
- subsConfig
- )
- }
- return@async SubsAppCardData(
- packageManager.getApplicationLabel(info).toString(),
- packageManager.getApplicationIcon(info),
- subsConfig
- )
}
- }.awaitAll()
- subsAppCardDataListStatus = Status.Success(newSubsAppCardDataList)
- delay(400 - (System.currentTimeMillis() - st))
+ } else {
+ sub!!
+ }
sub = newSub
- subsAppCardDataList = newSubsAppCardDataList
+ DbSet.subsConfigDao.queryAppTypeConfig(subsItem.id).flowOn(IO).cancellable().collect {
+ val mutableSet = it.toMutableSet()
+ val newSubsAppCards = newSub.apps.map { appRaw ->
+ mutableSet.firstOrNull { v ->
+ v.appId == appRaw.id
+ }.apply {
+ mutableSet.remove(this)
+ } ?: SubsConfig(
+ subsItemId = subsItem.id,
+ appId = appRaw.id,
+ type = SubsConfig.AppType
+ )
+ }.mapIndexed { index, subsConfig ->
+ SubsAppCardData(
+ subsConfig,
+ newSub.apps[index]
+ )
+ }
+ subsAppCards = newSubsAppCards
+ }
+ }
+
+ val openAppPage = scope.useTask().launchAsFn {
+ navController.navigate(AppItemPageDestination(it.appRaw, it.subsConfig))
}
LazyColumn(
@@ -165,113 +99,58 @@ val SubsPage = Page {
.fillMaxSize()
) {
item {
- Column {
- StatusBar()
- val textModifier = Modifier
- .fillMaxWidth()
- .placeholder(visible = sub == null, highlight = PlaceholderHighlight.fade())
- Column(
- modifier = Modifier.padding(10.dp, 0.dp),
- verticalArrangement = Arrangement.spacedBy(5.dp)
- ) {
- Text(
- text = "作者: " + (sub?.author ?: "未知"),
- modifier = textModifier
- )
- Text(
- text = "版本: ${sub?.version}",
- modifier = textModifier
- )
- Text(
- text = "描述: ${sub?.name}",
- modifier = textModifier
- )
- }
+ val textModifier = Modifier
+ .fillMaxWidth()
+ .placeholder(visible = sub == null, highlight = PlaceholderHighlight.fade())
+ Column(
+ modifier = Modifier.padding(10.dp, 0.dp),
+ verticalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ Text(
+ text = "作者: " + (sub?.author ?: "未知"),
+ modifier = textModifier
+ )
+ Text(
+ text = "版本: ${sub?.version}",
+ modifier = textModifier
+ )
+ Text(
+ text = "描述: ${sub?.name}",
+ modifier = textModifier
+ )
}
}
- subsAppCardDataList?.let { cardDataList ->
- items(cardDataList.size) { i ->
- AnimatedVisibility(visible = true) {
- Box(modifier = Modifier
- .wrapContentSize()
- .clickable {
- router.navigate(
- AppItemPage,
- AppItemPageParams(
- sub?.apps?.get(i)!!,
- cardDataList[i].subsConfig,
- cardDataList[i].appName
- )
- )
- }) {
- SubsAppCard(
- sub = cardDataList[i],
- onValueChange = changeItemThrottle.invoke { enable ->
- val newItem = cardDataList[i].subsConfig.copy(
- enable = enable
- )
- if (newItem.id == 0L) {
- RoomX.insert(newItem)
- } else {
- RoomX.update(newItem)
- }
- subsAppCardDataList = cardDataList.toMutableList().apply {
- set(i, cardDataList[i].copy(subsConfig = newItem))
- }
- }
+ subsAppCards?.let { subsAppCardsVal ->
+ items(subsAppCardsVal.size) { i ->
+ SubsAppCard(
+ sub = subsAppCardsVal[i],
+ onClick = {
+ openAppPage(subsAppCardsVal[i])
+ },
+ onValueChange = scope.launchAsFn { enable ->
+ val newItem = subsAppCardsVal[i].subsConfig.copy(
+ enable = enable
)
+ DbSet.subsConfigDao.insert(newItem)
}
- }
- }
- }
- if (subsAppCardDataList == null) {
- items(placeholderList.size) { i ->
- Box(
- modifier = Modifier
- .wrapContentSize()
- ) {
- SubsAppCard(loading = true, sub = placeholderList[i])
- Text(text = "")
- }
+ )
}
}
+// if (subsAppCards == null) {
+// items(placeholderList.size) { i ->
+// Box(
+// modifier = Modifier
+// .wrapContentSize()
+// ) {
+// SubsAppCard(loading = true, sub = placeholderList[i])
+// Text(text = "")
+// }
+// }
+// }
item(true) {
Spacer(modifier = Modifier.height(10.dp))
}
-
}
-
- if (subStatus !is Status.Success || subsAppCardDataListStatus !is Status.Success) {
- when (val s = subStatus) {
- is Status.Success -> {
- when (val s2 = subsAppCardDataListStatus) {
- is Status.Error -> {
- Dialog(onDismissRequest = { router.back() }) {
- Text(text = s2.value.toString())
- }
- }
-
- is Status.Progress -> {
- }
-
- else -> {}
- }
- }
-
- is Status.Error -> {
- Dialog(onDismissRequest = { router.back() }) {
- Text(text = s.value.toString())
- }
- }
-
- is Status.Progress -> {
- }
-
- else -> {}
- }
- }
-
-
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/component/SnapshotCard.kt b/app/src/main/java/li/songe/gkd/ui/component/SnapshotCard.kt
new file mode 100644
index 0000000..94ca60e
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/ui/component/SnapshotCard.kt
@@ -0,0 +1,32 @@
+package li.songe.gkd.ui.component
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun SnapshotCard() {
+ Row {
+ Text(text = "06-02 20:47:48")
+ Spacer(modifier = Modifier.size(10.dp))
+ Text(text = "酷安")
+ Spacer(modifier = Modifier.size(10.dp))
+ Text(text = "查看")
+ Spacer(modifier = Modifier.size(10.dp))
+ Text(text = "分享")
+ Spacer(modifier = Modifier.size(10.dp))
+ Text(text = "删除")
+ }
+}
+
+
+@Preview
+@Composable
+fun PreviewSnapshotCard() {
+ SnapshotCard()
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/component/StatusBar.kt b/app/src/main/java/li/songe/gkd/ui/component/StatusBar.kt
index bd48987..e3ec271 100644
--- a/app/src/main/java/li/songe/gkd/ui/component/StatusBar.kt
+++ b/app/src/main/java/li/songe/gkd/ui/component/StatusBar.kt
@@ -12,7 +12,7 @@ import com.blankj.utilcode.util.BarUtils
import com.blankj.utilcode.util.ConvertUtils
@Composable
-fun StatusBar(color: Color = Color.White) {
+fun StatusBar(color: Color = Color.Transparent) {
Spacer(
modifier = Modifier
.height(statusBarHeight)
diff --git a/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt b/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt
index edbe132..accae8d 100644
--- a/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt
+++ b/app/src/main/java/li/songe/gkd/ui/component/SubsAppCard.kt
@@ -2,6 +2,7 @@ package li.songe.gkd.ui.component
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -18,31 +19,43 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.fade
import com.google.accompanist.placeholder.material.placeholder
-import li.songe.gkd.db.table.SubsConfig
+import li.songe.gkd.R
+import li.songe.gkd.data.SubsConfig
+import li.songe.gkd.data.SubscriptionRaw
+import li.songe.gkd.data.getAppInfo
+import li.songe.gkd.utils.SafeR
@Composable
fun SubsAppCard(
loading: Boolean = false,
sub: SubsAppCardData,
+ onClick: (() -> Unit)? = null,
onValueChange: ((Boolean) -> Unit)? = null
) {
+ val info = getAppInfo(sub.appRaw.id)
Row(
modifier = Modifier
.height(60.dp)
+ .clickable {
+ onClick?.invoke()
+ }
.padding(4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
- painter = rememberDrawablePainter(sub.icon),
+ painter = if (info.icon != null) rememberDrawablePainter(info.icon) else painterResource(
+ SafeR.ic_app_2
+ ),
contentDescription = null,
modifier = Modifier
.fillMaxHeight()
@@ -60,7 +73,7 @@ fun SubsAppCard(
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
- text = sub.appName, maxLines = 1,
+ text = info.name ?: "-", maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
@@ -68,7 +81,7 @@ fun SubsAppCard(
.placeholder(loading, highlight = PlaceholderHighlight.fade())
)
Text(
- text = sub.subsConfig.appId, maxLines = 1,
+ text = sub.appRaw.groups.size.toString() + "组规则", maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
@@ -88,8 +101,7 @@ fun SubsAppCard(
}
data class SubsAppCardData(
- val appName: String,
- val icon: Drawable,
val subsConfig: SubsConfig,
+ val appRaw: SubscriptionRaw.AppRaw
)
diff --git a/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt b/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt
index 69b543a..4b033fe 100644
--- a/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt
+++ b/app/src/main/java/li/songe/gkd/ui/component/SubsItemCard.kt
@@ -21,15 +21,14 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import li.songe.gkd.R
-import li.songe.gkd.db.table.SubsItem
-import li.songe.gkd.util.Singleton
+import li.songe.gkd.data.SubsItem
+import li.songe.gkd.utils.SafeR
+import li.songe.gkd.utils.Singleton
@Composable
fun SubsItemCard(
subsItem: SubsItem,
onShareClick: (() -> Unit)? = null,
- onEditClick: (() -> Unit)? = null,
onDelClick: (() -> Unit)? = null,
onRefreshClick: (() -> Unit)? = null,
) {
@@ -49,16 +48,25 @@ fun SubsItemCard(
softWrap = false,
overflow = TextOverflow.Ellipsis
)
- Text(
- text = dateStr,
- maxLines = 1,
- softWrap = false,
- overflow = TextOverflow.Ellipsis
- )
+ Row {
+ Text(
+ text = dateStr,
+ maxLines = 1,
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(
+ text = "版本:" + subsItem.version,
+ maxLines = 1,
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
}
Spacer(modifier = Modifier.width(5.dp))
Image(
- painter = painterResource(R.drawable.ic_refresh),
+ painter = painterResource(SafeR.ic_refresh),
contentDescription = "refresh",
modifier = Modifier
.clickable {
@@ -69,7 +77,7 @@ fun SubsItemCard(
)
Spacer(modifier = Modifier.width(5.dp))
Image(
- painter = painterResource(R.drawable.ic_share),
+ painter = painterResource(SafeR.ic_share),
contentDescription = "share",
modifier = Modifier
.clickable {
@@ -80,18 +88,7 @@ fun SubsItemCard(
)
Spacer(modifier = Modifier.width(5.dp))
Image(
- painter = painterResource(R.drawable.ic_create_round),
- contentDescription = "edit",
- modifier = Modifier
- .clickable {
- onEditClick?.invoke()
- }
- .padding(4.dp)
- .size(20.dp)
- )
- Spacer(modifier = Modifier.width(5.dp))
- Image(
- painter = painterResource(R.drawable.ic_del),
+ painter = painterResource(SafeR.ic_del),
contentDescription = "edit",
modifier = Modifier
.clickable {
@@ -109,7 +106,6 @@ fun PreviewSubscriptionItemCard() {
Surface(modifier = Modifier.width(300.dp)) {
SubsItemCard(
SubsItem(
- filePath = "filepath",
updateUrl = "https://raw.githubusercontents.com/lisonge/gkd-subscription/main/src/ad-startup.gkd.json",
name = "APP工具箱"
)
diff --git a/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt b/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt
index 7c3e380..c9dd3a0 100644
--- a/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt
+++ b/app/src/main/java/li/songe/gkd/ui/component/TextSwitch.kt
@@ -1,33 +1,41 @@
package li.songe.gkd.ui.component
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
-import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
@Composable
fun TextSwitch(
- text: String,
- checked: Boolean,
+ name: String = "",
+ desc: String = "",
+ checked: Boolean = true,
onCheckedChange: ((Boolean) -> Unit)? = null,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
- val animatedColor = (
- Color(
- 0,
- 0,
- 0,
- (0xFF * (if (checked) 1f else .3f)).toInt()
- )
- )
- Text(
- text,
- color = animatedColor
- )
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ name,
+ fontSize = 18.sp
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ desc,
+ fontSize = 14.sp
+ )
+ }
+ Spacer(modifier = Modifier.width(10.dp))
Switch(
checked,
onCheckedChange,
@@ -38,7 +46,7 @@ fun TextSwitch(
@Preview
@Composable
fun PreviewTextSwitch() {
- Surface {
- TextSwitch("text", true)
+ Surface(modifier = Modifier.width(300.dp)) {
+ TextSwitch("隐藏后台", "在最近任务列表中隐藏", true)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/BottomNavItem.kt b/app/src/main/java/li/songe/gkd/ui/home/BottomNavItem.kt
index 2de4955..18ca73d 100644
--- a/app/src/main/java/li/songe/gkd/ui/home/BottomNavItem.kt
+++ b/app/src/main/java/li/songe/gkd/ui/home/BottomNavItem.kt
@@ -2,6 +2,7 @@ package li.songe.gkd.ui.home
import androidx.annotation.DrawableRes
import li.songe.gkd.R
+import li.songe.gkd.utils.SafeR
data class BottomNavItem(
val label: String,
@@ -13,12 +14,12 @@ data class BottomNavItem(
val BottomNavItems = listOf(
BottomNavItem(
label = "订阅",
- icon = R.drawable.ic_link,
+ icon = SafeR.ic_link,
route = "subscription"
),
BottomNavItem(
label = "设置",
- icon = R.drawable.ic_cog,
+ icon = SafeR.ic_cog,
route = "settings"
),
)
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/HomePage.kt b/app/src/main/java/li/songe/gkd/ui/home/HomePage.kt
index afee139..7c3d106 100644
--- a/app/src/main/java/li/songe/gkd/ui/home/HomePage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/home/HomePage.kt
@@ -1,47 +1,33 @@
package li.songe.gkd.ui.home
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.zIndex
-import li.songe.gkd.ui.component.StatusBar
-import li.songe.gkd.util.ModifierExt.noRippleClickable
-import li.songe.router.Page
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import li.songe.gkd.utils.LocalStateCache
+import li.songe.gkd.utils.StateCache
+import li.songe.gkd.utils.rememberCache
-val HomePage = Page {
- var tabInt by remember { mutableStateOf(0) }
- Column(modifier = Modifier.fillMaxSize()) {
- StatusBar()
- Scaffold(
- bottomBar = { BottomNavigationBar(tabInt) { tabInt = it } },
- content = { padding ->
- Box(modifier = Modifier.padding(padding)) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .alpha(if (tabInt == 0) 1f else 0f)
- .zIndex(if (tabInt == 0) 1f else 0f)
- .noRippleClickable { }) {
- SubscriptionManagePage()
- }
- Box(
- modifier = Modifier
- .fillMaxSize()
- .alpha(if (tabInt == 1) 1f else 0f)
- .zIndex(if (tabInt == 1) 1f else 0f)
- .noRippleClickable { }) {
- SettingsPage()
- }
- }
+@RootNavGraph(start = true)
+@Destination
+@Composable
+fun HomePage() {
+ var tabIndex by rememberCache { mutableStateOf(0) }
+ val subsStateCache = rememberCache { StateCache() }
+ val settingStateCache = rememberCache { StateCache() }
+ Scaffold(bottomBar = { BottomNavigationBar(tabIndex) { tabIndex = it } }, content = { padding ->
+ Box(modifier = Modifier.padding(padding)) {
+ when (tabIndex) {
+ 0 -> CompositionLocalProvider(LocalStateCache provides subsStateCache) { SubscriptionManagePage() }
+ 1 -> CompositionLocalProvider(LocalStateCache provides settingStateCache) { SettingsPage() }
}
- )
- }
+ }
+ })
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/NativePage.kt b/app/src/main/java/li/songe/gkd/ui/home/NativePage.kt
index 6cd0756..8c41cad 100644
--- a/app/src/main/java/li/songe/gkd/ui/home/NativePage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/home/NativePage.kt
@@ -17,6 +17,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import li.songe.gkd.R
+import li.songe.gkd.utils.SafeR
@Composable
fun NativePage() {
@@ -28,7 +29,7 @@ fun NativePage() {
modifier = Modifier.height(40.dp)
) {
Image(
- painter = painterResource(R.drawable.ic_app_2),
+ painter = painterResource(SafeR.ic_app_2),
contentDescription = "",
modifier = Modifier
.fillMaxHeight()
diff --git a/app/src/main/java/li/songe/gkd/ui/home/Nav.kt b/app/src/main/java/li/songe/gkd/ui/home/Nav.kt
index 9f85797..a5c3ce2 100644
--- a/app/src/main/java/li/songe/gkd/ui/home/Nav.kt
+++ b/app/src/main/java/li/songe/gkd/ui/home/Nav.kt
@@ -1,5 +1,6 @@
package li.songe.gkd.ui.home
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
@@ -43,11 +44,13 @@ import androidx.compose.ui.unit.dp
@Composable
fun BottomNavigationBar(tabInt: Int, onTabChange: ((Int) -> Unit)? = null) {
BottomNavigation(
- backgroundColor = Color.White,
+ backgroundColor = Color.Transparent,
+ elevation = 0.dp
) {
BottomNavItems.forEachIndexed { i, navItem ->
BottomNavigationItem(
selected = i == tabInt,
+ modifier = Modifier.background(Color.Transparent),
onClick = {
onTabChange?.invoke(i)
},
diff --git a/app/src/main/java/li/songe/gkd/ui/home/SettingsPage.kt b/app/src/main/java/li/songe/gkd/ui/home/SettingsPage.kt
index 1c7eb36..0521010 100644
--- a/app/src/main/java/li/songe/gkd/ui/home/SettingsPage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/home/SettingsPage.kt
@@ -1,6 +1,16 @@
package li.songe.gkd.ui.home
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.projection.MediaProjectionManager
+import android.net.Uri
+import android.provider.Settings
+import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -10,21 +20,43 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.LogUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.dylanc.activityresult.launcher.launchForResult
+import kotlinx.coroutines.delay
import li.songe.gkd.MainActivity
-import li.songe.gkd.R
-import li.songe.gkd.ui.DebugPage
+import li.songe.gkd.accessibility.GkdAbService
+import li.songe.gkd.debug.FloatingService
+import li.songe.gkd.debug.HttpService
+import li.songe.gkd.debug.ScreenshotService
import li.songe.gkd.ui.component.TextSwitch
-import li.songe.gkd.util.LocaleString.Companion.localeString
-import li.songe.gkd.util.Storage
-import li.songe.router.LocalRouter
+import li.songe.gkd.utils.Ext
+import li.songe.gkd.utils.LocalLauncher
+import li.songe.gkd.utils.LocalNavController
+import li.songe.gkd.utils.Storage
+import li.songe.gkd.utils.launchAsFn
+import li.songe.gkd.utils.usePollState
+import li.songe.gkd.utils.useTask
+import rikka.shizuku.Shizuku
+import com.ramcosta.composedestinations.navigation.navigate
+import li.songe.gkd.shizuku.shizukuIsSafeOK
+import li.songe.gkd.ui.destinations.AboutPageDestination
+import li.songe.gkd.ui.destinations.SnapshotPageDestination
@Composable
fun SettingsPage() {
+ val context = LocalContext.current as MainActivity
+ val launcher = LocalLauncher.current
+ val scope = rememberCoroutineScope()
+
+ val navController = LocalNavController.current
+
Column(
modifier = Modifier
.verticalScroll(
@@ -32,37 +64,154 @@ fun SettingsPage() {
)
.padding(20.dp, 0.dp)
) {
+ val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
+ TextSwitch("无障碍授权",
+ "用于获取屏幕信息,点击屏幕上的控件",
+ gkdAccessRunning,
+ onCheckedChange = scope.launchAsFn {
+ if (!it) return@launchAsFn
+ ToastUtils.showShort("请先启动无障碍服务")
+ delay(500)
+ val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(intent)
+ })
- val context = LocalContext.current as MainActivity
-// val composableScope = rememberCoroutineScope()
- val router = LocalRouter.current
+
+ val shizukuIsOk by usePollState { shizukuIsSafeOK() }
+ TextSwitch("Shizuku授权",
+ "高级运行模式,能更准确识别界面活动ID",
+ shizukuIsOk,
+ onCheckedChange = scope.launchAsFn {
+ if (!it) return@launchAsFn
+ try {
+ Shizuku.requestPermission(Activity.RESULT_OK)
+ } catch (e: Exception) {
+ ToastUtils.showShort("Shizuku可能没有运行")
+ }
+ })
+
+
+ val canDrawOverlays by usePollState {
+ Settings.canDrawOverlays(context)
+ }
+ Spacer(modifier = Modifier.height(5.dp))
+ TextSwitch("悬浮窗授权",
+ "用于后台提示,主动保存快照等功能",
+ canDrawOverlays,
+ onCheckedChange = scope.launchAsFn {
+ if (!Settings.canDrawOverlays(context)) {
+ val intent = Intent(
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:$context.packageName")
+ )
+ launcher.launch(intent) { resultCode, _ ->
+ if (resultCode != ComponentActivity.RESULT_OK) return@launch
+ if (!Settings.canDrawOverlays(context)) return@launch
+ val intent1 = Intent(context, FloatingService::class.java)
+ ContextCompat.startForegroundService(context, intent1)
+ }
+ }
+ })
+
+ Spacer(modifier = Modifier.height(15.dp))
+
+ val httpServerRunning by usePollState { HttpService.isRunning() }
+ TextSwitch("HTTP服务",
+ "开启HTTP服务, 以便在同一局域网下传递数据" + if (httpServerRunning) "\n${
+ Ext.getIpAddressInLocalNetwork()
+ .map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
+ .joinToString(",")
+ }" else "\n暂无地址",
+ httpServerRunning) {
+ if (it) {
+ HttpService.start()
+ } else {
+ HttpService.stop()
+ }
+ }
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ val screenshotRunning by usePollState { ScreenshotService.isRunning() }
+ TextSwitch("截屏服务",
+ "生成快照需要截取屏幕,Android>=11无需开启",
+ screenshotRunning,
+ scope.launchAsFn {
+ if (it) {
+ val mediaProjectionManager =
+ context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
+ val activityResult =
+ launcher.launchForResult(mediaProjectionManager.createScreenCaptureIntent())
+ if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) {
+ ScreenshotService.start(intent = activityResult.data!!)
+ }
+ } else {
+ ScreenshotService.stop()
+ }
+ })
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ val floatingRunning by usePollState {
+ FloatingService.isRunning()
+ }
+ TextSwitch("悬浮窗服务", "便于用户主动保存快照", floatingRunning) {
+ if (it) {
+ if (Settings.canDrawOverlays(context)) {
+ val intent = Intent(context, FloatingService::class.java)
+ ContextCompat.startForegroundService(context, intent)
+ } else {
+ val intent = Intent(
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:$context.packageName")
+ )
+ launcher.launch(intent) { resultCode, _ ->
+ if (resultCode != ComponentActivity.RESULT_OK) return@launch
+ if (!Settings.canDrawOverlays(context)) return@launch
+ val intent1 = Intent(context, FloatingService::class.java)
+ ContextCompat.startForegroundService(context, intent1)
+ }
+ }
+ } else {
+ FloatingService.stop(context)
+ }
+ }
+
+
+ Spacer(modifier = Modifier.height(15.dp))
var enableService by remember { mutableStateOf(Storage.settings.enableService) }
- TextSwitch(
- text = "保持服务${(if (enableService) "开启" else "关闭")}",
+
+ Spacer(modifier = Modifier.height(5.dp))
+ TextSwitch(name = "服务开启",
+ desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
checked = enableService,
onCheckedChange = {
enableService = it
Storage.settings.commit {
this.enableService = it
}
- }
- )
+ })
+
+ Spacer(modifier = Modifier.height(5.dp))
+
var excludeFromRecents by remember { mutableStateOf(Storage.settings.excludeFromRecents) }
- TextSwitch(
- text = "在[最近任务]界面中隐藏本应用",
+ TextSwitch(name = "隐藏后台",
+ desc = "在[最近任务]界面中隐藏本应用",
checked = excludeFromRecents,
onCheckedChange = {
excludeFromRecents = it
Storage.settings.commit {
this.excludeFromRecents = it
}
- }
- )
+ })
+
+ Spacer(modifier = Modifier.height(5.dp))
var enableConsoleLogOut by remember { mutableStateOf(Storage.settings.enableConsoleLogOut) }
- TextSwitch(
- text = "保持日志输出到控制台",
+ TextSwitch(name = "日志输出",
+ desc = "保持日志输出到控制台",
checked = enableConsoleLogOut,
onCheckedChange = {
enableConsoleLogOut = it
@@ -70,24 +219,49 @@ fun SettingsPage() {
Storage.settings.commit {
this.enableConsoleLogOut = it
}
- }
- )
+ })
+
+ Spacer(modifier = Modifier.height(5.dp))
var notificationVisible by remember { mutableStateOf(Storage.settings.notificationVisible) }
- TextSwitch(text = "通知栏显示", checked = notificationVisible,
+ TextSwitch(name = "通知栏显示",
+ desc = "通知栏显示可以降低系统杀后台的概率",
+ checked = notificationVisible,
onCheckedChange = {
notificationVisible = it
Storage.settings.commit {
this.notificationVisible = it
}
})
+ Spacer(modifier = Modifier.height(5.dp))
- Button(onClick = {
- router.navigate(DebugPage)
- }) {
- Text(text = "调试模式")
+ var enableScreenshot by remember {
+ mutableStateOf(Storage.settings.enableCaptureSystemScreenshot)
+ }
+ Spacer(modifier = Modifier.height(5.dp))
+ TextSwitch(
+ "自动快照", "当用户截屏时,自动保存当前界面的快照,目前仅支持miui", enableScreenshot
+ ) {
+ enableScreenshot = it
+ Storage.settings.commit {
+ enableCaptureSystemScreenshot = it
+ }
+ }
+
+ Spacer(modifier = Modifier.height(5.dp))
+ Button(onClick = scope.useTask().launchAsFn {
+ navController.navigate(SnapshotPageDestination)
+ }) {
+ Text(text = "查看快照记录")
+ }
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ Button(onClick = scope.useTask().launchAsFn {
+ navController.navigate(AboutPageDestination)
+ }) {
+ Text(text = "查看关于")
}
- Text(text = "多语言自动切换:" + localeString(R.string.app_name))
}
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/home/SubsManagePage.kt b/app/src/main/java/li/songe/gkd/ui/home/SubsManagePage.kt
index b16e421..6b6102a 100644
--- a/app/src/main/java/li/songe/gkd/ui/home/SubsManagePage.kt
+++ b/app/src/main/java/li/songe/gkd/ui/home/SubsManagePage.kt
@@ -1,5 +1,6 @@
package li.songe.gkd.ui.home
+import android.webkit.URLUtil
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -19,59 +20,127 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.blankj.utilcode.util.ClipboardUtils
-import com.blankj.utilcode.util.PathUtils
import com.blankj.utilcode.util.ToastUtils
import com.google.zxing.BarcodeFormat
+import com.ramcosta.composedestinations.navigation.navigate
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
-import li.songe.gkd.R
+import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.SubscriptionRaw
-import li.songe.gkd.db.table.SubsConfig
-import li.songe.gkd.db.table.SubsItem
-import li.songe.gkd.db.util.Operator.eq
-import li.songe.gkd.db.util.RoomX
-import li.songe.gkd.hooks.useNavigateForQrcodeResult
-import li.songe.gkd.ui.SubsPage
+import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.SubsItemCard
-import li.songe.gkd.util.Ext.launchTry
-import li.songe.gkd.util.Singleton
-import li.songe.gkd.util.ThrottleState
-import li.songe.router.LocalRouter
-import java.io.File
+import li.songe.gkd.ui.destinations.SubsPageDestination
+import li.songe.gkd.utils.LaunchedEffectTry
+import li.songe.gkd.utils.LocalNavController
+import li.songe.gkd.utils.SafeR
+import li.songe.gkd.utils.Singleton
+import li.songe.gkd.utils.launchAsFn
+import li.songe.gkd.utils.rememberCache
+import li.songe.gkd.utils.useNavigateForQrcodeResult
+import li.songe.gkd.utils.useTask
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SubscriptionManagePage() {
val scope = rememberCoroutineScope()
- val router = LocalRouter.current
+ val navController = LocalNavController.current
- var subItemList by remember { mutableStateOf(listOf()) }
- var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
- var shareQrcode: ImageBitmap? by remember { mutableStateOf(null) }
- var deleteSubItem: SubsItem? by remember { mutableStateOf(null) }
+ var subItems by rememberCache { mutableStateOf(listOf()) }
+ var shareSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
+ var shareQrcode: ImageBitmap? by rememberCache { mutableStateOf(null) }
+ var deleteSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
+ var moveSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
+
+ var showAddDialog by rememberCache { mutableStateOf(false) }
+
+ var showLinkDialog by rememberCache { mutableStateOf(false) }
+ var link by rememberCache { mutableStateOf("") }
- var showAddDialog by remember { mutableStateOf(false) }
- var showLinkInputDialog by remember { mutableStateOf(false) }
- val viewSubItemThrottle = ThrottleState.use(scope)
- val editSubItemThrottle = ThrottleState.use(scope)
- val refreshSubItemThrottle = ThrottleState.use(scope, 250)
val navigateForQrcodeResult = useNavigateForQrcodeResult()
- var linkText by remember {
- mutableStateOf("")
+ LaunchedEffectTry(Unit) {
+ DbSet.subsItemDao.query().flowOn(IO).collect {
+ subItems = it
+ }
}
- LaunchedEffect(Unit) {
- subItemList = RoomX.select().sortedBy { it.index }
+ val addSubs = scope.useTask(dialog = true).launchAsFn> { urls ->
+ val safeUrls = urls.filter { url ->
+ URLUtil.isNetworkUrl(url) && subItems.all { it.updateUrl != url }
+ }
+ if (safeUrls.isEmpty()) return@launchAsFn
+ onChangeLoading(true)
+ val newItems = safeUrls.mapIndexedNotNull { index, url ->
+ try {
+ val text = Singleton.client.get(url).bodyAsText()
+ val subscriptionRaw = SubscriptionRaw.parse5(text)
+ val newItem = SubsItem(
+ updateUrl = subscriptionRaw.updateUrl ?: url,
+ name = subscriptionRaw.name,
+ version = subscriptionRaw.version,
+ order = index + 1 + subItems.size
+ )
+ withContext(IO) {
+ newItem.subsFile.writeText(text)
+ }
+ newItem
+ } catch (e: Exception) {
+ null
+ }
+ }
+ if (newItems.isNotEmpty()) {
+ DbSet.subsItemDao.insert(*newItems.toTypedArray())
+ ToastUtils.showShort("成功添加 ${newItems.size} 条订阅")
+ } else {
+ ToastUtils.showShort("添加失败")
+ }
+ }
+
+ val updateSubs = scope.useTask(dialog = true).launchAsFn> { oldItems ->
+ if (oldItems.isEmpty()) return@launchAsFn
+ onChangeLoading(true)
+ val newItems = oldItems.mapNotNull { oldItem ->
+ try {
+ val subscriptionRaw = SubscriptionRaw.parse5(
+ Singleton.client.get(oldItem.updateUrl).bodyAsText()
+ )
+ if (subscriptionRaw.version <= oldItem.version) {
+ return@mapNotNull null
+ }
+ val newItem = oldItem.copy(
+ updateUrl = subscriptionRaw.updateUrl ?: oldItem.updateUrl,
+ name = subscriptionRaw.name,
+ mtime = System.currentTimeMillis(),
+ version = subscriptionRaw.version
+ )
+ withContext(IO) {
+ newItem.subsFile.writeText(
+ SubscriptionRaw.stringify(
+ subscriptionRaw
+ )
+ )
+ }
+ newItem
+ } catch (e: Exception) {
+ ToastUtils.showShort(e.message)
+ null
+ }
+ }
+ if (newItems.isEmpty()) {
+ ToastUtils.showShort("暂无更新")
+ } else {
+ DbSet.subsItemDao.update(*newItems.toTypedArray())
+ ToastUtils.showShort("更新 ${newItems.size} 条订阅")
+ }
}
LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxHeight()
) {
- item(subItemList) {
+ item(subItems) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@@ -79,11 +148,17 @@ fun SubscriptionManagePage() {
.fillMaxWidth()
.padding(10.dp, 0.dp)
) {
- Text(
- text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
- )
+ if (subItems.isEmpty()) {
+ Text(
+ text = "暂无订阅",
+ )
+ } else {
+ Text(
+ text = "共有${subItems.size}条订阅,激活:${subItems.count { it.enable }},禁用:${subItems.count { !it.enable }}",
+ )
+ }
Row {
- Image(painter = painterResource(R.drawable.ic_add),
+ Image(painter = painterResource(SafeR.ic_add),
contentDescription = "",
modifier = Modifier
.clickable {
@@ -91,98 +166,51 @@ fun SubscriptionManagePage() {
}
.padding(4.dp)
.size(25.dp))
- Image(painter = painterResource(R.drawable.ic_refresh),
+ Image(
+ painter = painterResource(SafeR.ic_refresh),
contentDescription = "",
modifier = Modifier
- .clickable {
- scope.launchTry {
- subItemList.mapIndexed { i, oldItem ->
- val subscriptionRaw = SubscriptionRaw.parse5(
- Singleton.client
- .get(oldItem.updateUrl)
- .bodyAsText()
- )
- if (subscriptionRaw.version <= oldItem.version) {
- ToastUtils.showShort("暂无更新:${oldItem.name}")
- return@mapIndexed
- }
- val newItem = oldItem.copy(
- updateUrl = subscriptionRaw.updateUrl
- ?: oldItem.updateUrl,
- name = subscriptionRaw.name,
- mtime = System.currentTimeMillis(),
- version = subscriptionRaw.version
- )
- RoomX.update(newItem)
- File(newItem.filePath).writeText(
- SubscriptionRaw.stringify(
- subscriptionRaw
- )
- )
- ToastUtils.showShort("更新成功:${newItem.name}")
- subItemList = subItemList
- .toMutableList()
- .also {
- it[i] = newItem
- }
- }
- }
- }
+ .clickable(onClick = {
+ updateSubs(subItems)
+ })
.padding(4.dp)
- .size(25.dp))
+ .size(25.dp)
+ )
}
}
}
- items(subItemList.size) { i ->
+ items(subItems.size) { i ->
Card(
modifier = Modifier
.animateItemPlacement()
.padding(vertical = 3.dp, horizontal = 8.dp)
- .clickable(onClick = { router.navigate(SubsPage, subItemList[i]) }),
+ .combinedClickable(
+ onClick = scope
+ .useTask()
+ .launchAsFn {
+ navController.navigate(SubsPageDestination(subItems[i]))
+ }, onLongClick = {
+ if (subItems.size > 1) {
+ moveSubItem = subItems[i]
+ }
+ }),
elevation = 0.dp,
border = BorderStroke(1.dp, Color(0xfff6f6f6)),
shape = RoundedCornerShape(8.dp),
) {
- SubsItemCard(subItemList[i], onShareClick = {
- shareSubItem = subItemList[i]
- }, onEditClick = editSubItemThrottle.invoke {
+ SubsItemCard(subItems[i], onShareClick = {
+ shareSubItem = subItems[i]
}, onDelClick = {
- deleteSubItem = subItemList[i]
- }, onRefreshClick = refreshSubItemThrottle.invoke {
- val oldItem = subItemList[i]
- val subscriptionRaw = SubscriptionRaw.parse5(
- Singleton.client.get(oldItem.updateUrl).bodyAsText()
- )
- if (subscriptionRaw.version <= oldItem.version) {
- ToastUtils.showShort("暂无更新:${oldItem.name}")
- return@invoke
- }
- val newItem = oldItem.copy(
- updateUrl = subscriptionRaw.updateUrl
- ?: oldItem.updateUrl,
- name = subscriptionRaw.name,
- mtime = System.currentTimeMillis(),
- version = subscriptionRaw.version
- )
- RoomX.update(newItem)
- withContext(IO) {
- File(newItem.filePath).writeText(SubscriptionRaw.stringify(subscriptionRaw))
- }
- subItemList = subItemList.toMutableList().also {
- it[i] = newItem
- }
- ToastUtils.showShort("更新成功:${newItem.name}")
- }.catch {
- if (!it.message.isNullOrEmpty()) {
- ToastUtils.showShort(it.message)
- }
+ deleteSubItem = subItems[i]
+ }, onRefreshClick = {
+ updateSubs(listOf(subItems[i]))
})
}
}
}
- shareSubItem?.let { _shareSubItem ->
+ shareSubItem?.let { shareSubItemVal ->
Dialog(onDismissRequest = { shareSubItem = null }) {
Box(
Modifier
@@ -191,64 +219,109 @@ fun SubscriptionManagePage() {
.padding(8.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text(text = "二维码",
- modifier = Modifier
- .clickable {
- shareQrcode = Singleton.barcodeEncoder
- .encodeBitmap(
- _shareSubItem.updateUrl,
- BarcodeFormat.QR_CODE,
- 500,
- 500
- )
- .asImageBitmap()
- shareSubItem = null
- }
- .fillMaxWidth()
- .padding(8.dp))
- Text(text = "导出至剪切板",
- modifier = Modifier
- .clickable {
- ClipboardUtils.copyText(_shareSubItem.updateUrl)
- shareSubItem = null
- }
- .fillMaxWidth()
- .padding(8.dp))
+ Text(text = "二维码", modifier = Modifier
+ .clickable {
+ shareQrcode = Singleton.barcodeEncoder
+ .encodeBitmap(
+ shareSubItemVal.updateUrl, BarcodeFormat.QR_CODE, 500, 500
+ )
+ .asImageBitmap()
+ shareSubItem = null
+ }
+ .fillMaxWidth()
+ .padding(8.dp))
+ Text(text = "导出至剪切板", modifier = Modifier
+ .clickable {
+ ClipboardUtils.copyText(shareSubItemVal.updateUrl)
+ shareSubItem = null
+ ToastUtils.showShort("复制成功")
+ }
+ .fillMaxWidth()
+ .padding(8.dp))
}
}
}
}
- shareQrcode?.let { _shareQrcode ->
+ shareQrcode?.let { shareQrcodeVal ->
Dialog(onDismissRequest = { shareQrcode = null }) {
Image(
- bitmap = _shareQrcode,
+ bitmap = shareQrcodeVal,
contentDescription = "qrcode",
modifier = Modifier.size(400.dp)
)
}
}
+ moveSubItem?.let { moveSubItemVal ->
+ Dialog(onDismissRequest = { moveSubItem = null }) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier
+ .width(200.dp)
+ .wrapContentHeight()
+ .background(Color.White)
+ .padding(8.dp)
+ ) {
+ if (subItems.firstOrNull() != moveSubItemVal) {
+ Text(
+ text = "上移",
+ modifier = Modifier
+ .clickable(
+ onClick = scope
+ .useTask()
+ .launchAsFn {
+ val lastItem =
+ subItems[subItems.indexOf(moveSubItemVal) - 1]
+ DbSet.subsItemDao.update(
+ lastItem.copy(
+ order = moveSubItemVal.order
+ ),
+ moveSubItemVal.copy(
+ order = lastItem.order
+ )
+ )
+ moveSubItem = null
+ })
+ .fillMaxWidth()
+ .padding(8.dp)
+ )
+ }
+ if (subItems.lastOrNull() != moveSubItemVal) {
+ Text(
+ text = "下移",
+ modifier = Modifier
+ .clickable(
+ onClick = scope
+ .useTask()
+ .launchAsFn {
+ val nextItem =
+ subItems[subItems.indexOf(moveSubItemVal) + 1]
+ DbSet.subsItemDao.update(
+ nextItem.copy(
+ order = moveSubItemVal.order
+ ),
+ moveSubItemVal.copy(
+ order = nextItem.order
+ )
+ )
+ moveSubItem = null
+ })
+ .fillMaxWidth()
+ .padding(8.dp)
+ )
+ }
+ }
+ }
+ }
- val delSubItemThrottle = ThrottleState.use(scope)
- if (deleteSubItem != null) {
+
+ deleteSubItem?.let { deleteSubItemVal ->
AlertDialog(onDismissRequest = { deleteSubItem = null },
title = { Text(text = "是否删除该项") },
confirmButton = {
- Button(onClick = delSubItemThrottle.invoke {
- if (deleteSubItem == null) return@invoke
- deleteSubItem?.let {
- RoomX.delete(it)
- RoomX.delete { SubsConfig::subsItemId eq it.id }
- }
- withContext(IO) {
- try {
- File(deleteSubItem!!.filePath).delete()
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
- subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) }
+ Button(onClick = scope.launchAsFn {
+ deleteSubItemVal.removeAssets()
deleteSubItem = null
}) {
Text("是")
@@ -262,98 +335,79 @@ fun SubscriptionManagePage() {
}
})
}
+
if (showAddDialog) {
- val clickQrcodeThrottle = ThrottleState.use(scope)
Dialog(onDismissRequest = { showAddDialog = false }) {
- Box(
- Modifier
+ Column(
+ modifier = Modifier
.width(250.dp)
.background(Color.White)
- .padding(8.dp)
+ .padding(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text(
- text = "二维码", modifier = Modifier
- .clickable(onClick = clickQrcodeThrottle.invoke {
- showAddDialog = false
- val qrCode = navigateForQrcodeResult()
- val contents = qrCode.contents
- if (contents != null) {
- showLinkInputDialog = true
- linkText = contents
- }
- })
- .fillMaxWidth()
- .padding(8.dp)
- )
- Text(text = "链接", modifier = Modifier
- .clickable {
- showLinkInputDialog = true
+ Text(
+ text = "默认订阅", modifier = Modifier
+ .clickable(onClick = {
showAddDialog = false
- }
+ addSubs(
+ listOf(
+ "https://cdn.lisonge.com/startup_ad.json",
+ "https://cdn.lisonge.com/internal_ad.json",
+ "https://cdn.lisonge.com/quick_util.json",
+ )
+ )
+ })
.fillMaxWidth()
- .padding(8.dp))
- }
+ .padding(8.dp)
+ )
+ Text(
+ text = "二维码", modifier = Modifier
+ .clickable(onClick = scope.launchAsFn {
+ showAddDialog = false
+ val qrCode = navigateForQrcodeResult()
+ val contents = qrCode.contents
+ if (contents != null) {
+ showLinkDialog = true
+ link = contents
+ }
+ })
+ .fillMaxWidth()
+ .padding(8.dp)
+ )
+ Text(text = "链接", modifier = Modifier
+ .clickable {
+ showLinkDialog = true
+ showAddDialog = false
+ }
+ .fillMaxWidth()
+ .padding(8.dp))
}
}
}
- if (showLinkInputDialog) {
- Dialog(onDismissRequest = { showLinkInputDialog = false;linkText = "" }) {
+
+
+
+ LaunchedEffect(showLinkDialog) {
+ if (!showLinkDialog) {
+ link = ""
+ }
+ }
+ if (showLinkDialog) {
+ Dialog(onDismissRequest = { showLinkDialog = false }) {
Box(
Modifier
- .width(250.dp)
+ .width(300.dp)
.background(Color.White)
.padding(8.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "请输入订阅链接")
TextField(
- value = linkText,
- onValueChange = { linkText = it },
- singleLine = true
+ value = link, onValueChange = { link = it.trim() }, singleLine = true
)
Button(onClick = {
- showLinkInputDialog = false
- if (subItemList.any { it.updateUrl == linkText }) {
- ToastUtils.showShort("该链接已经添加过")
- return@Button
- }
- scope.launch {
- try {
- val text = Singleton.client.get(linkText).bodyAsText()
- val subscriptionRaw = SubscriptionRaw.parse5(text)
- File(
- PathUtils.getExternalAppFilesPath()
- .plus("/subscription/")
- ).apply {
- if (!exists()) {
- mkdir()
- }
- }
- val file = File(
- PathUtils.getExternalAppFilesPath()
- .plus("/subscription/")
- .plus(System.currentTimeMillis())
- .plus(".json")
- )
- withContext(IO) {
- file.writeText(text)
- }
- val tempItem = SubsItem(
- updateUrl = subscriptionRaw.updateUrl ?: linkText,
- filePath = file.absolutePath,
- name = subscriptionRaw.name,
- version = subscriptionRaw.version
- )
- val newItem = tempItem.copy(
- id = RoomX.insert(tempItem)[0]
- )
- subItemList = subItemList.toMutableList().apply { add(newItem) }
- } catch (e: Exception) {
- e.printStackTrace()
- ToastUtils.showShort(e.message ?: "")
- }
- }
+ addSubs(listOf(link))
+ showLinkDialog = false
}) {
Text(text = "添加")
}
diff --git a/app/src/main/java/li/songe/gkd/ui/theme/Color.kt b/app/src/main/java/li/songe/gkd/ui/theme/Color.kt
index 4991555..9d1be6f 100644
--- a/app/src/main/java/li/songe/gkd/ui/theme/Color.kt
+++ b/app/src/main/java/li/songe/gkd/ui/theme/Color.kt
@@ -2,7 +2,7 @@ package li.songe.gkd.ui.theme
import androidx.compose.ui.graphics.Color
-val Purple200 = Color(0xFFBB86FC)
-val Purple500 = Color(0xFF6200EE)
-val Purple700 = Color(0xFF3700B3)
+val Purple200 = Color(0xFFf8f9f9)
+val Purple500 = Color(0xFFf2f3f4)
+val Purple700 = Color(0xFFe5e7e9)
val Teal200 = Color(0xFF03DAC5)
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt b/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt
index 1c7a26e..78e52eb 100644
--- a/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt
+++ b/app/src/main/java/li/songe/gkd/ui/theme/Theme.kt
@@ -6,29 +6,12 @@ import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
-private val DarkColorPalette = darkColors(
- primary = Purple200,
- primaryVariant = Purple700,
- secondary = Teal200
-)
+private val DarkColorPalette = darkColors()
-private val LightColorPalette = lightColors(
- primary = Purple500,
- primaryVariant = Purple700,
- secondary = Teal200
-
- /* Other default colors to override
- background = Color.White,
- surface = Color.White,
- onPrimary = Color.White,
- onSecondary = Color.Black,
- onBackground = Color.Black,
- onSurface = Color.Black,
- */
-)
+private val LightColorPalette = lightColors()
@Composable
-fun MainTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
+fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
diff --git a/app/src/main/java/li/songe/gkd/util/LocaleString.kt b/app/src/main/java/li/songe/gkd/util/LocaleString.kt
deleted file mode 100644
index 29cd39c..0000000
--- a/app/src/main/java/li/songe/gkd/util/LocaleString.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package li.songe.gkd.util
-
-import android.content.res.Configuration
-import android.os.LocaleList
-import androidx.annotation.StringRes
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import li.songe.gkd.App
-import java.util.*
-
-class LocaleString(private vararg val localeList: Locale) : (Int) -> String {
- private val languageContext by lazy {
- App.context.createConfigurationContext(Configuration(App.context.resources.configuration).apply {
- if (this@LocaleString.localeList.isNotEmpty()) {
- setLocales(LocaleList(*this@LocaleString.localeList))
- } else {
- setLocales(App.context.resources.configuration.locales)
- }
- })
- }
-
- override fun invoke(@StringRes resId: Int) = languageContext.getString(resId)
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as LocaleString
- if (!localeList.contentEquals(other.localeList)) return false
- return true
- }
-
- override fun hashCode(): Int {
-
- return localeList.contentHashCode()
- }
-
- companion object {
- var localeString by mutableStateOf(LocaleString())
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/util/ModifierExt.kt b/app/src/main/java/li/songe/gkd/util/ModifierExt.kt
deleted file mode 100644
index a1c536c..0000000
--- a/app/src/main/java/li/songe/gkd/util/ModifierExt.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package li.songe.gkd.util
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-
-object ModifierExt {
- inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
- clickable(indication = null,
- interactionSource = remember { MutableInteractionSource() }) {
- onClick()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/util/Status.kt b/app/src/main/java/li/songe/gkd/util/Status.kt
deleted file mode 100644
index 0638e5d..0000000
--- a/app/src/main/java/li/songe/gkd/util/Status.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package li.songe.gkd.util
-
-sealed class Status {
- object Empty : Status()
-
- /**
- * @param value 0f to 1f
- */
- class Progress(val value: Float = 0f) : Status()
- class Success(val value: T) : Status()
- class Error(val value: Any?) : Status() {
-// override fun toString(): String {
-// val nullMsg = "未知错误"
-// return when (value) {
-// null -> nullMsg
-// is String -> value
-// is Exception -> value.message ?: nullMsg
-// else -> value.toString()
-// }
-// }
- }
-}
diff --git a/app/src/main/java/li/songe/gkd/util/ThrottleState.kt b/app/src/main/java/li/songe/gkd/util/ThrottleState.kt
deleted file mode 100644
index 015f23f..0000000
--- a/app/src/main/java/li/songe/gkd/util/ThrottleState.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-package li.songe.gkd.util
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-class ThrottleState(
- private val scope: CoroutineScope,
- private val miniAwaitTime: Long = 200L,
- val loading: Boolean = false,
- private val onChangeLoading: (value: Boolean) -> Unit = {},
-) {
- companion object {
- private lateinit var defaultFalseInstance: ThrottleState
-
- @Composable
- fun use(scope: CoroutineScope, miniAwaitTime: Long = 0): ThrottleState {
- var loading by remember { mutableStateOf(false) }
- if (loading) {
- if (!::defaultFalseInstance.isInitialized) {
- defaultFalseInstance = ThrottleState(scope, miniAwaitTime, loading = true)
- }
- return defaultFalseInstance
- }
- return ThrottleState(scope, miniAwaitTime, loading = false) {
- loading = it
- }
- }
- }
-
- class CatchInvoke(
- private val onChangeCatch: (catchFn: ((e: Exception) -> Unit)) -> Unit,
- private val fn: () -> Unit,
- ) : () -> Unit {
- override fun invoke() {
- fn()
- }
-
- fun catch(catchFn: ((e: Exception) -> Unit)): CatchInvoke {
- onChangeCatch(catchFn)
- return this
- }
- }
-
- class CatchInvoke1(
- private val onChangeCatch: (catchFn: ((e: Exception) -> Unit)) -> Unit,
- private val fn: (T) -> Unit,
- ) : (T) -> Unit {
- override fun invoke(t: T) {
- fn(t)
- }
-
- fun catch(catchFn: ((e: Exception) -> Unit)): CatchInvoke1 {
- onChangeCatch(catchFn)
- return this
- }
- }
-
- fun invoke(
- miniAwaitTime: Long = this.miniAwaitTime,
- fn: suspend () -> Unit,
- ): CatchInvoke {
- var catchFn = { e: Exception -> e.printStackTrace() }
- return CatchInvoke({ catchFn = it }) fnWrapper@{
- if (loading) return@fnWrapper
- onChangeLoading(true)
- scope.launch {
- try {
- fn()
- } catch (e: Exception) {
- catchFn(e)
- } finally {
- delay(miniAwaitTime)
- onChangeLoading(false)
- }
- }
-
- }
- }
-
- fun invoke(
- miniAwaitTime: Long = this.miniAwaitTime,
- fn: suspend (T) -> Unit,
- ): CatchInvoke1 {
- var catchFn = { e: Exception -> e.printStackTrace() }
- return CatchInvoke1({ catchFn = it }) fnWrapper@{ t ->
- if (loading) return@fnWrapper
- onChangeLoading(true)
- scope.launch {
- try {
- fn(t)
- } catch (e: Exception) {
- catchFn(e)
- } finally {
- delay(miniAwaitTime)
- onChangeLoading(false)
- }
- }
-
- }
- }
-
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/util/UseHook.kt b/app/src/main/java/li/songe/gkd/util/UseHook.kt
deleted file mode 100644
index a29cdd9..0000000
--- a/app/src/main/java/li/songe/gkd/util/UseHook.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package li.songe.gkd.util
-
-import android.content.res.Configuration
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import com.blankj.utilcode.util.ScreenUtils
-
-object UseHook {
- var screenWidth by mutableStateOf(ScreenUtils.getAppScreenWidth())
- var screenHeight by mutableStateOf(ScreenUtils.getAppScreenHeight())
- var screenOrientationIsLandscape by mutableStateOf(ScreenUtils.isLandscape())
-// var locale by mutableStateOf(LanguageUtils.getSystemLanguage())
-
- fun update(newConfig: Configuration) {
- screenHeight = ScreenUtils.getAppScreenHeight()
- screenWidth = ScreenUtils.getAppScreenWidth()
- screenOrientationIsLandscape = ScreenUtils.isLandscape()
- }
-}
diff --git a/app/src/main/java/li/songe/gkd/util/AppSettings.kt b/app/src/main/java/li/songe/gkd/utils/AppSettings.kt
similarity index 69%
rename from app/src/main/java/li/songe/gkd/util/AppSettings.kt
rename to app/src/main/java/li/songe/gkd/utils/AppSettings.kt
index cd76913..d6699c9 100644
--- a/app/src/main/java/li/songe/gkd/util/AppSettings.kt
+++ b/app/src/main/java/li/songe/gkd/utils/AppSettings.kt
@@ -1,6 +1,8 @@
-package li.songe.gkd.util
+package li.songe.gkd.utils
import android.os.Parcelable
+import androidx.compose.runtime.collectAsState
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.parcelize.Parcelize
/**
@@ -8,24 +10,24 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class AppSettings(
- var ctime: Long = System.currentTimeMillis(),
- var mtime: Long = System.currentTimeMillis(),
var enableService: Boolean = true,
var excludeFromRecents: Boolean = true,
var notificationVisible: Boolean = true,
- var enableDebugServer: Boolean = false,
- var httpServerPort: Int = 8888,
var enableConsoleLogOut: Boolean = true,
+ var enableCaptureSystemScreenshot: Boolean = true,
+ var httpServerPort: Int = 8888,
) : Parcelable {
fun commit(block: AppSettings.() -> Unit) {
val backup = copy()
block.invoke(this)
if (this != backup) {
- mtime = System.currentTimeMillis()
Storage.kv.encode(saveKey, this)
}
}
+
companion object {
- const val saveKey = "settings-v1"
+ const val saveKey = "settings-v2"
}
-}
\ No newline at end of file
+}
+
+val appSettingsFlow by lazy { MutableStateFlow(AppSettings()) }
diff --git a/app/src/main/java/li/songe/gkd/utils/ComposeExt.kt b/app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
new file mode 100644
index 0000000..a8581a6
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
@@ -0,0 +1,50 @@
+package li.songe.gkd.utils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import com.blankj.utilcode.util.ToastUtils
+import com.dylanc.activityresult.launcher.StartActivityLauncher
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+
+
+val LocalLauncher =
+ compositionLocalOf { error("not found StartActivityLauncher") }
+
+@Composable
+fun usePollState(interval: Long = 1000L, getter: () -> T): MutableState {
+ val mutableState = remember { mutableStateOf(getter()) }
+ LaunchedEffect(Unit) {
+ while (isActive) {
+ delay(interval)
+ mutableState.value = getter()
+ }
+ }
+ return mutableState
+}
+
+@Composable
+fun LaunchedEffectTry(
+ key1: Any? = null,
+ block: suspend CoroutineScope.() -> Unit
+) {
+ LaunchedEffect(key1) {
+ try {
+ block()
+ } catch (e: CancellationException) {
+ e.printStackTrace()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ToastUtils.showShort(e.message ?: "")
+ }
+ }
+}
+
+
+
diff --git a/app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt b/app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
new file mode 100644
index 0000000..f1167c0
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
@@ -0,0 +1,100 @@
+package li.songe.gkd.utils
+
+import com.blankj.utilcode.util.LogUtils
+import com.blankj.utilcode.util.ToastUtils
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+
+
+fun CoroutineScope.launchWhile(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend CoroutineScope.() -> Unit,
+) = launch(context, start) {
+ while (isActive) {
+ block()
+ }
+}
+
+
+fun CoroutineScope.launchWhileTry(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ interval: Long = 0,
+ block: suspend CoroutineScope.() -> Unit,
+) = launch(context, start) {
+ while (isActive) {
+ try {
+ block()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ delay(interval)
+ }
+}
+
+fun CoroutineScope.launchTry(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend CoroutineScope.() -> Unit,
+) = launch(context, start) {
+ try {
+ block()
+ } catch (e: CancellationException) {
+ e.printStackTrace()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ToastUtils.showShort(e.message)
+ }
+}
+
+fun CoroutineScope.launchAsFn(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend CoroutineScope.() -> Unit,
+): () -> Unit {
+ return {
+ launch(context, start) {
+ try {
+ block()
+ } catch (e: CancellationException) {
+ e.printStackTrace()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ToastUtils.showShort(e.message)
+ }
+ }
+ }
+}
+
+fun CoroutineScope.launchAsFn(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend CoroutineScope.(T) -> Unit,
+): (T) -> Unit {
+ return {
+ launch(context, start) {
+ try {
+ block(it)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ToastUtils.showShort(e.message)
+ }
+ }
+ }
+}
+
+suspend fun stopJob(): Nothing {
+ coroutineContext[Job]?.cancel()
+ delay(1)
+ error("stop failed")
+}
+
diff --git a/app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt b/app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
new file mode 100644
index 0000000..c1d4308
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
@@ -0,0 +1,79 @@
+package li.songe.gkd.utils
+
+import android.graphics.Path
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RectShape
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.addPathNodes
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+fun createDrawable(block: () -> Unit) {
+ val s = materialIcon(name = "xx") {
+ addPath(addPathNodes(""))
+ }
+}
+
+fun createVectorDrawable(block: () -> Unit) {
+ val path = Path().apply {
+ addPathNodes("").forEach {
+ it.isQuad
+ }
+ }
+ val shapeDrawable = ShapeDrawable(RectShape())
+ shapeDrawable.apply {
+
+ addPathNodes("")[0].isCurve
+ }
+// val r = Resources()
+// Drawable.createFromXml()
+}
+
+
+val x = createDrawable {
+// val p =
+ vector {
+ width = 24.dp
+ height = 24.dp
+ viewportWidth = 24F
+ viewportHeight = 24F
+ path {
+ width
+ fillColor = Color(0xFF000000)
+ pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
+ }
+ }
+}
+
+interface VectorType {
+ var width: Dp
+ var height: Dp
+ var viewportWidth: Float
+ var viewportHeight: Float
+}
+
+fun vector(block: VectorType.() -> Unit) {}
+
+interface PathType {
+ var fillColor: Color
+ var pathData: String
+}
+
+fun path(block: PathType.() -> Unit) {}
+
+fun testDrawable() {
+
+ val s2 = vector {
+ width = 24.dp
+ height = 24.dp
+ viewportWidth = 24F
+ viewportHeight = 24F
+ path {
+ width
+ fillColor = Color(0xFF000000)
+ pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/util/Ext.kt b/app/src/main/java/li/songe/gkd/utils/Ext.kt
similarity index 65%
rename from app/src/main/java/li/songe/gkd/util/Ext.kt
rename to app/src/main/java/li/songe/gkd/utils/Ext.kt
index b6f8258..aa4baf4 100644
--- a/app/src/main/java/li/songe/gkd/util/Ext.kt
+++ b/app/src/main/java/li/songe/gkd/utils/Ext.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.util
+package li.songe.gkd.utils
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -16,32 +16,18 @@ import android.os.Looper
import androidx.compose.runtime.*
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
-import com.blankj.utilcode.util.ToastUtils
-import com.dylanc.activityresult.launcher.StartActivityLauncher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
+import androidx.core.graphics.drawable.IconCompat
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeoutOrNull
import li.songe.gkd.App
import li.songe.gkd.MainActivity
import li.songe.gkd.R
-import li.songe.gkd.data.RuleManager
-import li.songe.gkd.data.SubscriptionRaw
-import li.songe.gkd.db.table.SubsItem
-import li.songe.gkd.db.util.RoomX
-import li.songe.gkd.shizuku.Shell
-import li.songe.gkd.shizuku.ShizukuShell
-import java.io.File
+import li.songe.gkd.db.DbSet
+import li.songe.gkd.icon.AddIcon
import java.net.NetworkInterface
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
+
object Ext {
fun PackageManager.getApplicationInfoExt(
packageName: String,
@@ -120,7 +106,7 @@ object Ext {
)
val builder = NotificationCompat.Builder(context, channelId)
- .setSmallIcon(R.drawable.ic_app_2)
+ .setSmallIcon(SafeR.ic_launcher)
.setContentTitle("调试模式")
.setContentText("正在录制您的屏幕内容")
.setContentIntent(pendingIntent)
@@ -160,90 +146,14 @@ object Ext {
}
suspend fun getSubsFileLastModified(): Long {
- return RoomX.select().map { File(it.filePath) }
+ return DbSet.subsItemDao.query().first().map { it.subsFile }
.filter { it.isFile && it.exists() }
.maxOfOrNull { it.lastModified() } ?: -1L
}
- suspend fun buildRuleManager(): RuleManager {
- return RuleManager(*RoomX.select().sortedBy { it.index }.map { subsItem ->
- if (!subsItem.enable) return@map null
- try {
- val file = File(subsItem.filePath)
- if (file.isFile && file.exists()) {
- return@map SubscriptionRaw.parse5(file.readText())
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- return@map null
- }.filterNotNull().toTypedArray())
- }
-
- suspend fun getActivityIdByShizuku(): String? {
- if (!ShizukuShell.instance.isAvailable) return null
- val result = withTimeoutOrNull(250) {
- withContext(Dispatchers.IO) {
- ShizukuShell.instance.exec(Shell.Command("dumpsys activity activities | grep mResumedActivity"))
- }
- } ?: return null
- val strList = result.out.split("\u0020")
- if (!result.isSuccessful || strList.size < 4 || !strList[3].contains('/')) {
- return null
- }
- var (appId, activityId) = strList[3].split('/')
- if (activityId.startsWith('.')) {
- activityId = appId + activityId
- }
- return activityId
- }
-
- fun CoroutineScope.launchWhile(
- context: CoroutineContext = EmptyCoroutineContext,
- start: CoroutineStart = CoroutineStart.DEFAULT,
- block: suspend CoroutineScope.() -> Unit,
- ) = launch(context, start) {
- while (isActive) {
- block()
- }
- }
-
- fun CoroutineScope.launchTry(
- context: CoroutineContext = EmptyCoroutineContext,
- start: CoroutineStart = CoroutineStart.DEFAULT,
- block: suspend CoroutineScope.() -> Unit,
- ) = launch(context, start) {
- try {
- block()
- } catch (e: Exception) {
- e.printStackTrace()
- ToastUtils.showShort(e.message)
- }
- }
-
-
+ @SuppressWarnings("fallthrough")
fun createNotificationChannel(context: Service) {
- val channelId = "channel_service_ab"
- val intent = Intent(context, MainActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }
-
- val pendingIntent: PendingIntent = PendingIntent.getActivity(
- context,
- 0,
- intent,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- )
-
- val builder = NotificationCompat.Builder(context, channelId)
- .setSmallIcon(R.drawable.ic_app_2)
- .setContentTitle("调试模式2")
- .setContentText("测试后台任务")
- .setContentIntent(pendingIntent)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setOngoing(true)
- .setAutoCancel(false)
-
+ val channelId = "无障碍后台服务"
val name = "无障碍服务"
val descriptionText = "无障碍服务保持活跃"
val importance = NotificationManager.IMPORTANCE_DEFAULT
@@ -252,31 +162,40 @@ object Ext {
}
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.createNotificationChannel(channel)
+
+ val serviceId = 100
+ val intent = Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val builder = NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(SafeR.ic_add)
+ .setContentTitle("搞快点")
+ .setContentText("无障碍正在运行")
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setOngoing(true)
+ .setAutoCancel(false)
+
val notification = builder.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
context.startForeground(
- 110,
+ serviceId,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
} else {
- context.startForeground(110, notification)
+ context.startForeground(serviceId, notification)
}
}
- val LocalLauncher =
- compositionLocalOf { error("not found StartActivityLauncher") }
- @Composable
- fun usePollState(interval: Long = 400L, getter: () -> T): MutableState {
- val mutableState = remember { mutableStateOf(getter()) }
- LaunchedEffect(Unit) {
- while (isActive) {
- delay(interval)
- mutableState.value = getter()
- }
- }
- return mutableState
- }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/utils/FolderExt.kt b/app/src/main/java/li/songe/gkd/utils/FolderExt.kt
new file mode 100644
index 0000000..e8cccee
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/FolderExt.kt
@@ -0,0 +1,18 @@
+package li.songe.gkd.utils
+
+import com.blankj.utilcode.util.PathUtils
+import java.io.File
+
+object FolderExt {
+ private fun createFolder(name: String): File {
+ return File(PathUtils.getExternalAppFilesPath().plus("/$name")).apply {
+ if (!exists()) {
+ mkdirs()
+ }
+ }
+ }
+
+ val dbFolder by lazy { createFolder("db") }
+ val subsFolder by lazy { createFolder("subscription") }
+ val snapshotFolder by lazy { createFolder("snapshot") }
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/hooks/Ext.kt b/app/src/main/java/li/songe/gkd/utils/HookExt.kt
similarity index 58%
rename from app/src/main/java/li/songe/gkd/hooks/Ext.kt
rename to app/src/main/java/li/songe/gkd/utils/HookExt.kt
index 847ff49..c037668 100644
--- a/app/src/main/java/li/songe/gkd/hooks/Ext.kt
+++ b/app/src/main/java/li/songe/gkd/utils/HookExt.kt
@@ -1,23 +1,12 @@
-package li.songe.gkd.hooks
+package li.songe.gkd.utils
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import com.blankj.utilcode.util.LogUtils
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
-import io.ktor.client.call.body
-import io.ktor.client.request.get
-import io.ktor.client.statement.bodyAsText
-import kotlinx.coroutines.delay
-import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.data.Value
-import li.songe.gkd.util.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -43,14 +32,3 @@ fun useNavigateForQrcodeResult(): suspend () -> ScanIntentResult {
}
}
-@Composable
-fun useFetchSubs(): suspend (String) -> String {
- val scope = rememberCoroutineScope()
- var loading by remember { mutableStateOf(false) }
- return remember {
- { url ->
- loading
- Singleton.client.get(url).bodyAsText()
- }
- }
-}
diff --git a/app/src/main/java/li/songe/gkd/utils/ModifierExt.kt b/app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
new file mode 100644
index 0000000..594fba8
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
@@ -0,0 +1,14 @@
+package li.songe.gkd.utils
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+
+inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
+ clickable(indication = null,
+ interactionSource = remember { MutableInteractionSource() }) {
+ onClick()
+ }
+}
diff --git a/app/src/main/java/li/songe/gkd/utils/NavExt.kt b/app/src/main/java/li/songe/gkd/utils/NavExt.kt
new file mode 100644
index 0000000..e82c119
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/NavExt.kt
@@ -0,0 +1,8 @@
+package li.songe.gkd.utils
+
+import androidx.compose.runtime.compositionLocalOf
+import androidx.navigation.NavHostController
+
+
+val LocalNavController =
+ compositionLocalOf { error("not found DestinationsNavigator") }
diff --git a/app/src/main/java/li/songe/gkd/utils/SafeR.kt b/app/src/main/java/li/songe/gkd/utils/SafeR.kt
new file mode 100644
index 0000000..fdf7690
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/SafeR.kt
@@ -0,0 +1,28 @@
+package li.songe.gkd.utils
+
+import li.songe.gkd.R
+
+
+/**
+ * ![image](https://github.com/lisonge/gkd/assets/38517192/545c4fce-77b2-4003-8e22-a21b48ef3d98)
+ */
+@Suppress("UNRESOLVED_REFERENCE")
+object SafeR {
+ val capture: Int = R.drawable.capture
+ val ic_add: Int = R.drawable.ic_add
+ val ic_app_2: Int = R.drawable.ic_app_2
+ val ic_apps: Int = R.drawable.ic_apps
+ val ic_back: Int = R.drawable.ic_back
+ val ic_chart_bar: Int = R.drawable.ic_chart_bar
+ val ic_cog: Int = R.drawable.ic_cog
+ val ic_create_round: Int = R.drawable.ic_create_round
+ val ic_database_set: Int = R.drawable.ic_database_set
+ val ic_del: Int = R.drawable.ic_del
+ val ic_launcher: Int = R.drawable.ic_launcher
+ val ic_launcher_background: Int = R.drawable.ic_launcher_background
+ val ic_launcher_round: Int = R.drawable.ic_launcher_round
+ val ic_link: Int = R.drawable.ic_link
+ val ic_menu: Int = R.drawable.ic_menu
+ val ic_refresh: Int = R.drawable.ic_refresh
+ val ic_share: Int = R.drawable.ic_share
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/util/ScreenshotUtil.kt b/app/src/main/java/li/songe/gkd/utils/ScreenshotUtil.kt
similarity index 98%
rename from app/src/main/java/li/songe/gkd/util/ScreenshotUtil.kt
rename to app/src/main/java/li/songe/gkd/utils/ScreenshotUtil.kt
index 1381556..55d313d 100644
--- a/app/src/main/java/li/songe/gkd/util/ScreenshotUtil.kt
+++ b/app/src/main/java/li/songe/gkd/utils/ScreenshotUtil.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.util
+package li.songe.gkd.utils
import android.annotation.SuppressLint
import android.app.Activity
@@ -16,7 +16,7 @@ import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.os.Looper
import com.blankj.utilcode.util.ScreenUtils
-import li.songe.gkd.util.Ext.isEmptyBitmap
+import li.songe.gkd.utils.Ext.isEmptyBitmap
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
diff --git a/app/src/main/java/li/songe/gkd/util/Singleton.kt b/app/src/main/java/li/songe/gkd/utils/Singleton.kt
similarity index 68%
rename from app/src/main/java/li/songe/gkd/util/Singleton.kt
rename to app/src/main/java/li/songe/gkd/utils/Singleton.kt
index 13ab695..9a79466 100644
--- a/app/src/main/java/li/songe/gkd/util/Singleton.kt
+++ b/app/src/main/java/li/songe/gkd/utils/Singleton.kt
@@ -1,9 +1,9 @@
-package li.songe.gkd.util
+package li.songe.gkd.utils
import blue.endless.jankson.Jankson
import com.journeyapps.barcodescanner.BarcodeEncoder
import io.ktor.client.HttpClient
-import io.ktor.client.engine.cio.CIO
+import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.ContentType
@@ -12,6 +12,9 @@ import kotlinx.serialization.json.Json
import java.text.SimpleDateFormat
import java.util.Locale
+/**
+ * 所有单例及其属性必须是不可变属性,以保持多进程下的配置统一性
+ */
object Singleton {
val json by lazy {
@@ -23,16 +26,17 @@ object Singleton {
}
val json5: Jankson by lazy { Jankson.builder().build() }
val client by lazy {
- HttpClient(CIO) {
+ HttpClient(Android) {
install(ContentNegotiation) {
json(json, ContentType.Any)
}
- install(HttpTimeout){
- connectTimeoutMillis = 3000
+ engine {
+ connectTimeout = 10_000
+ socketTimeout = 10_000
}
}
}
- val simpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
+ val simpleDateFormat by lazy { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
val barcodeEncoder by lazy { BarcodeEncoder() }
diff --git a/app/src/main/java/li/songe/gkd/utils/StateCache.kt b/app/src/main/java/li/songe/gkd/utils/StateCache.kt
new file mode 100644
index 0000000..a85546d
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/StateCache.kt
@@ -0,0 +1,87 @@
+package li.songe.gkd.utils
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisallowComposableCalls
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.remember
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination
+import androidx.navigation.NavHostController
+import com.blankj.utilcode.util.LogUtils
+
+
+data class StateCache(
+ var list: MutableList = mutableListOf(),
+ var visitCount: Int = 0
+) {
+ fun assign(other: StateCache) {
+ other.list = list
+ other.visitCount = visitCount
+ }
+}
+
+val LocalStateCache = compositionLocalOf { error("not default value for StateCache") }
+
+@Suppress("UNCHECKED_CAST")
+@Composable
+inline fun rememberCache(
+ crossinline calculation: @DisallowComposableCalls () -> T
+): T {
+ val cache = LocalStateCache.current
+ val state = remember {
+ val visitCount = cache.visitCount
+ cache.visitCount++
+ if (cache.list.size > visitCount) {
+ val value = cache.list[visitCount] as T
+ value
+ } else {
+ val value = calculation()
+ cache.list.add(value)
+ value
+ }
+ }
+ DisposableEffect(Unit) {
+ onDispose {
+ cache.visitCount = 0
+ }
+ }
+ return state
+}
+
+/**
+ * 如果不在乎进程的重建,可以使用此缓存保存任意数据
+ */
+@Composable
+fun StackCacheProvider(navController: NavHostController, content: @Composable () -> Unit) {
+ val stackCaches = remember {
+ Array(navController.backQueue.size) { StateCache() }.toMutableList()
+ }
+// 不使用 mutableStateOf 来避免多余的重组
+ val currentCache = remember {
+ StateCache()
+ }
+ DisposableEffect(Unit) {
+ val listener: (NavController, NavDestination, Bundle?) -> Unit =
+ { navController: NavController, _: NavDestination, _: Bundle? ->
+ val realSize = navController.backQueue.size
+ while (realSize != stackCaches.size) {
+ if (stackCaches.size > realSize) {
+ stackCaches.removeLast()
+ } else if (stackCaches.size < realSize) {
+ stackCaches.add(StateCache())
+ } else {
+ break
+ }
+ }
+ stackCaches.last().assign(currentCache)
+ }
+ navController.addOnDestinationChangedListener(listener)
+ onDispose {
+ navController.removeOnDestinationChangedListener(listener)
+ }
+ }
+ CompositionLocalProvider(LocalStateCache provides currentCache, content = content)
+}
\ No newline at end of file
diff --git a/app/src/main/java/li/songe/gkd/utils/Status.kt b/app/src/main/java/li/songe/gkd/utils/Status.kt
new file mode 100644
index 0000000..6d452be
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/Status.kt
@@ -0,0 +1,13 @@
+package li.songe.gkd.utils
+
+sealed class Status {
+ object Empty : Status()
+
+ /**
+ * @param value 0f to 1f
+ */
+ class Progress(val value: Float = 0f) : Status()
+ class Success(val value: T) : Status()
+ class Error(val value: Any?) : Status() {
+ }
+}
diff --git a/app/src/main/java/li/songe/gkd/util/Storage.kt b/app/src/main/java/li/songe/gkd/utils/Storage.kt
similarity index 91%
rename from app/src/main/java/li/songe/gkd/util/Storage.kt
rename to app/src/main/java/li/songe/gkd/utils/Storage.kt
index b1f8485..2ba43ae 100644
--- a/app/src/main/java/li/songe/gkd/util/Storage.kt
+++ b/app/src/main/java/li/songe/gkd/utils/Storage.kt
@@ -1,4 +1,4 @@
-package li.songe.gkd.util
+package li.songe.gkd.utils
import com.tencent.mmkv.MMKV
diff --git a/app/src/main/java/li/songe/gkd/utils/TaskExt.kt b/app/src/main/java/li/songe/gkd/utils/TaskExt.kt
new file mode 100644
index 0000000..83e8375
--- /dev/null
+++ b/app/src/main/java/li/songe/gkd/utils/TaskExt.kt
@@ -0,0 +1,108 @@
+package li.songe.gkd.utils
+
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.blankj.utilcode.util.LogUtils
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+
+
+private val emptyFn = {}
+private val emptyFn2 = { _: Boolean -> }
+private val emptyFnT1 = { _: Any? -> }
+
+data class TaskState(
+ private val scope: CoroutineScope,
+ val loading: Boolean = false,
+ private val miniInterval: Long = 0,
+ var innerLoading: Boolean = false,
+ val onChangeLoading: (Boolean) -> Unit = emptyFn2,
+) {
+ fun launchAsFn(
+ changeLoading: Boolean = false,
+ block: suspend TaskState.() -> Unit
+ ): () -> Unit {
+ if (loading) return emptyFn
+ return scope.launchAsFn {
+ if (innerLoading) return@launchAsFn
+ innerLoading = true
+ onChangeLoading(changeLoading)
+ val start = System.currentTimeMillis()
+ try {
+ try {
+ block()
+ } finally {
+ val end = System.currentTimeMillis()
+ delay(miniInterval - (end - start))
+ onChangeLoading(false)
+ innerLoading = false
+ }
+ } catch (_: CancellationException) {
+ }
+ }
+ }
+
+ fun launchAsFn(
+ changeLoading: Boolean = false,
+ block: suspend TaskState.(T) -> Unit
+ ): (T) -> Unit {
+ if (loading) return emptyFnT1
+ return scope.launchAsFn {
+ if (innerLoading) return@launchAsFn
+ innerLoading = true
+ onChangeLoading(changeLoading)
+ val start = System.currentTimeMillis()
+ try {
+ try {
+ block(it)
+ } finally {
+ val end = System.currentTimeMillis()
+ delay(miniInterval - (end - start))
+ onChangeLoading(false)
+ innerLoading = false
+ }
+ } catch (_: CancellationException) {
+
+ }
+ }
+ }
+}
+
+@Composable
+fun CoroutineScope.useTask(miniInterval: Long = 1500L, dialog: Boolean = false): TaskState {
+ val trueTask: TaskState = remember {
+ TaskState(scope = this, loading = true)
+ }
+ lateinit var task: MutableState
+ lateinit var falseTask: TaskState
+ falseTask = remember {
+ TaskState(scope = this, miniInterval = miniInterval) {
+ task.value = if (it) trueTask else falseTask
+ }
+ }
+ task = remember { mutableStateOf(falseTask) }
+ if (dialog && task.value.loading) {
+ Dialog(
+ onDismissRequest = {},
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(10.dp),
+ )
+ }
+ }
+ return task.value
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
index 1fa9275..cc9545c 100644
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -1,16 +1,4 @@
-
-
-
+
+
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
index 9a4de82..c94e65f 100644
--- a/app/src/main/res/drawable/ic_add.xml
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -1,6 +1,11 @@
-
-
+
+
diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 0000000..82e3b9a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to app/src/main/res/drawable/ic_launcher.xml
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 69d1e46..043bced 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -1,16 +1,4 @@
-
gkd
+ gkd
+ Cabin -free speed point service based on rules -based matching
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index fbaaeb9..f58814e 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,11 +1,9 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
deleted file mode 100644
index bb82200..0000000
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- 搞快点
-
\ No newline at end of file
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
new file mode 100644
index 0000000..1c8a920
--- /dev/null
+++ b/app/src/main/res/values-zh/strings.xml
@@ -0,0 +1,6 @@
+
+
+ 搞快点
+ 搞快点
+ 基于规则匹配的无障碍速点服务
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index dcfa2b1..0d2c4cc 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,14 +1,4 @@
- #FFBB86FC
- #FF6200EE
- #FF3700B3
- #FF03DAC5
- #FF018786
- #FF000000
- #FFFFFFFF
-
-
- #4D000000
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index af6ccf2..b86f958 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,6 @@
gkd
- 搞快点
- 基于规则匹配的无障碍速点服务
+ gkd
+ Cabin -free speed point service based on rules -based matching
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index cdc57eb..968b947 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,20 +1,10 @@
-
-
-
+
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/accessibility_service_description.xml b/app/src/main/res/xml/ab_desc.xml
similarity index 87%
rename from app/src/main/res/xml/accessibility_service_description.xml
rename to app/src/main/res/xml/ab_desc.xml
index 441210b..9cf33a9 100644
--- a/app/src/main/res/xml/accessibility_service_description.xml
+++ b/app/src/main/res/xml/ab_desc.xml
@@ -5,6 +5,7 @@
android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews|flagDefault|flagRetrieveInteractiveWindows"
android:canPerformGestures="true"
android:canRetrieveWindowContent="true"
- android:description="@string/accessibility_service_description"
+ android:description="@string/ab_desc"
android:notificationTimeout="100"
+ android:canTakeScreenshot="true"
android:settingsActivity="li.songe.gkd.MainActivity" />
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..580c03d
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/src/test/java/li/songe/gkd/ExampleUnitTest.kt b/app/src/test/java/li/songe/gkd/ExampleUnitTest.kt
index d068378..6d956ad 100644
--- a/app/src/test/java/li/songe/gkd/ExampleUnitTest.kt
+++ b/app/src/test/java/li/songe/gkd/ExampleUnitTest.kt
@@ -1,13 +1,6 @@
package li.songe.gkd
-import kotlinx.serialization.decodeFromString
-import li.songe.gkd.debug.Snapshot
-import li.songe.gkd.util.Singleton
-import li.songe.selector_core.NodeExt
-import li.songe.selector_core.Selector
import org.junit.Test
-import java.io.File
-import li.songe.gkd.debug.NodeSnapshot as ApiNode
/**
* Example local unit test, which will execute on the development machine (host).
@@ -15,80 +8,10 @@ import li.songe.gkd.debug.NodeSnapshot as ApiNode
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
+
@Test
- fun check_selector() {
-// println(Selector.parse("X View >n Text > Button[a=1][b=false][c=null][d!=`hello`] + A - X < Z"))
-// println(Selector.parse("A[a=1][a!=3][a*=3][a!*=3][a^=null]"))
-// println(Selector.parse("@LinearLayout > TextView[id=`com.byted.pangle:id/tt_item_tv`][text=`不感兴趣`]"))
+ fun check_parser_selector() {
- val s1 =
- "TextView[text$=`的广告`] - Image[id=null]" to "D:/User/Downloads/edge/snapshot-1684652764082/snapshot.json"
-
- val selector = Selector.parse(s1.first)
- val nodes =
- Singleton.json.decodeFromString(File(s1.second).readText()).nodes
- ?: emptyList()
-
- val simpleNodes = nodes.map { n ->
- SimpleNode(
- value = n
- )
- }
- simpleNodes.forEach { simpleNode ->
- simpleNode.parent = simpleNodes.getOrNull(simpleNode.value.pid)?.apply {
- children.add(simpleNode)
- }
- }
- val rootWrapper = simpleNodes.map { SimpleNodeWrapper(it) }[0]
- println(rootWrapper.querySelector(selector))
}
- class SimpleNode(
- var parent: SimpleNode? = null,
- val children: MutableList = mutableListOf(),
- val value: ApiNode
- ) {
- override fun toString(): String {
- return value.toString()
- }
- }
-
- data class SimpleNodeWrapper(val value: SimpleNode) : NodeExt {
-
- override val parent: NodeExt?
- get() = value.parent?.let { SimpleNodeWrapper(it) }
- override val children: Sequence
- get() = sequence {
- value.children.forEach { yield(SimpleNodeWrapper(it)) }
- }
- override val name: CharSequence
- get() = value.value.attr?.name ?: ""
-
- override fun attr(name: String): Any? {
- val attr = value.value.attr
- return when (name) {
- "id" -> attr?.id
- "name" -> attr?.name
- "text" -> attr?.text
- "textLen" -> attr?.text?.length
- "desc" -> attr?.desc
- "descLen" -> attr?.desc?.length
- "isClickable" -> attr?.isClickable
- "isChecked" -> null
- "index" -> {
- val children = value.parent?.children ?: return null
- children.forEachIndexed { index, simpleNode ->
- if (simpleNode as SimpleNodeWrapper == this) {
- return index
- }
- }
- return null
- }
-
- "_id" -> value.value.id
- "_pid" -> value.value.pid
- else -> null
- }
- }
- }
}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 32bf567..4ecac37 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,15 +10,25 @@ buildscript {
classpath(libs.android.gradle)
classpath(libs.kotlin.gradle.plugin)
classpath(libs.kotlin.serialization)
- classpath(libs.rikka.gradle)
+// classpath(libs.rikka.gradle)
}
}
-// https://youtrack.jetbrains.com/issue/KT-33191/
-tasks.register("clean").configure {
- delete(rootProject.buildDir)
+// https://youtrack.jetbrains.com/issue/KTIJ-19369
+@Suppress(
+ "DSL_SCOPE_VIOLATION",
+)
+plugins {
+ alias(libs.plugins.google.ksp) apply false
+ alias(libs.plugins.rikka.refine) apply false
}
+// can not work with Kotlin Multiplatform
+// https://youtrack.jetbrains.com/issue/KT-33191/
+//tasks.register("clean").configure {
+// delete(rootProject.buildDir)
+//}
+
project.gradle.taskGraph.whenReady {
allTasks.forEach { task ->
// error: The binary version of its metadata is 1.8.0, expected version is 1.6.0.
@@ -27,4 +37,10 @@ project.gradle.taskGraph.whenReady {
task.enabled = false
}
}
+}
+
+// https://kotlinlang.org/docs/js-project-setup.html#use-pre-installed-node-js
+rootProject.plugins.withType {
+ rootProject.the().download =
+ false
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 3efce85..111bf09 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -23,3 +23,5 @@ kotlin.code.style=official
#android.experimental.legacyTransform.forceNonIncremental=true
android.debug.obsoleteApi=true
kotlin.js.compiler=ir
+kotlin.mpp.enableGranularSourceSetsMetadata=true
+kotlin.native.enableDependencyPropagation=false
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index c60b1fb..17eedbf 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Wed Oct 13 10:13:24 CST 2021
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/router/.gitignore b/hidden_api/.gitignore
similarity index 100%
rename from router/.gitignore
rename to hidden_api/.gitignore
diff --git a/hidden_api/build.gradle.kts b/hidden_api/build.gradle.kts
new file mode 100644
index 0000000..4aec901
--- /dev/null
+++ b/hidden_api/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ id("com.android.library")
+}
+
+android {
+ namespace = "li.songe.gkd"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.android.buildToolsVersion.get()
+
+ defaultConfig {
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ targetSdk = libs.versions.android.targetSdk.get().toInt()
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ buildFeatures {
+ aidl = true
+ buildConfig = false
+ }
+}
+
+dependencies {
+ annotationProcessor(libs.rikka.processor)
+ compileOnly(libs.rikka.annotation)
+}
\ No newline at end of file
diff --git a/hidden_api/src/main/AndroidManifest.xml b/hidden_api/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/hidden_api/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/hidden_api/src/main/aidl/android/accessibilityservice/IAccessibilityServiceClient.aidl b/hidden_api/src/main/aidl/android/accessibilityservice/IAccessibilityServiceClient.aidl
new file mode 100644
index 0000000..cbb1295
--- /dev/null
+++ b/hidden_api/src/main/aidl/android/accessibilityservice/IAccessibilityServiceClient.aidl
@@ -0,0 +1,5 @@
+// IAccessibilityServiceClient.aidl
+package android.accessibilityservice;
+
+interface IAccessibilityServiceClient {
+}
diff --git a/hidden_api/src/main/aidl/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/hidden_api/src/main/aidl/android/accessibilityservice/IAccessibilityServiceConnection.aidl
new file mode 100644
index 0000000..55a1458
--- /dev/null
+++ b/hidden_api/src/main/aidl/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -0,0 +1,7 @@
+// IAccessibilityServiceConnection.aidl
+package android.accessibilityservice;
+
+// Declare any non-default types here with import statements
+
+interface IAccessibilityServiceConnection {
+}
\ No newline at end of file
diff --git a/hidden_api/src/main/aidl/android/app/IUiAutomationConnection.aidl b/hidden_api/src/main/aidl/android/app/IUiAutomationConnection.aidl
new file mode 100644
index 0000000..1e66190
--- /dev/null
+++ b/hidden_api/src/main/aidl/android/app/IUiAutomationConnection.aidl
@@ -0,0 +1,19 @@
+package android.app;
+
+import android.accessibilityservice.IAccessibilityServiceClient;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.view.InputEvent;
+import android.os.ParcelFileDescriptor;
+
+interface IUiAutomationConnection {
+ void connect(IAccessibilityServiceClient client, int flags);
+ void disconnect();
+ boolean injectInputEvent(in InputEvent event, boolean sync);
+ void syncInputTransactions();
+ boolean setRotation(int rotation);
+ Bitmap takeScreenshot(in Rect crop, int rotation);
+ void executeShellCommand(String command, in ParcelFileDescriptor sink,
+ in ParcelFileDescriptor source);
+ oneway void shutdown();
+}
diff --git a/hidden_api/src/main/java/android/accessibilityservice/AccessibilityServiceHidden.java b/hidden_api/src/main/java/android/accessibilityservice/AccessibilityServiceHidden.java
new file mode 100644
index 0000000..f66e949
--- /dev/null
+++ b/hidden_api/src/main/java/android/accessibilityservice/AccessibilityServiceHidden.java
@@ -0,0 +1,43 @@
+
+package android.accessibilityservice;
+
+import android.graphics.Region;
+import android.os.IBinder;
+import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityEvent;
+
+import dev.rikka.tools.refine.RefineAs;
+
+@SuppressWarnings("unused")
+@RefineAs(AccessibilityService.class)
+public class AccessibilityServiceHidden {
+
+ public interface Callbacks {
+ void onAccessibilityEvent(AccessibilityEvent event);
+
+ void onInterrupt();
+
+ void onServiceConnected();
+
+ void init(int connectionId, IBinder windowToken);
+
+ boolean onGesture(int gestureId);
+
+ boolean onKeyEvent(KeyEvent event);
+
+ void onMagnificationChanged(int displayId, Region region,
+ float scale, float centerX, float centerY);
+
+ void onSoftKeyboardShowModeChanged(int showMode);
+
+ void onPerformGestureResult(int sequence, boolean completedSuccessfully);
+
+ void onFingerprintCapturingGesturesChanged(boolean active);
+
+ void onFingerprintGesture(int gesture);
+
+ void onAccessibilityButtonClicked();
+
+ void onAccessibilityButtonAvailabilityChanged(boolean available);
+ }
+}
diff --git a/hidden_api/src/main/java/android/app/ActivityManagerNative.java b/hidden_api/src/main/java/android/app/ActivityManagerNative.java
new file mode 100644
index 0000000..c3cb9a8
--- /dev/null
+++ b/hidden_api/src/main/java/android/app/ActivityManagerNative.java
@@ -0,0 +1,11 @@
+package android.app;
+
+import android.os.IBinder;
+
+@SuppressWarnings("unused")
+public class ActivityManagerNative {
+
+ public static IActivityManager asInterface(IBinder obj) {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/hidden_api/src/main/java/android/app/ActivityThread.java b/hidden_api/src/main/java/android/app/ActivityThread.java
new file mode 100644
index 0000000..fa77aff
--- /dev/null
+++ b/hidden_api/src/main/java/android/app/ActivityThread.java
@@ -0,0 +1,13 @@
+package android.app;
+
+@SuppressWarnings("unused")
+public class ActivityThread {
+
+ public static ActivityThread currentActivityThread() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public ContextImpl getSystemContext() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/hidden_api/src/main/java/android/app/ContextImpl.java b/hidden_api/src/main/java/android/app/ContextImpl.java
new file mode 100644
index 0000000..7575dab
--- /dev/null
+++ b/hidden_api/src/main/java/android/app/ContextImpl.java
@@ -0,0 +1,8 @@
+package android.app;
+
+import android.content.Context;
+
+@SuppressWarnings("unused")
+public abstract class ContextImpl extends Context {
+
+}
diff --git a/hidden_api/src/main/java/android/app/IActivityManager.java b/hidden_api/src/main/java/android/app/IActivityManager.java
new file mode 100644
index 0000000..96ad574
--- /dev/null
+++ b/hidden_api/src/main/java/android/app/IActivityManager.java
@@ -0,0 +1,26 @@
+package android.app;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+
+import java.util.List;
+
+@SuppressWarnings("unused")
+public interface IActivityManager extends IInterface {
+ List getRunningAppProcesses() throws RemoteException;
+
+ List getTasks(int maxNum) throws RemoteException;
+
+ List getFilteredTasks(int maxNum, int ignoreActivityType, int ignoreWindowingMode) throws RemoteException;
+
+ void forceStopPackage(String packageName, int userId);
+
+ abstract class Stub extends Binder implements IActivityManager {
+
+ public static IActivityManager asInterface(IBinder obj) {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/hidden_api/src/main/java/android/app/IActivityTaskManager.java b/hidden_api/src/main/java/android/app/IActivityTaskManager.java
new file mode 100644
index 0000000..59105cb
--- /dev/null
+++ b/hidden_api/src/main/java/android/app/IActivityTaskManager.java
@@ -0,0 +1,18 @@
+package android.app;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+
+import java.util.List;
+
+@SuppressWarnings("unused")
+public interface IActivityTaskManager extends IInterface {
+ List getTasks(int maxNum, boolean filterOnlyVisibleRecents, boolean keepIntentExtra);
+
+ abstract class Stub extends Binder implements IActivityTaskManager {
+ public static IActivityTaskManager asInterface(IBinder obj) {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/hidden_api/src/main/java/android/app/UiAutomationConnection.java b/hidden_api/src/main/java/android/app/UiAutomationConnection.java
new file mode 100644
index 0000000..4069648
--- /dev/null
+++ b/hidden_api/src/main/java/android/app/UiAutomationConnection.java
@@ -0,0 +1,9 @@
+
+package android.app;
+
+@SuppressWarnings("unused")
+public class UiAutomationConnection extends IUiAutomationConnection.Default {
+ public UiAutomationConnection() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/hidden_api/src/main/java/android/app/UiAutomationHidden.java b/hidden_api/src/main/java/android/app/UiAutomationHidden.java
new file mode 100644
index 0000000..ecd9f0b
--- /dev/null
+++ b/hidden_api/src/main/java/android/app/UiAutomationHidden.java
@@ -0,0 +1,31 @@
+package android.app;
+
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+
+import dev.rikka.tools.refine.RefineAs;
+
+@SuppressWarnings("unused")
+@RefineAs(UiAutomation.class)
+public class UiAutomationHidden {
+
+ public UiAutomationHidden(Looper looper, IUiAutomationConnection connection) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void connect() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void connect(int flag) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public void disconnect() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public ParcelFileDescriptor[] executeShellCommandRwe(String command) {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/hidden_api/src/main/java/android/content/pm/IPackageManager.java b/hidden_api/src/main/java/android/content/pm/IPackageManager.java
new file mode 100644
index 0000000..dea8e1c
--- /dev/null
+++ b/hidden_api/src/main/java/android/content/pm/IPackageManager.java
@@ -0,0 +1,25 @@
+package android.content.pm;
+
+import android.content.ComponentName;
+import android.os.IBinder;
+import android.os.IInterface;
+
+import java.util.List;
+
+@SuppressWarnings("unused")
+public interface IPackageManager extends IInterface {
+ ComponentName getHomeActivities(List outHomeCandidates);
+
+ void setComponentEnabledSetting(ComponentName componentName, int newState, int flags, int userId);
+
+ ApplicationInfo getApplicationInfo(String packageName, long flags, int userId);
+
+ PackageInfo getPackageInfo(String packageName, long flags, int userId);
+
+ abstract class Stub {
+
+ public static IPackageManager asInterface(IBinder obj) {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/hidden_api/src/main/java/android/hardware/input/IInputManager.java b/hidden_api/src/main/java/android/hardware/input/IInputManager.java
new file mode 100644
index 0000000..bb53e4f
--- /dev/null
+++ b/hidden_api/src/main/java/android/hardware/input/IInputManager.java
@@ -0,0 +1,19 @@
+package android.hardware.input;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+import android.view.InputEvent;
+
+@SuppressWarnings("unused")
+public interface IInputManager extends IInterface {
+ boolean injectInputEvent(InputEvent event, int mode) throws RemoteException;
+
+ abstract class Stub extends Binder implements IInputManager {
+
+ public static IInputManager asInterface(IBinder obj) {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/router/build.gradle.kts b/router/build.gradle.kts
deleted file mode 100644
index 3a855ab..0000000
--- a/router/build.gradle.kts
+++ /dev/null
@@ -1,40 +0,0 @@
-plugins {
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-android {
- namespace = "li.songe.router"
- compileSdk = libs.versions.android.compileSdk.get().toInt()
- buildToolsVersion = libs.versions.android.buildToolsVersion.get()
- defaultConfig {
- minSdk = libs.versions.android.minSdk.get().toInt()
- targetSdk = libs.versions.android.targetSdk.get().toInt()
- }
- buildTypes {
- release {
- isMinifyEnabled = false
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = "1.8"
- }
- buildFeatures {
- compose = true
- }
- composeOptions {
- kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
- }
-}
-
-dependencies {
- implementation(libs.others.utilcodex)
- implementation(libs.compose.ui)
- implementation(libs.compose.material)
- implementation(libs.compose.activity)
- implementation(libs.androidx.core.ktx)
-}
\ No newline at end of file
diff --git a/router/src/main/AndroidManifest.xml b/router/src/main/AndroidManifest.xml
deleted file mode 100644
index 44008a4..0000000
--- a/router/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/router/src/main/java/li/songe/router/Ext.kt b/router/src/main/java/li/songe/router/Ext.kt
deleted file mode 100644
index dc18ffa..0000000
--- a/router/src/main/java/li/songe/router/Ext.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package li.songe.router
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.compositionLocalOf
-import kotlinx.coroutines.CoroutineScope
-
-val LocalRoute = compositionLocalOf { error("not default value for Route") }
-val LocalRouter = compositionLocalOf { error("not default value for Router") }
-internal val LocalRouteShow = compositionLocalOf { true }
-
-const val pageTransitionDurationMillis = 300
-
-@Composable
-fun usePageShow(block: suspend CoroutineScope.() -> Unit) {
- val show = LocalRouteShow.current
- LaunchedEffect(show) {
- if (show) {
- block()
- }
- }
-}
-
-@Composable
-fun usePageHide(block: suspend CoroutineScope.() -> Unit) {
- val show = LocalRouteShow.current
- LaunchedEffect(show) {
- if (!show) {
- block()
- }
- }
-}
\ No newline at end of file
diff --git a/router/src/main/java/li/songe/router/Page.kt b/router/src/main/java/li/songe/router/Page.kt
deleted file mode 100644
index fac2b52..0000000
--- a/router/src/main/java/li/songe/router/Page.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package li.songe.router
-
-import androidx.compose.runtime.Composable
-
-open class Page(
- val name: String ="default_name",
- val content: @Composable () -> Unit,
-) {
- @Composable
- inline operator fun invoke() {
- content()
- }
-}
\ No newline at end of file
diff --git a/router/src/main/java/li/songe/router/Route.kt b/router/src/main/java/li/songe/router/Route.kt
deleted file mode 100644
index 77f2bb0..0000000
--- a/router/src/main/java/li/songe/router/Route.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package li.songe.router
-
-data class Route(
- val page: Page,
- val data: Any? = null,
- val onBack: (result: Any?) -> Unit
-)
diff --git a/router/src/main/java/li/songe/router/Router.kt b/router/src/main/java/li/songe/router/Router.kt
deleted file mode 100644
index 32d932d..0000000
--- a/router/src/main/java/li/songe/router/Router.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package li.songe.router
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.async
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-data class Router(
- private val pushHistory: suspend (Route) -> Unit,
- private val backHistory: suspend (result: Any?) -> Unit,
- private val scope: CoroutineScope,
-) {
-
- fun navigate(page: Page, data: Any? = null) = scope.launch {
- navigateForResult(page, data)
- }
-
- suspend fun navigateForResult(page: Page, data: Any? = null): T {
- return suspendCoroutine { continuation ->
- scope.launch {
- pushHistory(Route(
- page,
- data,
- ) { result ->
- continuation.resume(result as T)
- })
- }
- }
- }
-
- fun back(result: Any? = null) = scope.launch {
- backHistory(result)
- }
-}
\ No newline at end of file
diff --git a/router/src/main/java/li/songe/router/RouterHost.kt b/router/src/main/java/li/songe/router/RouterHost.kt
deleted file mode 100644
index a3fb1b0..0000000
--- a/router/src/main/java/li/songe/router/RouterHost.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-package li.songe.router
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.TweenSpec
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
-import com.blankj.utilcode.util.ScreenUtils
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.async
-import kotlinx.coroutines.launch
-import java.util.ArrayDeque
-
-@Composable
-fun RouterHost(startPage: Page) {
- val screenWidth = remember {
- ScreenUtils.getScreenWidth()
- }
- val scope = rememberCoroutineScope()
-
- var history by remember {
- mutableStateOf(
- listOf(
- Route(startPage, null) {} to Animatable(
- 0,
- Int.VectorConverter
- )
- )
- )
- }
- val tasks = remember {
- ArrayDeque>()
- }
-
- val router = remember {
- Router({ newRoute ->
- tasks.add(scope.async {
- val newAnim = Animatable(
- screenWidth,
- Int.VectorConverter
- )
- val lastAnim = history.last().second
- history = history.toMutableList().apply {
- add(newRoute to newAnim)
- }
- val t1 = scope.launch {
- newAnim.animateTo(
- 0,
- TweenSpec(
- pageTransitionDurationMillis,
- easing = FastOutSlowInEasing
- )
- )
- }
- val t2 = scope.launch {
- lastAnim.animateTo(
- screenWidth / -3,
- TweenSpec(
- pageTransitionDurationMillis,
- easing = FastOutSlowInEasing
- )
- )
- }
- t1.join()
- t2.join()
- })
- while (tasks.isNotEmpty()) {
- tasks.removeFirst().await()
- }
- }, { result ->
- tasks.add(scope.async {
- val currentAnim = history.last().second
- val currentRoute = history.last().first
- val lastAnim = history[history.size - 2].second
- val t1 = scope.launch {
- currentAnim.animateTo(
- screenWidth,
- TweenSpec(
- pageTransitionDurationMillis,
- easing = FastOutSlowInEasing
- )
- )
- }
- val t2 = scope.launch {
- lastAnim.animateTo(
- 0,
- TweenSpec(
- pageTransitionDurationMillis,
- easing = FastOutSlowInEasing
- )
- )
- }
- t1.join()
- t2.join()
- currentRoute.onBack(result)
- history = history.toMutableList().apply { removeLast() }
- })
- while (tasks.isNotEmpty()) {
- tasks.removeFirst().await()
- }
- }, scope)
- }
-
- BackHandler(history.size > 1) {
- router.back()
- }
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.White)
- ) {
- history.forEachIndexed { index, pair ->
- key(index) {
- CompositionLocalProvider(LocalRouter provides router) {
- val route = pair.first
- val anim = pair.second
- Box(
- modifier = Modifier
- .fillMaxSize()
- .clipToBounds()
- .graphicsLayer {
- translationX = anim.value.toFloat()
- }
- .background(Color.White)
- ) {
- key(Unit) {
- CompositionLocalProvider(
- LocalRoute provides route,
- LocalRouteShow provides (route == history.last().first)
- ) {
- route.page()
- }
- }
- Box(modifier = Modifier
- .fillMaxSize()
- .graphicsLayer {
- alpha = 0.5f * (anim.value / (screenWidth / -3f))
- }
- .background(Color.Black)
- )
- }
- }
- }
- }
- }
-}
-
-
-
-
-
-
-
-
diff --git a/selector/build.gradle.kts b/selector/build.gradle.kts
new file mode 100644
index 0000000..fe87c3a
--- /dev/null
+++ b/selector/build.gradle.kts
@@ -0,0 +1,31 @@
+plugins {
+ kotlin("multiplatform")
+ kotlin("plugin.serialization")
+}
+
+kotlin {
+ jvm {
+ compilations.all {
+ kotlinOptions.jvmTarget = JavaVersion.VERSION_17.majorVersion
+ }
+ }
+// https://kotlinlang.org/docs/js-to-kotlin-interop.html#kotlin-types-in-javascript
+ js(IR) {
+ binaries.executable()
+// useEsModules() many bugs
+// bug example kotlin CharSequence.contains(char: Char) not work with js string.includes(string)
+ generateTypeScriptDefinitions()
+ browser {}
+ }
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(kotlin("stdlib-common"))
+ }
+ }
+ }
+ sourceSets["jvmTest"].dependencies {
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.junit)
+ }
+}
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/CommonSelector.kt b/selector/src/commonMain/kotlin/li/songe/selector/CommonSelector.kt
new file mode 100644
index 0000000..5c6820b
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/CommonSelector.kt
@@ -0,0 +1,30 @@
+package li.songe.selector
+
+import kotlin.js.ExperimentalJsExport
+import kotlin.js.JsExport
+
+
+@OptIn(ExperimentalJsExport::class)
+@JsExport
+class CommonSelector private constructor(
+ internal val selector: Selector,
+) {
+ val tracks = selector.tracks
+ val trackIndex = selector.trackIndex
+
+ fun match(node: T, transform: CommonTransform): T? {
+ return selector.match(node, transform.transform)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun matchTrack(node: T, transform: CommonTransform): Array? {
+ return selector.matchTracks(node, transform.transform)?.toTypedArray() as Array?
+ }
+
+ override fun toString() = selector.toString()
+
+ companion object {
+ fun parse(source: String) = CommonSelector(Selector.parse(source))
+ }
+}
+
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/CommonTransform.kt b/selector/src/commonMain/kotlin/li/songe/selector/CommonTransform.kt
new file mode 100644
index 0000000..a08c737
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/CommonTransform.kt
@@ -0,0 +1,46 @@
+package li.songe.selector
+
+import kotlin.js.ExperimentalJsExport
+import kotlin.js.JsExport
+
+@OptIn(ExperimentalJsExport::class)
+@JsExport
+class CommonTransform(
+ getAttr: (T, String) -> Any?,
+ getName: (T) -> String?,
+ getChildren: (T) -> Array,
+ getParent: (T) -> T?,
+) {
+ internal val transform = Transform(
+ getAttr = getAttr,
+ getName = getName,
+ getChildren = { node -> getChildren(node).asSequence() },
+ getParent = getParent,
+ )
+
+ @Suppress("UNCHECKED_CAST")
+ val querySelectorAll: (T, CommonSelector) -> Array = { node, selector ->
+ val result =
+ transform.querySelectorAll(node, selector.selector).toList().toTypedArray()
+ result as Array
+ }
+
+ val querySelector: (T, CommonSelector) -> T? = { node, selector ->
+ transform.querySelectorAll(node, selector.selector).firstOrNull()
+ }
+
+
+ @Suppress("UNCHECKED_CAST")
+ val querySelectorTrackAll: (T, CommonSelector) -> Array> = { node, selector ->
+ val result = transform.querySelectorTrackAll(node, selector.selector)
+ .map { it.toTypedArray() as Array }.toList().toTypedArray()
+ result as Array>
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val querySelectorTrack: (T, CommonSelector) -> Array? = { node, selector ->
+ transform.querySelectorTrackAll(node, selector.selector).firstOrNull()
+ ?.toTypedArray() as Array?
+ }
+
+}
\ No newline at end of file
diff --git a/selector_core/src/main/java/li/songe/selector_core/parser/ExtSyntaxError.kt b/selector/src/commonMain/kotlin/li/songe/selector/ExtSyntaxError.kt
similarity index 51%
rename from selector_core/src/main/java/li/songe/selector_core/parser/ExtSyntaxError.kt
rename to selector/src/commonMain/kotlin/li/songe/selector/ExtSyntaxError.kt
index baadd0d..632c0af 100644
--- a/selector_core/src/main/java/li/songe/selector_core/parser/ExtSyntaxError.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/ExtSyntaxError.kt
@@ -1,14 +1,22 @@
-package li.songe.selector_core.parser
+package li.songe.selector
-data class ExtSyntaxError(val expectedValue: String, val position: Int, val source: String) :
- Exception(
- "expected $expectedValue in selector at position $position, but got ${
- source.getOrNull(
- position
- )
- }"
- ) {
- companion object {
+import kotlin.js.ExperimentalJsExport
+import kotlin.js.JsExport
+
+@OptIn(ExperimentalJsExport::class)
+@JsExport
+data class ExtSyntaxError internal constructor(
+ val expectedValue: String,
+ val position: Int,
+ val source: String,
+) : Exception(
+ "expected $expectedValue in selector at position $position, but got ${
+ source.getOrNull(
+ position
+ )
+ }"
+) {
+ internal companion object {
fun assert(source: String, offset: Int, value: String = "", expectedValue: String? = null) {
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
throw ExtSyntaxError(expectedValue ?: value, offset, source)
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt b/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt
new file mode 100644
index 0000000..8e3ac70
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt
@@ -0,0 +1,14 @@
+package li.songe.selector
+
+internal interface NodeMatchFc {
+ operator fun invoke(node: T, transform: Transform): Boolean
+}
+
+internal interface NodeSequenceFc {
+ operator fun invoke(sequence: Sequence): Sequence
+}
+
+internal interface NodeTraversalFc {
+ operator fun invoke(node: T, transform: Transform): Sequence
+}
+
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt
new file mode 100644
index 0000000..5e52098
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt
@@ -0,0 +1,46 @@
+package li.songe.selector
+
+import li.songe.selector.data.PropertyWrapper
+import li.songe.selector.parser.ParserSet
+
+
+class Selector internal constructor(private val propertyWrapper: PropertyWrapper) {
+ override fun toString(): String {
+ return propertyWrapper.toString()
+ }
+
+ val tracks by lazy {
+ val list = mutableListOf(propertyWrapper)
+ while (true) {
+ list.add(list.last().to?.to ?: break)
+ }
+ list.reverse()
+ list.map { p -> p.propertySegment.tracked }.toTypedArray()
+ }
+
+ val trackIndex = tracks.indexOfFirst { it }.let { i ->
+ if (i < 0) 0 else i
+ }
+
+ fun match(
+ node: T,
+ transform: Transform,
+ trackNodes: MutableList = mutableListOf(),
+ ): T? {
+ val trackTempNodes = matchTracks(node, transform, trackNodes) ?: return null
+ return trackTempNodes[trackIndex]
+ }
+
+ fun matchTracks(
+ node: T,
+ transform: Transform,
+ trackNodes: MutableList = mutableListOf(),
+ ): List? {
+ trackNodes.clear()
+ return propertyWrapper.matchTracks(node, transform, trackNodes)
+ }
+
+ companion object {
+ fun parse(source: String) = ParserSet.selectorParser(source)
+ }
+}
\ No newline at end of file
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt b/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt
new file mode 100644
index 0000000..4b819b2
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt
@@ -0,0 +1,103 @@
+package li.songe.selector
+
+
+class Transform(
+ val getAttr: (T, String) -> Any?,
+ val getName: (T) -> CharSequence?,
+ val getChildren: (T) -> Sequence,
+ val getChild: (T, Int) -> T? = { node, offset -> getChildren(node).elementAtOrNull(offset) },
+ val getParent: (T) -> T?,
+ val getAncestors: (T) -> Sequence = { node ->
+ sequence {
+ var parentVar: T? = getParent(node) ?: return@sequence
+ while (parentVar != null) {
+ parentVar?.let {
+ yield(it)
+ parentVar = getParent(it)
+ }
+ }
+ }
+ },
+ val getAncestor: (T, Int) -> T? = { node, offset -> getAncestors(node).elementAtOrNull(offset) },
+
+ val getBeforeBrothers: (T) -> Sequence = { node ->
+ sequence {
+ val parentVal = getParent(node) ?: return@sequence
+ val list = getChildren(parentVal).takeWhile { it != node }.toMutableList()
+ list.reverse()
+ yieldAll(list)
+ }
+ },
+ val getBeforeBrother: (T, Int) -> T? = { node, offset ->
+ getBeforeBrothers(node).elementAtOrNull(
+ offset
+ )
+ },
+
+ val getAfterBrothers: (T) -> Sequence = { node ->
+ sequence {
+ val parentVal = getParent(node) ?: return@sequence
+ yieldAll(getChildren(parentVal).dropWhile { it != node }.drop(1))
+ }
+ },
+ val getAfterBrother: (T, Int) -> T? = { node, offset ->
+ getAfterBrothers(node).elementAtOrNull(
+ offset
+ )
+ },
+
+ val getDescendants: (T) -> Sequence = { node ->
+ sequence { // 深度优先 先序遍历
+ // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector
+ val stack = mutableListOf(node)
+ val tempNodes = mutableListOf()
+ do {
+ val top = stack.removeLast()
+ yield(top)
+ for (childNode in getChildren(top)) {
+ if (childNode != null) {
+ tempNodes.add(childNode)
+ }
+ }
+ if (tempNodes.isNotEmpty()) {
+ for (i in tempNodes.size - 1 downTo 0) {
+ stack.add(tempNodes[i])
+ }
+ tempNodes.clear()
+ }
+ } while (stack.isNotEmpty())
+ }
+ },
+
+ ) {
+ val querySelectorAll: (T, Selector) -> Sequence = { node, selector ->
+ sequence {
+ val trackNodes: MutableList = mutableListOf()
+ getDescendants(node).forEach { childNode ->
+ val r = selector.match(childNode, this@Transform, trackNodes)
+ trackNodes.clear()
+ if (r != null) yield(r)
+ }
+ }
+ }
+ val querySelector: (T, Selector) -> T? = { node, selector ->
+ querySelectorAll(
+ node, selector
+ ).firstOrNull()
+ }
+ val querySelectorTrackAll: (T, Selector) -> Sequence> = { node, selector ->
+ sequence {
+ val trackNodes: MutableList = mutableListOf()
+ getDescendants(node).forEach { childNode ->
+ val r = selector.matchTracks(childNode, this@Transform, trackNodes)?.toList()
+ trackNodes.clear()
+ if (r != null) yield(r)
+ }
+ }
+ }
+ val querySelectorTrack: (T, Selector) -> List? = { node, selector ->
+ querySelectorTrackAll(
+ node, selector
+ ).firstOrNull()
+ }
+}
\ No newline at end of file
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Version.kt b/selector/src/commonMain/kotlin/li/songe/selector/Version.kt
new file mode 100644
index 0000000..e09cb8b
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/Version.kt
@@ -0,0 +1,8 @@
+package li.songe.selector
+
+import kotlin.js.ExperimentalJsExport
+import kotlin.js.JsExport
+
+@OptIn(ExperimentalJsExport::class)
+@JsExport
+const val version = "0.0.4"
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt
new file mode 100644
index 0000000..3937d7d
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt
@@ -0,0 +1,16 @@
+package li.songe.selector.data
+
+import li.songe.selector.Transform
+
+data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) {
+ fun match(node: T, transform: Transform) =
+ operator.compare(transform.getAttr(node, name), value)
+
+ override fun toString() = "${name}${operator}${
+ if (value is String) {
+ "'${value.replace("'", "\\'")}'"
+ } else {
+ value
+ }
+ }"
+}
diff --git a/selector_core/src/main/java/li/songe/selector_core/data/CompareOperator.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt
similarity index 98%
rename from selector_core/src/main/java/li/songe/selector_core/data/CompareOperator.kt
rename to selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt
index abd61f6..3671b38 100644
--- a/selector_core/src/main/java/li/songe/selector_core/data/CompareOperator.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt
@@ -1,4 +1,4 @@
-package li.songe.selector_core.data
+package li.songe.selector.data
sealed class CompareOperator(val key: String) {
override fun toString() = key
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectOperator.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectOperator.kt
new file mode 100644
index 0000000..c88df67
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectOperator.kt
@@ -0,0 +1,62 @@
+package li.songe.selector.data
+
+import li.songe.selector.Transform
+
+sealed class ConnectOperator(val key: String) {
+ override fun toString() = key
+ abstract fun traversal(node: T, transform: Transform): Sequence
+ abstract fun traversal(node: T, transform: Transform, offset: Int): T?
+
+ companion object {
+ val allSubClasses = listOf(
+ BeforeBrother,
+ AfterBrother,
+ Ancestor,
+ Child
+ ).sortedBy { -it.key.length }
+ }
+
+ /**
+ * A + B, 1,2,3,A,B,7,8
+ */
+ object BeforeBrother : ConnectOperator("+") {
+ override fun traversal(node: T, transform: Transform) =
+ transform.getBeforeBrothers(node)
+
+ override fun traversal(node: T, transform: Transform, offset: Int): T? =
+ transform.getBeforeBrother(node, offset)
+ }
+
+ /**
+ * A - B, 1,2,3,B,A,7,8
+ */
+ object AfterBrother : ConnectOperator("-") {
+ override fun traversal(node: T, transform: Transform) =
+ transform.getAfterBrothers(node)
+
+ override fun traversal(node: T, transform: Transform, offset: Int): T? =
+ transform.getAfterBrother(node, offset)
+ }
+
+ /**
+ * A > B, A is the ancestor of B
+ */
+ object Ancestor : ConnectOperator(">") {
+ override fun traversal(node: T, transform: Transform) =
+ transform.getAncestors(node)
+
+ override fun traversal(node: T, transform: Transform, offset: Int): T? =
+ transform.getAncestor(node, offset)
+ }
+
+ /**
+ * A < B, A is the child of B
+ */
+ object Child : ConnectOperator("<") {
+ override fun traversal(node: T, transform: Transform) =
+ transform.getChildren(node)
+
+ override fun traversal(node: T, transform: Transform, offset: Int): T? =
+ transform.getChild(node, offset)
+ }
+}
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectSegment.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectSegment.kt
new file mode 100644
index 0000000..cbfaa58
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectSegment.kt
@@ -0,0 +1,36 @@
+package li.songe.selector.data
+
+import li.songe.selector.Transform
+import li.songe.selector.NodeTraversalFc
+
+data class ConnectSegment(
+ val operator: ConnectOperator = ConnectOperator.Ancestor,
+ val polynomialExpression: PolynomialExpression = PolynomialExpression()
+) {
+ override fun toString(): String {
+ if (operator == ConnectOperator.Ancestor && polynomialExpression.a == 1 && polynomialExpression.b == 0) {
+ return ""
+ }
+ return operator.toString() + polynomialExpression.toString()
+ }
+
+ internal val traversal = if (polynomialExpression.isConstant) {
+ object : NodeTraversalFc {
+ override fun invoke(node: T, transform: Transform): Sequence = sequence {
+ val node1 = operator.traversal(node, transform, polynomialExpression.b1)
+ if (node1 != null) {
+ yield(node1)
+ }
+ }
+ }
+ } else {
+ object : NodeTraversalFc {
+ override fun invoke(node: T, transform: Transform): Sequence {
+ return polynomialExpression.traversal(
+ operator.traversal(node, transform)
+ )
+ }
+ }
+ }
+
+}
diff --git a/selector_core/src/main/java/li/songe/selector_core/data/ConnectWrapper.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectWrapper.kt
similarity index 50%
rename from selector_core/src/main/java/li/songe/selector_core/data/ConnectWrapper.kt
rename to selector/src/commonMain/kotlin/li/songe/selector/data/ConnectWrapper.kt
index a1f3b50..2bfea54 100644
--- a/selector_core/src/main/java/li/songe/selector_core/data/ConnectWrapper.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectWrapper.kt
@@ -1,6 +1,6 @@
-package li.songe.selector_core.data
+package li.songe.selector.data
-import li.songe.selector_core.NodeExt
+import li.songe.selector.Transform
data class ConnectWrapper(
val connectSegment: ConnectSegment,
@@ -10,13 +10,13 @@ data class ConnectWrapper(
return (to.toString() + "\u0020" + connectSegment.toString()).trim()
}
- fun match(
- node: NodeExt,
- trackNodes: MutableList = mutableListOf(),
- ): List? {
- connectSegment.traversal(node).forEach {
+ fun matchTracks(
+ node: T, transform: Transform,
+ trackNodes: MutableList = mutableListOf(),
+ ): List? {
+ connectSegment.traversal(node, transform).forEach {
if (it == null) return@forEach
- val r = to.match(it, trackNodes)
+ val r = to.matchTracks(it, transform, trackNodes)
if (r != null) return r
}
return null
diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/OrExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/OrExpression.kt
new file mode 100644
index 0000000..b9196fd
--- /dev/null
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/OrExpression.kt
@@ -0,0 +1,14 @@
+package li.songe.selector.data
+
+import li.songe.selector.Transform
+
+data class OrExpression(val expressions: List) {
+ override fun toString(): String {
+ if (expressions.isEmpty()) return ""
+ return "[" + expressions.joinToString("||") + "]"
+ }
+
+ fun match(node: T, transform: Transform): Boolean {
+ return expressions.any { ex -> ex.match(node, transform) }
+ }
+}
diff --git a/selector_core/src/main/java/li/songe/selector_core/data/PolynomialExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PolynomialExpression.kt
similarity index 61%
rename from selector_core/src/main/java/li/songe/selector_core/data/PolynomialExpression.kt
rename to selector/src/commonMain/kotlin/li/songe/selector/data/PolynomialExpression.kt
index f989046..25a3556 100644
--- a/selector_core/src/main/java/li/songe/selector_core/data/PolynomialExpression.kt
+++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PolynomialExpression.kt
@@ -1,6 +1,6 @@
-package li.songe.selector_core.data
+package li.songe.selector.data
-import li.songe.selector_core.NodeExt
+import li.songe.selector.NodeSequenceFc
/**
* an+b
@@ -35,11 +35,19 @@ data class PolynomialExpression(val a: Int = 0, val b: Int = 1) {
*/
val b1 = b - 1
- val traversal: (Sequence) -> Sequence =
- if (a <= 0 && b <= 0) ({ emptySequence() })
- else ({ sequence ->
- sequence.filterIndexed { x, _ -> (x - b1) % a == 0 && (x - b1) / a > 0 }
- })
+ internal val traversal = if (a <= 0 && b <= 0) {
+ object : NodeSequenceFc {
+ override fun invoke(sequence: Sequence): Sequence {
+ return emptySequence()
+ }
+ }
+ } else {
+ object : NodeSequenceFc {
+ override fun invoke(sequence: Sequence