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): Sequence { + return sequence.filterIndexed { x, _ -> (x - b1) % a == 0 && (x - b1) / a > 0 } + } + } + } val isConstant = a == 0 } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt new file mode 100644 index 0000000..66c543a --- /dev/null +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt @@ -0,0 +1,43 @@ +package li.songe.selector.data + +import li.songe.selector.NodeMatchFc +import li.songe.selector.Transform + + +data class PropertySegment( + /** + * 此属性选择器是否被 @ 标记 + */ + val tracked: Boolean, + val name: String, + val expressions: List, +) { + override fun toString(): String { + val matchTag = if (tracked) "@" else "" + return matchTag + name + expressions.joinToString("") + } + + private val matchName = if (name.isBlank() || name == "*") { + object : NodeMatchFc { + override fun invoke(node: T, transform: Transform) = true + } + } else { + object : NodeMatchFc { + override fun invoke(node: T, transform: Transform): Boolean { + val str = transform.getName(node) ?: return false + return (str.contentEquals(name) || (str.endsWith(name) && str[str.length - name.length - 1] == '.')) + } + } + } + + fun match(node: T, transform: Transform): Boolean { + return matchName(node, transform) && expressions.all { ex -> ex.match(node, transform) } + } + +} + + + + + + diff --git a/selector_core/src/main/java/li/songe/selector_core/data/PropertyWrapper.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertyWrapper.kt similarity index 51% rename from selector_core/src/main/java/li/songe/selector_core/data/PropertyWrapper.kt rename to selector/src/commonMain/kotlin/li/songe/selector/data/PropertyWrapper.kt index f5a7dd7..dde528e 100644 --- a/selector_core/src/main/java/li/songe/selector_core/data/PropertyWrapper.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertyWrapper.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 PropertyWrapper( val propertySegment: PropertySegment, @@ -14,19 +14,18 @@ data class PropertyWrapper( }) + propertySegment.toString() } - fun match( - node: NodeExt, - trackNodes: MutableList = mutableListOf(), - ): List? { - if (!propertySegment.match(node)) { + fun matchTracks( + node: T, + transform: Transform, + trackNodes: MutableList = mutableListOf(), + ): List? { + if (!propertySegment.match(node, transform)) { return null } - if (propertySegment.tracked || trackNodes.isEmpty()) { - trackNodes.add(node) - } + trackNodes.add(node) if (to == null) { return trackNodes } - return to.match(node, trackNodes) + return to.matchTracks(node, transform, trackNodes) } } diff --git a/selector_core/src/main/java/li/songe/selector_core/parser/Parser.kt b/selector/src/commonMain/kotlin/li/songe/selector/parser/Parser.kt similarity index 54% rename from selector_core/src/main/java/li/songe/selector_core/parser/Parser.kt rename to selector/src/commonMain/kotlin/li/songe/selector/parser/Parser.kt index f556605..6332b2d 100644 --- a/selector_core/src/main/java/li/songe/selector_core/parser/Parser.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/parser/Parser.kt @@ -1,8 +1,8 @@ -package li.songe.selector_core.parser +package li.songe.selector.parser internal open class Parser( val prefix: String = "", private val temp: (source: String, offset: Int, prefix: String) -> ParserResult -) : (String, Int) -> ParserResult { - override fun invoke(source: String, offset: Int) = temp(source, offset, prefix) +) { + operator fun invoke(source: String, offset: Int) = temp(source, offset, prefix) } \ No newline at end of file diff --git a/selector_core/src/main/java/li/songe/selector_core/parser/ParserResult.kt b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserResult.kt similarity index 65% rename from selector_core/src/main/java/li/songe/selector_core/parser/ParserResult.kt rename to selector/src/commonMain/kotlin/li/songe/selector/parser/ParserResult.kt index deb7054..3839d6e 100644 --- a/selector_core/src/main/java/li/songe/selector_core/parser/ParserResult.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserResult.kt @@ -1,3 +1,3 @@ -package li.songe.selector_core.parser +package li.songe.selector.parser internal data class ParserResult(val data: T, val length: Int = 0) diff --git a/selector_core/src/main/java/li/songe/selector_core/parser/ParserSet.kt b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt similarity index 76% rename from selector_core/src/main/java/li/songe/selector_core/parser/ParserSet.kt rename to selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt index f0e41b3..d775c25 100644 --- a/selector_core/src/main/java/li/songe/selector_core/parser/ParserSet.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt @@ -1,14 +1,16 @@ -package li.songe.selector_core.parser +package li.songe.selector.parser -import li.songe.selector_core.Selector -import li.songe.selector_core.data.BinaryExpression -import li.songe.selector_core.data.CompareOperator -import li.songe.selector_core.data.ConnectOperator -import li.songe.selector_core.data.ConnectSegment -import li.songe.selector_core.data.ConnectWrapper -import li.songe.selector_core.data.PolynomialExpression -import li.songe.selector_core.data.PropertySegment -import li.songe.selector_core.data.PropertyWrapper +import li.songe.selector.ExtSyntaxError +import li.songe.selector.Selector +import li.songe.selector.data.BinaryExpression +import li.songe.selector.data.CompareOperator +import li.songe.selector.data.ConnectOperator +import li.songe.selector.data.ConnectSegment +import li.songe.selector.data.ConnectWrapper +import li.songe.selector.data.OrExpression +import li.songe.selector.data.PolynomialExpression +import li.songe.selector.data.PropertySegment +import li.songe.selector.data.PropertyWrapper internal object ParserSet { val whiteCharParser = Parser("\u0020\t\r\n") { source, offset, prefix -> @@ -28,7 +30,7 @@ internal object ParserSet { Parser("*1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_") { source, offset, prefix -> var i = offset val s0 = source.getOrNull(i) - if (s0 != null && !prefix.contains(s0)) { + if ((s0 != null) && !prefix.contains(s0)) { return@Parser ParserResult("") } ExtSyntaxError.assert(source, i, prefix, "*0-9a-zA-Z_") @@ -192,20 +194,21 @@ internal object ParserSet { } ?: ExtSyntaxError.throwError(source, offset, "CompareOperator") ParserResult(operator, operator.key.length) } - val stringParser = Parser("`") { source, offset, prefix -> + val stringParser = Parser("`'\"") { source, offset, prefix -> var i = offset ExtSyntaxError.assert(source, i, prefix) + val startChar = source[i] i++ var data = "" - while (source[i] != '`') { + while (source[i] != startChar) { if (i == source.length - 1) { - ExtSyntaxError.assert(source, i, "`") + ExtSyntaxError.assert(source, i, startChar.toString()) break } if (source[i] == '\\') { i++ ExtSyntaxError.assert(source, i) - if (source[i] == '`') { + if (source[i] == startChar) { data += source[i] ExtSyntaxError.assert(source, i + 1) } else { @@ -236,68 +239,67 @@ internal object ParserSet { ParserResult(data, i - offset) } - val valueParser = Parser("tfn`1234567890") { source, offset, prefix -> - var i = offset - ExtSyntaxError.assert(source, i, prefix) - val value: Any? = when (source[i]) { - 't' -> { - i++ - "rue".forEach { c -> - ExtSyntaxError.assert(source, i, c.toString()) + val valueParser = + Parser("tfn" + stringParser.prefix + integerParser.prefix) { source, offset, prefix -> + var i = offset + ExtSyntaxError.assert(source, i, prefix) + val value: Any? = when (source[i]) { + 't' -> { i++ + "rue".forEach { c -> + ExtSyntaxError.assert(source, i, c.toString()) + i++ + } + true } - true - } - 'f' -> { - i++ - "alse".forEach { c -> - ExtSyntaxError.assert(source, i, c.toString()) + 'f' -> { i++ + "alse".forEach { c -> + ExtSyntaxError.assert(source, i, c.toString()) + i++ + } + false } - false - } - 'n' -> { - i++ - "ull".forEach { c -> - ExtSyntaxError.assert(source, i, c.toString()) + 'n' -> { i++ + "ull".forEach { c -> + ExtSyntaxError.assert(source, i, c.toString()) + i++ + } + null } - null - } - '`' -> { - val s = stringParser(source, i) - i += s.length - s.data - } + in stringParser.prefix -> { + val s = stringParser(source, i) + i += s.length + s.data + } - in "1234567890" -> { - val n = integerParser(source, i) - i += n.length - n.data - } + in integerParser.prefix -> { + val n = integerParser(source, i) + i += n.length + n.data + } - else -> { - ExtSyntaxError.throwError(source, i, prefix) + else -> { + ExtSyntaxError.throwError(source, i, prefix) + } } + ParserResult(value, i - offset) } - ParserResult(value, i - offset) - } - val attrParser = Parser("[") { source, offset, prefix -> + val attrParser = Parser("") { source, offset, _ -> var i = offset - ExtSyntaxError.assert(source, i, prefix) - i++ val parserResult = propertyParser(source, i) i += parserResult.length + i += whiteCharParser(source, i).length val operatorResult = attrOperatorParser(source, i) i += operatorResult.length + i += whiteCharParser(source, i).length val valueResult = valueParser(source, i) i += valueResult.length - ExtSyntaxError.assert(source, i, "]") - i++ ParserResult( BinaryExpression( parserResult.data, @@ -307,6 +309,37 @@ internal object ParserSet { ) } + val orParser = Parser("[") { source, offset, prefix -> + var i = offset + ExtSyntaxError.assert(source, i, prefix) + i++ + i += whiteCharParser(source, i).length + val binaryExpressions = mutableListOf() + while (i < source.length && source[i] != ']') { + if (binaryExpressions.isNotEmpty()) { + ExtSyntaxError.assert(source, i, "|") + ExtSyntaxError.assert(source, i, "|") + i += 2 + i += whiteCharParser(source, i).length + } + val attrResult = attrParser(source, i) + i += attrResult.length + binaryExpressions.add(attrResult.data) + i += whiteCharParser(source, i).length + } + if (binaryExpressions.isEmpty()) { + ExtSyntaxError.throwError(source, i, "binaryExpression") + } + ExtSyntaxError.assert(source, i, "]") + i++ + ParserResult( + OrExpression( + binaryExpressions, + ), + i - offset + ) + } + val selectorUnitParser = Parser { source, offset, _ -> var i = offset var tracked = false @@ -316,16 +349,16 @@ internal object ParserSet { } val nameResult = nameParser(source, i) i += nameResult.length - val attrList = mutableListOf() + val orExpressions = mutableListOf() while (i < source.length && source[i] == '[') { - val attrResult = attrParser(source, i) + val attrResult = orParser(source, i) i += attrResult.length - attrList.add(attrResult.data) + orExpressions.add(attrResult.data) } - if (nameResult.length == 0 && attrList.size == 0) { + if (nameResult.length == 0 && orExpressions.size == 0) { ExtSyntaxError.throwError(source, i, "[") } - ParserResult(PropertySegment(tracked, nameResult.data, attrList), i - offset) + ParserResult(PropertySegment(tracked, nameResult.data, orExpressions), i - offset) } val connectSelectorParser = Parser { source, offset, _ -> diff --git a/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt b/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt new file mode 100644 index 0000000..fe12f97 --- /dev/null +++ b/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt @@ -0,0 +1,65 @@ +package li.songe.selector + +import junit.framework.TestCase.assertTrue +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.intOrNull +import org.junit.Test +import java.io.File + + +class ParserTest { + + @Test + fun string_selector() { + val text = "Button > View[a=1||b=2||c$=3][x=3] Image > Layout @*" + println(Selector.parse(text)) + } + + @Test + fun query_selector() { + val projectCwd = File("../").absolutePath + val text = + "* > View[isClickable=true][childCount=1][textLen=0] > Image[isClickable=false][textLen=0]" + val selector = Selector.parse(text) + println("selector: $selector") + + val jsonString = File("$projectCwd/_assets/snapshot-1686629593092.json").readText() + val json = Json { + ignoreUnknownKeys = true + } + val nodes = json.decodeFromString(jsonString).nodes + + nodes.forEach { node -> + node.parent = nodes.getOrNull(node.pid) + node.parent?.apply { + children.add(node) + } + } + val transform = Transform(getAttr = { node, name -> + val value = node.attr[name] ?: return@Transform null + if (value is JsonNull) return@Transform null + value.intOrNull ?: value.booleanOrNull ?: value.content + }, getName = { node -> node.attr["name"]?.content }, getChildren = { node -> + node.children.asSequence() + }, getParent = { node -> node.parent }) + val targets = transform.querySelectorAll(nodes.first(), selector).toList() + println("target_size: " + targets.size) + assertTrue(targets.size == 1) + println("id: " + targets.first().id) + + val trackTargets = transform.querySelectorTrackAll(nodes.first(), selector).toList() + println("trackTargets_size: " + trackTargets.size) + assertTrue(trackTargets.size == 1) + println(trackTargets.first().mapIndexed { index, testNode -> + testNode.id to selector.tracks[index] + }) + } + + @Test + fun check_parser() { + println(Selector.parse("View > Text")) + } +} \ No newline at end of file diff --git a/selector/src/jvmTest/kotlin/li/songe/selector/TestNode.kt b/selector/src/jvmTest/kotlin/li/songe/selector/TestNode.kt new file mode 100644 index 0000000..3693260 --- /dev/null +++ b/selector/src/jvmTest/kotlin/li/songe/selector/TestNode.kt @@ -0,0 +1,19 @@ +package li.songe.selector + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonPrimitive + +@Serializable +data class TestNode( + val id: Int, + val pid: Int, + val attr: Map, +) { + @Transient + var parent: TestNode? = null + + @Transient + var children: MutableList = mutableListOf() +} + diff --git a/selector/src/jvmTest/kotlin/li/songe/selector/TestSnapshot.kt b/selector/src/jvmTest/kotlin/li/songe/selector/TestSnapshot.kt new file mode 100644 index 0000000..9da7731 --- /dev/null +++ b/selector/src/jvmTest/kotlin/li/songe/selector/TestSnapshot.kt @@ -0,0 +1,8 @@ +package li.songe.selector + +import kotlinx.serialization.Serializable + +@Serializable +data class TestSnapshot( + val nodes: List +) diff --git a/selector_core/.gitignore b/selector_core/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/selector_core/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/selector_core/build.gradle.kts b/selector_core/build.gradle.kts deleted file mode 100644 index 03188ec..0000000 --- a/selector_core/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} diff --git a/selector_core/src/main/java/li/songe/selector_core/NodeExt.kt b/selector_core/src/main/java/li/songe/selector_core/NodeExt.kt deleted file mode 100644 index f729620..0000000 --- a/selector_core/src/main/java/li/songe/selector_core/NodeExt.kt +++ /dev/null @@ -1,79 +0,0 @@ -package li.songe.selector_core - -interface NodeExt { - val parent: NodeExt? - val children: Sequence - val name: CharSequence - fun attr(name: String): Any? - - /** - * constant traversal - */ - fun getChild(offset: Int) = children.elementAtOrNull(offset) - - val ancestors: Sequence - get() = sequence { - var parentVar: NodeExt? = parent ?: return@sequence - while (parentVar != null) { - yield(parentVar) - parentVar = parentVar.parent - } - } - - fun getAncestor(offset: Int) = ancestors.elementAtOrNull(offset) - - // if index=3, traverse 2,1,0 - val beforeBrothers: Sequence - get() = sequence { - val parentVal = parent ?: return@sequence - val list = parentVal.children.takeWhile { it != this@NodeExt }.toMutableList() - list.reverse() - yieldAll(list) - } - - fun getBeforeBrother(offset: Int) = beforeBrothers.elementAtOrNull(offset) - - // if index=3, traverse 4,5,6... - val afterBrothers: Sequence - get() = sequence { - val parentVal = parent ?: return@sequence - yieldAll(parentVal.children.dropWhile { it != this@NodeExt }.drop(1)) - } - - fun getAfterBrother(offset: Int) = afterBrothers.elementAtOrNull(offset) - - val descendants: Sequence - get() = sequence { -// 深度优先先序遍历 -// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector - val stack = mutableListOf(this@NodeExt) - val reverseList = mutableListOf() - do { - val top = stack.removeLast() - yield(top) - for (childNode in top.children) { - if (childNode != null) { - reverseList.add(childNode) - } - } - if (reverseList.isNotEmpty()) { - reverseList.reverse() - stack.addAll(reverseList) - reverseList.clear() - } - } while (stack.isNotEmpty()) - } - - fun querySelector(selector: Selector) = querySelectorAll(selector).firstOrNull() - - fun querySelectorAll(selector: Selector) = sequence { - descendants.forEach { node -> - val r = selector.match(node) - if (r != null) - yield(r) - } - } -} - - - diff --git a/selector_core/src/main/java/li/songe/selector_core/Selector.kt b/selector_core/src/main/java/li/songe/selector_core/Selector.kt deleted file mode 100644 index 1e6310f..0000000 --- a/selector_core/src/main/java/li/songe/selector_core/Selector.kt +++ /dev/null @@ -1,28 +0,0 @@ -package li.songe.selector_core - -import li.songe.selector_core.data.PropertyWrapper -import li.songe.selector_core.parser.ParserSet - -data class Selector(private val propertyWrapper: PropertyWrapper) { - override fun toString() = propertyWrapper.toString() -// val segments by lazy { -// sequence { -// var c = propertyWrapper.to -// yield(propertyWrapper.propertySegment) -// while (c != null) { -// yield(c!!.connectSegment) -// yield(c!!.to.propertySegment) -// c = c!!.to.to -// } -// }.toList().reversed() -// } - - fun match(node: NodeExt): NodeExt? { - val trackNodes = propertyWrapper.match(node) ?: return null - return trackNodes.lastOrNull() ?: node - } - - companion object { - fun parse(source: String) = ParserSet.selectorParser(source) - } -} \ No newline at end of file diff --git a/selector_core/src/main/java/li/songe/selector_core/data/BinaryExpression.kt b/selector_core/src/main/java/li/songe/selector_core/data/BinaryExpression.kt deleted file mode 100644 index dd21a11..0000000 --- a/selector_core/src/main/java/li/songe/selector_core/data/BinaryExpression.kt +++ /dev/null @@ -1,14 +0,0 @@ -package li.songe.selector_core.data - -import li.songe.selector_core.NodeExt - -data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) { - fun match(node: NodeExt) = operator.compare(node.attr(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/ConnectOperator.kt b/selector_core/src/main/java/li/songe/selector_core/data/ConnectOperator.kt deleted file mode 100644 index 045471e..0000000 --- a/selector_core/src/main/java/li/songe/selector_core/data/ConnectOperator.kt +++ /dev/null @@ -1,50 +0,0 @@ -package li.songe.selector_core.data - -import li.songe.selector_core.NodeExt - -sealed class ConnectOperator(val key: String) { - override fun toString() = key - abstract fun traversal(node: NodeExt): Sequence - abstract fun traversal(node: NodeExt, offset: Int): NodeExt? - - 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: NodeExt) = node.beforeBrothers - override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getBeforeBrother(offset) - } - - /** - * A - B, 1,2,3,B,A,7,8 - */ - object AfterBrother : ConnectOperator("-") { - override fun traversal(node: NodeExt) = node.afterBrothers - override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getAfterBrother(offset) - } - - /** - * A > B, A is the ancestor of B - */ - object Ancestor : ConnectOperator(">") { - override fun traversal(node: NodeExt) = node.ancestors - override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getAncestor(offset) - } - - /** - * A < B, A is the child of B - */ - object Child : ConnectOperator("<") { - override fun traversal(node: NodeExt) = node.children - override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getChild(offset) - } -} diff --git a/selector_core/src/main/java/li/songe/selector_core/data/ConnectSegment.kt b/selector_core/src/main/java/li/songe/selector_core/data/ConnectSegment.kt deleted file mode 100644 index 99e6c54..0000000 --- a/selector_core/src/main/java/li/songe/selector_core/data/ConnectSegment.kt +++ /dev/null @@ -1,29 +0,0 @@ -package li.songe.selector_core.data - -import li.songe.selector_core.NodeExt - -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() - } - - val traversal: (node: NodeExt) -> Sequence = if (polynomialExpression.isConstant) { - ({ node -> - sequence { - val node1 = operator.traversal(node, polynomialExpression.b1) - if (node1 != null) { - yield(node1) - } - } - }) - } else { - ({ node -> polynomialExpression.traversal(operator.traversal(node)) }) - } - -} diff --git a/selector_core/src/main/java/li/songe/selector_core/data/PropertySegment.kt b/selector_core/src/main/java/li/songe/selector_core/data/PropertySegment.kt deleted file mode 100644 index fa25833..0000000 --- a/selector_core/src/main/java/li/songe/selector_core/data/PropertySegment.kt +++ /dev/null @@ -1,35 +0,0 @@ -package li.songe.selector_core.data - -import li.songe.selector_core.NodeExt - -data class PropertySegment( - /** - * 此属性选择器是否被 @ 标记 - */ - val tracked: Boolean, - val name: String, - val expressions: List, -) { - override fun toString(): String { - val matchTag = if (tracked) "@" else "" - return matchTag + name + expressions.joinToString("") - } - - val matchName: (node: NodeExt) -> Boolean = - if (name.isBlank() || name == "*") - ({ true }) - else ({ node -> - val str = node.name - str.contentEquals(name) || - (str.endsWith(name) && str[str.length - name.length - 1] == '.') - }) - - val matchExpressions: (node: NodeExt) -> Boolean = { node -> - expressions.all { ex -> ex.match(node) } - } - - fun match(node: NodeExt): Boolean { - return matchName(node) && matchExpressions(node) - } - -} diff --git a/settings.gradle.kts b/settings.gradle.kts index d37ca1c..b348f29 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,7 @@ rootProject.name = "gkd" include(":app") -include(":router") -include(":selector_core") +include(":selector") +include(":hidden_api") pluginManagement { repositories { @@ -10,22 +10,30 @@ pluginManagement { } dependencyResolutionManagement { + // https://youtrack.jetbrains.com/issue/KT-55620 - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) +// https://stackoverflow.com/questions/69163511 + repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) + repositories { mavenLocal() mavenCentral() google() maven("https://jitpack.io") } + versionCatalogs { create("libs") { - library("android.gradle", "com.android.tools.build:gradle:7.3.1") + // use jdk17 + version("jdkVersion", JavaVersion.VERSION_17.majorVersion) + version("kotlinVersion", "1.8.20") version("android.compileSdk", "33") - version("android.minSdk", "26") version("android.targetSdk", "33") - version("android.buildToolsVersion", "33.0.0") + version("android.buildToolsVersion", "33.0.2") + version("android.minSdk", "26") + + library("android.gradle", "com.android.tools.build:gradle:8.0.2") // 当前 android 项目 kotlin 的版本 library("kotlin.gradle.plugin", "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") @@ -34,7 +42,7 @@ dependencyResolutionManagement { // compose 编译器的版本, 需要注意它与 compose 的版本没有关联 // https://mvnrepository.com/artifact/androidx.compose.compiler/compiler - version("compose.compiler", "1.4.6") + version("compose.compilerVersion", "1.4.6") library("compose.ui", "androidx.compose.ui:ui:1.4.2") library("compose.material", "androidx.compose.material:material:1.4.2") library("compose.preview", "androidx.compose.ui:ui-tooling-preview:1.4.2") @@ -47,9 +55,19 @@ dependencyResolutionManagement { // https://bugly.qq.com/docs/user-guide/instruction-manual-android/ library("tencent.bugly", "com.tencent.bugly:crashreport:4.0.4") - library("rikka.gradle", "dev.rikka.tools.refine:gradle-plugin:3.0.3") - library("rikka.shizuku.api", "dev.rikka.shizuku:api:12.1.0") - library("rikka.shizuku.provider", "dev.rikka.shizuku:provider:12.1.0") + // https://github.com/RikkaApps/HiddenApiRefinePlugin + plugin("rikka.refine", "dev.rikka.tools.refine").version("4.3.0") + library("rikka.gradle", "dev.rikka.tools.refine:gradle-plugin:4.3.0") + library("rikka.processor", "dev.rikka.tools.refine:annotation-processor:4.3.0") + library("rikka.annotation", "dev.rikka.tools.refine:annotation:4.3.0") + library("rikka.runtime", "dev.rikka.tools.refine:runtime:4.3.0") + + // https://github.com/RikkaApps/Shizuku-API + library("rikka.shizuku.api", "dev.rikka.shizuku:api:13.1.2") + library("rikka.shizuku.provider", "dev.rikka.shizuku:provider:13.1.2") + + // https://github.com/LSPosed/AndroidHiddenApiBypass + library("lsposed.hiddenapibypass", "org.lsposed.hiddenapibypass:hiddenapibypass:4.3") // 工具集合类 // https://github.com/Blankj/AndroidUtilCode/blob/master/lib/utilcode/README-CN.md @@ -65,8 +83,6 @@ dependencyResolutionManagement { library("others.zxing.android.embedded", "com.journeyapps:zxing-android-embedded:4.3.0") library("others.floating.bubble.view", "io.github.torrydo:floating-bubble-view:0.5.2") - - library("androidx.localbroadcastmanager", "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0") library("androidx.appcompat", "androidx.appcompat:appcompat:1.6.1") library("androidx.core.ktx", "androidx.core:core-ktx:1.10.0") library( @@ -81,6 +97,8 @@ dependencyResolutionManagement { library("androidx.room.compiler", "androidx.room:room-compiler:2.5.1") library("androidx.room.ktx", "androidx.room:room-ktx:2.5.1") + library("androidx.splashscreen", "androidx.core:core-splashscreen:1.0.1") + library( "google.accompanist.drawablepainter", "com.google.accompanist:accompanist-drawablepainter:0.23.1" @@ -90,22 +108,28 @@ dependencyResolutionManagement { "com.google.accompanist:accompanist-placeholder-material:0.23.1" ) +// https://google.github.io/accompanist/systemuicontroller/ + library("google.accompanist.systemuicontroller", "com.google.accompanist:accompanist-systemuicontroller:0.30.1") + library("junit", "junit:junit:4.13.2") // 请注意,当 client 和 server 版本不一致时, 会报错 socket hang up - library("ktor.server.core", "io.ktor:ktor-server-core:2.2.3") - library("ktor.server.netty", "io.ktor:ktor-server-netty:2.2.3") - library("ktor.server.cors", "io.ktor:ktor-server-cors:2.2.3") + library("ktor.server.core", "io.ktor:ktor-server-core:2.3.1") + library("ktor.server.netty", "io.ktor:ktor-server-netty:2.3.1") + library("ktor.server.cors", "io.ktor:ktor-server-cors:2.3.1") library( "ktor.server.content.negotiation", - "io.ktor:ktor-server-content-negotiation:2.2.3" + "io.ktor:ktor-server-content-negotiation:2.3.1" ) - library("ktor.client.core", "io.ktor:ktor-client-core:2.2.3") - library("ktor.client.cio", "io.ktor:ktor-client-cio:2.2.3") + library("ktor.client.core", "io.ktor:ktor-client-core:2.3.1") +// library("ktor.client.okhttp", "io.ktor:ktor-client-okhttp:2.3.1") +// https://ktor.io/docs/http-client-engines.html#android android 平台使用 android 或者 okhttp 都行 + library("ktor.client.android", "io.ktor:ktor-client-android:2.3.1") library( "ktor.client.content.negotiation", - "io.ktor:ktor-client-content-negotiation:2.2.3" + "io.ktor:ktor-client-content-negotiation:2.3.1" ) + library( "ktor.serialization.kotlinx.json", "io.ktor:ktor-serialization-kotlinx-json:2.2.3" @@ -123,6 +147,13 @@ dependencyResolutionManagement { // https://developer.android.com/reference/kotlin/org/json/package-summary library("org.json", "org.json:json:20210307") + + plugin("google.ksp", "com.google.devtools.ksp").version("1.8.20-1.0.11") + +// https://composedestinations.rafaelcosta.xyz/setup + library("destinations.core", "io.github.raamcosta.compose-destinations:core:1.8.42-beta") + library("destinations.ksp", "io.github.raamcosta.compose-destinations:ksp:1.8.42-beta") + library("destinations.animations", "io.github.raamcosta.compose-destinations:animations-core:1.8.42-beta") } } }