Compare commits
2 Commits
9ccdf846f0
...
5ea0cdde63
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ea0cdde63 | |||
| 03556ff1d4 |
@@ -4,7 +4,6 @@ go 1.22
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.0.12
|
github.com/go-chi/chi/v5 v5.0.12
|
||||||
github.com/go-playground/validator/v10 v10.19.0
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
@@ -16,3 +15,45 @@ require (
|
|||||||
golang.org/x/oauth2 v0.18.0
|
golang.org/x/oauth2 v0.18.0
|
||||||
modernc.org/sqlite v1.29.5
|
modernc.org/sqlite v1.29.5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/compute v1.23.3 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
github.com/alecthomas/chroma/v2 v2.2.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||||
|
golang.org/x/net v0.22.0 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
|
modernc.org/libc v1.41.0 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
159
portal/go.sum
Normal file
159
portal/go.sum
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||||
|
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
|
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||||
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||||
|
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||||
|
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
|
||||||
|
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||||
|
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||||
|
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
|
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||||
|
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||||
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||||
|
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||||
|
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
|
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||||
|
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||||
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
|
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
|
||||||
|
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
394
src/main/assets/apps/store/store.lua
Normal file
394
src/main/assets/apps/store/store.lua
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
-- store.lua - App Store system app logic
|
||||||
|
-- Milestone 10: Device-Side App Management
|
||||||
|
|
||||||
|
-- State
|
||||||
|
local state = {
|
||||||
|
screen = "home", -- home, games, updates, search, detail
|
||||||
|
installed = {}, -- Installed apps from mosis.apps
|
||||||
|
updates = {}, -- Available updates
|
||||||
|
featured = {}, -- Featured apps from store API
|
||||||
|
categories = {}, -- Category list
|
||||||
|
search_query = "", -- Current search
|
||||||
|
selected_app = nil, -- Selected app for detail view
|
||||||
|
is_loading = false,
|
||||||
|
error_message = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Store API configuration
|
||||||
|
local STORE_API = "https://portal.mosis.dev/store"
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Initialization
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function init()
|
||||||
|
print("[Store] Initializing...")
|
||||||
|
|
||||||
|
-- Load installed apps
|
||||||
|
refreshInstalledApps()
|
||||||
|
|
||||||
|
-- Check for updates
|
||||||
|
checkForUpdates()
|
||||||
|
|
||||||
|
-- Fetch featured apps (async)
|
||||||
|
fetchFeaturedApps()
|
||||||
|
end
|
||||||
|
|
||||||
|
function refreshInstalledApps()
|
||||||
|
if mosis and mosis.apps then
|
||||||
|
state.installed = mosis.apps.getInstalled() or {}
|
||||||
|
print("[Store] Loaded " .. #state.installed .. " installed apps")
|
||||||
|
else
|
||||||
|
print("[Store] Warning: mosis.apps API not available")
|
||||||
|
state.installed = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function checkForUpdates()
|
||||||
|
if mosis and mosis.apps then
|
||||||
|
state.updates = mosis.apps.checkUpdates() or {}
|
||||||
|
print("[Store] Found " .. #state.updates .. " updates")
|
||||||
|
updateBadge()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateBadge()
|
||||||
|
-- Update the updates tab badge
|
||||||
|
local badge = document:GetElementById("updates-badge")
|
||||||
|
if badge then
|
||||||
|
if #state.updates > 0 then
|
||||||
|
badge.inner_rml = tostring(#state.updates)
|
||||||
|
badge.style.display = "block"
|
||||||
|
else
|
||||||
|
badge.style.display = "none"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- API Calls
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function fetchFeaturedApps()
|
||||||
|
state.is_loading = true
|
||||||
|
|
||||||
|
-- TODO: Make HTTP request to STORE_API
|
||||||
|
-- For now, use placeholder data
|
||||||
|
state.featured = {
|
||||||
|
{
|
||||||
|
id = "com.mosis.weather",
|
||||||
|
name = "Weather Pro",
|
||||||
|
category = "Weather",
|
||||||
|
rating = 4.8,
|
||||||
|
downloads = 125000,
|
||||||
|
size = 15728640, -- 15 MB
|
||||||
|
description = "Beautiful forecasts for your virtual world",
|
||||||
|
icon = "W",
|
||||||
|
color = "#2196F3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = "com.mosis.notes",
|
||||||
|
name = "Notes",
|
||||||
|
category = "Productivity",
|
||||||
|
rating = 4.7,
|
||||||
|
downloads = 89000,
|
||||||
|
size = 8388608, -- 8 MB
|
||||||
|
description = "Simple note-taking app",
|
||||||
|
icon = "N",
|
||||||
|
color = "#03DAC6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.is_loading = false
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function searchApps(query)
|
||||||
|
state.search_query = query
|
||||||
|
state.screen = "search"
|
||||||
|
|
||||||
|
if query == "" then
|
||||||
|
state.screen = "home"
|
||||||
|
render()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
state.is_loading = true
|
||||||
|
render()
|
||||||
|
|
||||||
|
-- TODO: Make HTTP request to STORE_API/search
|
||||||
|
-- For now, filter featured apps
|
||||||
|
local results = {}
|
||||||
|
local lower_query = query:lower()
|
||||||
|
for _, app in ipairs(state.featured) do
|
||||||
|
if app.name:lower():find(lower_query) or
|
||||||
|
app.category:lower():find(lower_query) then
|
||||||
|
table.insert(results, app)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
state.search_results = results
|
||||||
|
state.is_loading = false
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Installation
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function installApp(app_id, download_url, signature)
|
||||||
|
print("[Store] Installing: " .. app_id)
|
||||||
|
|
||||||
|
showProgress(app_id)
|
||||||
|
|
||||||
|
if mosis and mosis.apps then
|
||||||
|
mosis.apps.install(download_url or "", signature or "", function(progress)
|
||||||
|
updateProgress(progress)
|
||||||
|
|
||||||
|
if progress.stage == "complete" then
|
||||||
|
hideProgress()
|
||||||
|
showToast("App installed successfully!")
|
||||||
|
refreshInstalledApps()
|
||||||
|
render()
|
||||||
|
elseif progress.stage == "failed" then
|
||||||
|
hideProgress()
|
||||||
|
showError("Installation failed: " .. (progress.error or "Unknown error"))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
hideProgress()
|
||||||
|
showError("App installation not available")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function uninstallApp(package_id)
|
||||||
|
print("[Store] Uninstalling: " .. package_id)
|
||||||
|
|
||||||
|
if mosis and mosis.apps then
|
||||||
|
local success = mosis.apps.uninstall(package_id)
|
||||||
|
if success then
|
||||||
|
showToast("App uninstalled")
|
||||||
|
refreshInstalledApps()
|
||||||
|
render()
|
||||||
|
else
|
||||||
|
showError("Failed to uninstall app")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function openApp(package_id)
|
||||||
|
print("[Store] Launching: " .. package_id)
|
||||||
|
|
||||||
|
if mosis and mosis.apps then
|
||||||
|
mosis.apps.launch(package_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateApp(package_id)
|
||||||
|
print("[Store] Updating: " .. package_id)
|
||||||
|
|
||||||
|
-- Find update info
|
||||||
|
for _, update in ipairs(state.updates) do
|
||||||
|
if update.package_id == package_id then
|
||||||
|
installApp(package_id, update.download_url, update.signature)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
showError("No update available for this app")
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateAllApps()
|
||||||
|
print("[Store] Updating all apps...")
|
||||||
|
|
||||||
|
for _, update in ipairs(state.updates) do
|
||||||
|
-- Queue updates (in a real implementation, this would be sequential)
|
||||||
|
installApp(update.package_id, update.download_url, update.signature)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- UI Helpers
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function isInstalled(package_id)
|
||||||
|
for _, app in ipairs(state.installed) do
|
||||||
|
if app.package_id == package_id then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function hasUpdate(package_id)
|
||||||
|
for _, update in ipairs(state.updates) do
|
||||||
|
if update.package_id == package_id then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function formatSize(bytes)
|
||||||
|
if bytes >= 1048576 then
|
||||||
|
return string.format("%.1f MB", bytes / 1048576)
|
||||||
|
elseif bytes >= 1024 then
|
||||||
|
return string.format("%.0f KB", bytes / 1024)
|
||||||
|
else
|
||||||
|
return bytes .. " B"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function formatDownloads(count)
|
||||||
|
if count >= 1000000 then
|
||||||
|
return string.format("%.1fM", count / 1000000)
|
||||||
|
elseif count >= 1000 then
|
||||||
|
return string.format("%.0fK", count / 1000)
|
||||||
|
else
|
||||||
|
return tostring(count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Progress Dialog
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function showProgress(app_name)
|
||||||
|
local dialog = document:GetElementById("progress-dialog")
|
||||||
|
if dialog then
|
||||||
|
dialog.style.display = "flex"
|
||||||
|
local title = document:GetElementById("progress-title")
|
||||||
|
if title then
|
||||||
|
title.inner_rml = "Installing " .. (app_name or "App")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateProgress(progress)
|
||||||
|
local bar = document:GetElementById("progress-bar")
|
||||||
|
if bar then
|
||||||
|
bar.style.width = (progress.progress * 100) .. "%"
|
||||||
|
end
|
||||||
|
|
||||||
|
local status = document:GetElementById("progress-status")
|
||||||
|
if status then
|
||||||
|
local stage_names = {
|
||||||
|
downloading = "Downloading...",
|
||||||
|
verifying = "Verifying...",
|
||||||
|
extracting = "Extracting...",
|
||||||
|
registering = "Registering...",
|
||||||
|
complete = "Complete!",
|
||||||
|
failed = "Failed"
|
||||||
|
}
|
||||||
|
status.inner_rml = stage_names[progress.stage] or progress.stage
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function hideProgress()
|
||||||
|
local dialog = document:GetElementById("progress-dialog")
|
||||||
|
if dialog then
|
||||||
|
dialog.style.display = "none"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Toast/Error Messages
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function showToast(message)
|
||||||
|
local toast = document:GetElementById("toast")
|
||||||
|
if toast then
|
||||||
|
toast.inner_rml = message
|
||||||
|
toast.style.display = "block"
|
||||||
|
-- Auto-hide after 3 seconds (would need timer API)
|
||||||
|
end
|
||||||
|
print("[Store] Toast: " .. message)
|
||||||
|
end
|
||||||
|
|
||||||
|
function showError(message)
|
||||||
|
state.error_message = message
|
||||||
|
local error_el = document:GetElementById("error-dialog")
|
||||||
|
if error_el then
|
||||||
|
local msg = document:GetElementById("error-message")
|
||||||
|
if msg then
|
||||||
|
msg.inner_rml = message
|
||||||
|
end
|
||||||
|
error_el.style.display = "flex"
|
||||||
|
end
|
||||||
|
print("[Store] Error: " .. message)
|
||||||
|
end
|
||||||
|
|
||||||
|
function hideError()
|
||||||
|
state.error_message = nil
|
||||||
|
local error_el = document:GetElementById("error-dialog")
|
||||||
|
if error_el then
|
||||||
|
error_el.style.display = "none"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Navigation
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function showHome()
|
||||||
|
state.screen = "home"
|
||||||
|
setActiveTab("apps")
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function showGames()
|
||||||
|
state.screen = "games"
|
||||||
|
setActiveTab("games")
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function showUpdates()
|
||||||
|
state.screen = "updates"
|
||||||
|
setActiveTab("updates")
|
||||||
|
checkForUpdates()
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function showSearch()
|
||||||
|
state.screen = "search"
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function showAppDetail(app_id)
|
||||||
|
state.screen = "detail"
|
||||||
|
-- Find app in featured or installed
|
||||||
|
for _, app in ipairs(state.featured) do
|
||||||
|
if app.id == app_id then
|
||||||
|
state.selected_app = app
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function setActiveTab(tab)
|
||||||
|
local tabs = {"apps", "games", "updates"}
|
||||||
|
for _, t in ipairs(tabs) do
|
||||||
|
local el = document:GetElementById("nav-" .. t)
|
||||||
|
if el then
|
||||||
|
if t == tab then
|
||||||
|
el:SetClass("active", true)
|
||||||
|
else
|
||||||
|
el:SetClass("active", false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Rendering
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
function render()
|
||||||
|
-- The RML is mostly static with dynamic data binding
|
||||||
|
-- In a full implementation, we'd update innerHTML of content areas
|
||||||
|
print("[Store] Rendering screen: " .. state.screen)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize on load
|
||||||
|
init()
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<link type="text/rcss" href="../../ui/theme.rcss"/>
|
<link type="text/rcss" href="../../ui/theme.rcss"/>
|
||||||
<link type="text/rcss" href="../../ui/components.rcss"/>
|
<link type="text/rcss" href="../../ui/components.rcss"/>
|
||||||
<script src="../../scripts/navigation.lua"></script>
|
<script src="../../scripts/navigation.lua"></script>
|
||||||
|
<script src="store.lua"></script>
|
||||||
<title>Store</title>
|
<title>Store</title>
|
||||||
<style>
|
<style>
|
||||||
.store-screen {
|
.store-screen {
|
||||||
@@ -296,6 +297,121 @@
|
|||||||
.bg-red { background-color: #F44336; }
|
.bg-red { background-color: #F44336; }
|
||||||
.bg-pink { background-color: #E91E63; }
|
.bg-pink { background-color: #E91E63; }
|
||||||
.bg-indigo { background-color: #3F51B5; }
|
.bg-indigo { background-color: #3F51B5; }
|
||||||
|
|
||||||
|
/* Dialog Overlay */
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background-color: #2D2D2D;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-message {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #B3B3B3;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-status {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #B3B3B3;
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #BB86FC;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn:hover {
|
||||||
|
background-color: rgba(187, 134, 252, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar */
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #1E1E1E;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #BB86FC;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #323232;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge */
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background-color: #F44336;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-nav-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="store-screen">
|
<body class="store-screen">
|
||||||
@@ -471,18 +587,44 @@
|
|||||||
|
|
||||||
<!-- Bottom Navigation -->
|
<!-- Bottom Navigation -->
|
||||||
<div class="store-bottom-nav">
|
<div class="store-bottom-nav">
|
||||||
<div class="store-nav-item active">
|
<div id="nav-apps" class="store-nav-item active" onclick="showHome()">
|
||||||
<img src="../../icons/home.tga"/>
|
<img src="../../icons/home.tga"/>
|
||||||
<span>Apps</span>
|
<span>Apps</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="store-nav-item">
|
<div id="nav-games" class="store-nav-item" onclick="showGames()">
|
||||||
<img src="../../icons/game.tga"/>
|
<img src="../../icons/game.tga"/>
|
||||||
<span>Games</span>
|
<span>Games</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="store-nav-item">
|
<div id="nav-updates" class="store-nav-item" onclick="showUpdates()">
|
||||||
<img src="../../icons/download.tga"/>
|
<img src="../../icons/download.tga"/>
|
||||||
<span>Updates</span>
|
<span>Updates</span>
|
||||||
|
<div id="updates-badge" class="badge" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Dialog (hidden by default) -->
|
||||||
|
<div id="progress-dialog" class="dialog-overlay" style="display: none;">
|
||||||
|
<div class="dialog">
|
||||||
|
<div id="progress-title" class="dialog-title">Installing...</div>
|
||||||
|
<div class="progress-container">
|
||||||
|
<div id="progress-bar" class="progress-bar" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="progress-status" class="dialog-status">Preparing...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Dialog (hidden by default) -->
|
||||||
|
<div id="error-dialog" class="dialog-overlay" style="display: none;">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-title">Error</div>
|
||||||
|
<div id="error-message" class="dialog-message"></div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<div class="dialog-btn" onclick="hideError()">OK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast (hidden by default) -->
|
||||||
|
<div id="toast" class="toast" style="display: none;"></div>
|
||||||
</body>
|
</body>
|
||||||
</rml>
|
</rml>
|
||||||
|
|||||||
539
src/main/cpp/apps/app_api.cpp
Normal file
539
src/main/cpp/apps/app_api.cpp
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
// app_api.cpp - Lua API bindings for app management implementation
|
||||||
|
// Milestone 10: Device-Side App Management
|
||||||
|
|
||||||
|
#include "app_api.h"
|
||||||
|
#include "app_manager.h"
|
||||||
|
#include "update_service.h"
|
||||||
|
#include "../logger.h"
|
||||||
|
|
||||||
|
#include <lua.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
// Registry keys for storing pointers
|
||||||
|
static const char* APP_MANAGER_KEY = "mosis.app_manager";
|
||||||
|
static const char* UPDATE_SERVICE_KEY = "mosis.update_service";
|
||||||
|
static const char* CURRENT_APP_ID_KEY = "mosis.current_app_id";
|
||||||
|
static const char* IS_SYSTEM_APP_KEY = "mosis.is_system_app";
|
||||||
|
|
||||||
|
// Helper to get AppManager from Lua registry
|
||||||
|
static AppManager* GetAppManager(lua_State* L) {
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY);
|
||||||
|
auto* manager = static_cast<AppManager*>(lua_touserdata(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get UpdateService from Lua registry
|
||||||
|
static UpdateService* GetUpdateService(lua_State* L) {
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY);
|
||||||
|
auto* service = static_cast<UpdateService*>(lua_touserdata(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get current app ID
|
||||||
|
static std::string GetCurrentAppId(lua_State* L) {
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY);
|
||||||
|
std::string id = lua_tostring(L, -1) ? lua_tostring(L, -1) : "";
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if system app
|
||||||
|
static bool IsSystemApp(lua_State* L) {
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY);
|
||||||
|
bool is_system = lua_toboolean(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return is_system;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// mosis.apps.* - System apps only
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// mosis.apps.getInstalled() -> [{package_id, name, version_name, version_code, installed_at}]
|
||||||
|
static int apps_getInstalled(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.getInstalled requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_newtable(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto apps = manager->GetInstalledApps();
|
||||||
|
|
||||||
|
lua_createtable(L, static_cast<int>(apps.size()), 0);
|
||||||
|
int idx = 1;
|
||||||
|
|
||||||
|
for (const auto& app : apps) {
|
||||||
|
lua_createtable(L, 0, 8);
|
||||||
|
|
||||||
|
lua_pushstring(L, app.package_id.c_str());
|
||||||
|
lua_setfield(L, -2, "package_id");
|
||||||
|
|
||||||
|
lua_pushstring(L, app.name.c_str());
|
||||||
|
lua_setfield(L, -2, "name");
|
||||||
|
|
||||||
|
lua_pushstring(L, app.version_name.c_str());
|
||||||
|
lua_setfield(L, -2, "version_name");
|
||||||
|
|
||||||
|
lua_pushinteger(L, app.version_code);
|
||||||
|
lua_setfield(L, -2, "version_code");
|
||||||
|
|
||||||
|
lua_pushboolean(L, app.is_system_app);
|
||||||
|
lua_setfield(L, -2, "is_system_app");
|
||||||
|
|
||||||
|
lua_pushstring(L, app.icon_path.c_str());
|
||||||
|
lua_setfield(L, -2, "icon");
|
||||||
|
|
||||||
|
lua_pushstring(L, app.developer_name.c_str());
|
||||||
|
lua_setfield(L, -2, "developer");
|
||||||
|
|
||||||
|
// installed_at as Unix timestamp
|
||||||
|
auto ts = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
app.installed_at.time_since_epoch()).count();
|
||||||
|
lua_pushinteger(L, ts);
|
||||||
|
lua_setfield(L, -2, "installed_at");
|
||||||
|
|
||||||
|
lua_rawseti(L, -2, idx++);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.getInfo(package_id) -> {info} or nil
|
||||||
|
static int apps_getInfo(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.getInfo requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* package_id = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto app = manager->GetApp(package_id);
|
||||||
|
if (!app) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_createtable(L, 0, 12);
|
||||||
|
|
||||||
|
lua_pushstring(L, app->package_id.c_str());
|
||||||
|
lua_setfield(L, -2, "package_id");
|
||||||
|
|
||||||
|
lua_pushstring(L, app->name.c_str());
|
||||||
|
lua_setfield(L, -2, "name");
|
||||||
|
|
||||||
|
lua_pushstring(L, app->version_name.c_str());
|
||||||
|
lua_setfield(L, -2, "version_name");
|
||||||
|
|
||||||
|
lua_pushinteger(L, app->version_code);
|
||||||
|
lua_setfield(L, -2, "version_code");
|
||||||
|
|
||||||
|
lua_pushboolean(L, app->is_system_app);
|
||||||
|
lua_setfield(L, -2, "is_system_app");
|
||||||
|
|
||||||
|
lua_pushinteger(L, app->package_size);
|
||||||
|
lua_setfield(L, -2, "package_size");
|
||||||
|
|
||||||
|
lua_pushinteger(L, app->data_size);
|
||||||
|
lua_setfield(L, -2, "data_size");
|
||||||
|
|
||||||
|
lua_pushstring(L, app->icon_path.c_str());
|
||||||
|
lua_setfield(L, -2, "icon");
|
||||||
|
|
||||||
|
lua_pushstring(L, app->developer_name.c_str());
|
||||||
|
lua_setfield(L, -2, "developer");
|
||||||
|
|
||||||
|
// Permissions array
|
||||||
|
lua_createtable(L, static_cast<int>(app->permissions.size()), 0);
|
||||||
|
int idx = 1;
|
||||||
|
for (const auto& perm : app->permissions) {
|
||||||
|
lua_pushstring(L, perm.c_str());
|
||||||
|
lua_rawseti(L, -2, idx++);
|
||||||
|
}
|
||||||
|
lua_setfield(L, -2, "permissions");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.install(url, signature, callback)
|
||||||
|
static int apps_install(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.install requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* url = luaL_checkstring(L, 1);
|
||||||
|
const char* signature = lua_isstring(L, 2) ? lua_tostring(L, 2) : "";
|
||||||
|
|
||||||
|
// Callback is optional (argument 3)
|
||||||
|
bool has_callback = lua_isfunction(L, 3);
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_callback) {
|
||||||
|
// Store callback reference
|
||||||
|
lua_pushvalue(L, 3);
|
||||||
|
int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
|
||||||
|
// Create progress callback that calls Lua
|
||||||
|
// Note: This is simplified - real implementation needs thread safety
|
||||||
|
ProgressCallback progress_cb = [L, callback_ref](const InstallProgress& progress) {
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, callback_ref);
|
||||||
|
|
||||||
|
lua_createtable(L, 0, 3);
|
||||||
|
|
||||||
|
lua_pushstring(L, InstallProgress::StageName(progress.stage));
|
||||||
|
lua_setfield(L, -2, "stage");
|
||||||
|
|
||||||
|
lua_pushnumber(L, progress.progress);
|
||||||
|
lua_setfield(L, -2, "progress");
|
||||||
|
|
||||||
|
lua_pushstring(L, progress.error.c_str());
|
||||||
|
lua_setfield(L, -2, "error");
|
||||||
|
|
||||||
|
if (lua_pcall(L, 1, 0, 0) != LUA_OK) {
|
||||||
|
LOG_ERROR("Install callback error: %s", lua_tostring(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up ref when complete
|
||||||
|
if (progress.stage == InstallProgress::Stage::Complete ||
|
||||||
|
progress.stage == InstallProgress::Stage::Failed) {
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool success = manager->Install(url, signature, progress_cb);
|
||||||
|
lua_pushboolean(L, success);
|
||||||
|
} else {
|
||||||
|
bool success = manager->Install(url, signature, nullptr);
|
||||||
|
lua_pushboolean(L, success);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.uninstall(package_id) -> boolean
|
||||||
|
static int apps_uninstall(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.uninstall requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* package_id = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = manager->Uninstall(package_id);
|
||||||
|
lua_pushboolean(L, success);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.launch(package_id) -> boolean
|
||||||
|
static int apps_launch(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.launch requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* package_id = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = manager->LaunchApp(package_id);
|
||||||
|
lua_pushboolean(L, success);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.getDataSize(package_id) -> number
|
||||||
|
static int apps_getDataSize(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.getDataSize requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* package_id = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t size = manager->GetAppDataSize(package_id);
|
||||||
|
lua_pushinteger(L, size);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.clearCache(package_id) -> boolean
|
||||||
|
static int apps_clearCache(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.clearCache requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* package_id = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = manager->ClearAppCache(package_id);
|
||||||
|
lua_pushboolean(L, success);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.clearData(package_id) -> boolean
|
||||||
|
static int apps_clearData(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.clearData requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* package_id = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = manager->ClearAppData(package_id);
|
||||||
|
lua_pushboolean(L, success);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.apps.checkUpdates(callback) -> void
|
||||||
|
static int apps_checkUpdates(lua_State* L) {
|
||||||
|
if (!IsSystemApp(L)) {
|
||||||
|
return luaL_error(L, "mosis.apps.checkUpdates requires system permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* service = GetUpdateService(L);
|
||||||
|
if (!service) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto updates = service->CheckForUpdates();
|
||||||
|
|
||||||
|
lua_createtable(L, static_cast<int>(updates.size()), 0);
|
||||||
|
int idx = 1;
|
||||||
|
|
||||||
|
for (const auto& update : updates) {
|
||||||
|
lua_createtable(L, 0, 8);
|
||||||
|
|
||||||
|
lua_pushstring(L, update.package_id.c_str());
|
||||||
|
lua_setfield(L, -2, "package_id");
|
||||||
|
|
||||||
|
lua_pushstring(L, update.name.c_str());
|
||||||
|
lua_setfield(L, -2, "name");
|
||||||
|
|
||||||
|
lua_pushstring(L, update.current_version.c_str());
|
||||||
|
lua_setfield(L, -2, "current_version");
|
||||||
|
|
||||||
|
lua_pushstring(L, update.new_version.c_str());
|
||||||
|
lua_setfield(L, -2, "new_version");
|
||||||
|
|
||||||
|
lua_pushinteger(L, update.download_size);
|
||||||
|
lua_setfield(L, -2, "size");
|
||||||
|
|
||||||
|
lua_pushstring(L, update.release_notes.c_str());
|
||||||
|
lua_setfield(L, -2, "release_notes");
|
||||||
|
|
||||||
|
lua_pushboolean(L, update.is_critical);
|
||||||
|
lua_setfield(L, -2, "critical");
|
||||||
|
|
||||||
|
lua_rawseti(L, -2, idx++);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// mosis.app.* - All apps (info about current app)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// mosis.app.info() -> {package_id, name, version_name, version_code}
|
||||||
|
static int app_info(lua_State* L) {
|
||||||
|
std::string app_id = GetCurrentAppId(L);
|
||||||
|
if (app_id.empty()) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* manager = GetAppManager(L);
|
||||||
|
if (!manager) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto app = manager->GetApp(app_id);
|
||||||
|
if (!app) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_createtable(L, 0, 4);
|
||||||
|
|
||||||
|
lua_pushstring(L, app->package_id.c_str());
|
||||||
|
lua_setfield(L, -2, "package_id");
|
||||||
|
|
||||||
|
lua_pushstring(L, app->name.c_str());
|
||||||
|
lua_setfield(L, -2, "name");
|
||||||
|
|
||||||
|
lua_pushstring(L, app->version_name.c_str());
|
||||||
|
lua_setfield(L, -2, "version_name");
|
||||||
|
|
||||||
|
lua_pushinteger(L, app->version_code);
|
||||||
|
lua_setfield(L, -2, "version_code");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.app.checkUpdate(callback) -> void
|
||||||
|
static int app_checkUpdate(lua_State* L) {
|
||||||
|
// Get callback
|
||||||
|
if (!lua_isfunction(L, 1)) {
|
||||||
|
return luaL_error(L, "callback function required");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string app_id = GetCurrentAppId(L);
|
||||||
|
auto* service = GetUpdateService(L);
|
||||||
|
|
||||||
|
if (app_id.empty() || !service) {
|
||||||
|
// Call callback with no update
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pcall(L, 2, 0, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto updates = service->GetPendingUpdates();
|
||||||
|
for (const auto& update : updates) {
|
||||||
|
if (update.package_id == app_id) {
|
||||||
|
// Call callback with update info
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
lua_pushboolean(L, true);
|
||||||
|
lua_pushstring(L, update.new_version.c_str());
|
||||||
|
lua_pcall(L, 2, 0, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No update available
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
lua_pushboolean(L, false);
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pcall(L, 2, 0, 0);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mosis.app.openStorePage() -> void
|
||||||
|
static int app_openStorePage(lua_State* L) {
|
||||||
|
std::string app_id = GetCurrentAppId(L);
|
||||||
|
if (app_id.empty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Navigate to store page for this app
|
||||||
|
// This would typically trigger navigation to:
|
||||||
|
// mosis://store/app/{app_id}
|
||||||
|
LOG_INFO("Open store page for: %s", app_id.c_str());
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Registration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static const luaL_Reg apps_functions[] = {
|
||||||
|
{"getInstalled", apps_getInstalled},
|
||||||
|
{"getInfo", apps_getInfo},
|
||||||
|
{"install", apps_install},
|
||||||
|
{"uninstall", apps_uninstall},
|
||||||
|
{"launch", apps_launch},
|
||||||
|
{"getDataSize", apps_getDataSize},
|
||||||
|
{"clearCache", apps_clearCache},
|
||||||
|
{"clearData", apps_clearData},
|
||||||
|
{"checkUpdates", apps_checkUpdates},
|
||||||
|
{nullptr, nullptr}
|
||||||
|
};
|
||||||
|
|
||||||
|
static const luaL_Reg app_functions[] = {
|
||||||
|
{"info", app_info},
|
||||||
|
{"checkUpdate", app_checkUpdate},
|
||||||
|
{"openStorePage", app_openStorePage},
|
||||||
|
{nullptr, nullptr}
|
||||||
|
};
|
||||||
|
|
||||||
|
void RegisterAppAPIs(lua_State* L,
|
||||||
|
AppManager* app_manager,
|
||||||
|
UpdateService* update_service,
|
||||||
|
const std::string& current_app_id,
|
||||||
|
bool is_system_app) {
|
||||||
|
// Store pointers in registry
|
||||||
|
lua_pushlightuserdata(L, app_manager);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, APP_MANAGER_KEY);
|
||||||
|
|
||||||
|
lua_pushlightuserdata(L, update_service);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, UPDATE_SERVICE_KEY);
|
||||||
|
|
||||||
|
lua_pushstring(L, current_app_id.c_str());
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, CURRENT_APP_ID_KEY);
|
||||||
|
|
||||||
|
lua_pushboolean(L, is_system_app);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, IS_SYSTEM_APP_KEY);
|
||||||
|
|
||||||
|
// Get or create mosis table
|
||||||
|
lua_getglobal(L, "mosis");
|
||||||
|
if (lua_isnil(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
lua_newtable(L);
|
||||||
|
lua_setglobal(L, "mosis");
|
||||||
|
lua_getglobal(L, "mosis");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mosis.apps table (system apps only)
|
||||||
|
if (is_system_app) {
|
||||||
|
lua_newtable(L);
|
||||||
|
luaL_setfuncs(L, apps_functions, 0);
|
||||||
|
lua_setfield(L, -2, "apps");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mosis.app table (all apps)
|
||||||
|
lua_newtable(L);
|
||||||
|
luaL_setfuncs(L, app_functions, 0);
|
||||||
|
lua_setfield(L, -2, "app");
|
||||||
|
|
||||||
|
lua_pop(L, 1); // pop mosis table
|
||||||
|
|
||||||
|
LOG_DEBUG("Registered app APIs for: %s (system=%d)",
|
||||||
|
current_app_id.c_str(), is_system_app);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
21
src/main/cpp/apps/app_api.h
Normal file
21
src/main/cpp/apps/app_api.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// app_api.h - Lua API bindings for app management
|
||||||
|
// Milestone 10: Device-Side App Management
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
struct lua_State;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
class AppManager;
|
||||||
|
class UpdateService;
|
||||||
|
|
||||||
|
// Register Lua APIs for app management
|
||||||
|
// - mosis.apps.* - System apps only (App Store, Settings)
|
||||||
|
// - mosis.app.* - All apps (info about current app)
|
||||||
|
void RegisterAppAPIs(lua_State* L,
|
||||||
|
AppManager* app_manager,
|
||||||
|
UpdateService* update_service,
|
||||||
|
const std::string& current_app_id,
|
||||||
|
bool is_system_app);
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
680
src/main/cpp/apps/app_manager.cpp
Normal file
680
src/main/cpp/apps/app_manager.cpp
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
// app_manager.cpp - App installation and management implementation
|
||||||
|
// Milestone 10: Device-Side App Management
|
||||||
|
|
||||||
|
#include "app_manager.h"
|
||||||
|
#include "../sandbox/sandbox_manager.h"
|
||||||
|
#include "../logger.h"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <random>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
// For JSON parsing
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
// For ZIP extraction
|
||||||
|
#include <minizip/unzip.h>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
AppManager::AppManager(const std::string& data_root)
|
||||||
|
: m_data_root(data_root)
|
||||||
|
{
|
||||||
|
// Create directory structure
|
||||||
|
fs::create_directories(m_data_root + "/apps");
|
||||||
|
fs::create_directories(m_data_root + "/downloads");
|
||||||
|
fs::create_directories(m_data_root + "/backups");
|
||||||
|
fs::create_directories(m_data_root + "/config");
|
||||||
|
|
||||||
|
// Load installed apps registry
|
||||||
|
LoadInstalledApps();
|
||||||
|
|
||||||
|
LOG_INFO("AppManager initialized at: %s", m_data_root.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
AppManager::~AppManager() {
|
||||||
|
SaveInstalledApps();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::Install(const std::string& package_url,
|
||||||
|
const std::string& signature,
|
||||||
|
ProgressCallback callback) {
|
||||||
|
callback({InstallProgress::Stage::Downloading, 0.0f, ""});
|
||||||
|
|
||||||
|
// Generate download path
|
||||||
|
std::string download_path = m_data_root + "/downloads/" + GenerateUUID() + ".mosis";
|
||||||
|
|
||||||
|
// Download package
|
||||||
|
if (!DownloadFile(package_url, download_path, [&](float p) {
|
||||||
|
callback({InstallProgress::Stage::Downloading, p, ""});
|
||||||
|
})) {
|
||||||
|
callback({InstallProgress::Stage::Failed, 0.0f, "Download failed"});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({InstallProgress::Stage::Verifying, 0.0f, ""});
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
if (!signature.empty() && !VerifySignature(download_path, signature)) {
|
||||||
|
fs::remove(download_path);
|
||||||
|
callback({InstallProgress::Stage::Failed, 0.0f, "Signature verification failed"});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify package integrity
|
||||||
|
if (!VerifyPackage(download_path)) {
|
||||||
|
fs::remove(download_path);
|
||||||
|
callback({InstallProgress::Stage::Failed, 0.0f, "Package verification failed"});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with installation
|
||||||
|
bool result = InstallFromFile(download_path, callback);
|
||||||
|
|
||||||
|
// Clean up download
|
||||||
|
fs::remove(download_path);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::InstallFromFile(const std::string& package_path,
|
||||||
|
ProgressCallback callback) {
|
||||||
|
callback({InstallProgress::Stage::Verifying, 0.0f, ""});
|
||||||
|
|
||||||
|
// Extract manifest to get package_id
|
||||||
|
auto manifest = ExtractManifest(package_path);
|
||||||
|
if (!manifest) {
|
||||||
|
callback({InstallProgress::Stage::Failed, 0.0f, "Invalid manifest"});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Installing app: %s v%s", manifest->id.c_str(), manifest->version.c_str());
|
||||||
|
|
||||||
|
callback({InstallProgress::Stage::Extracting, 0.0f, ""});
|
||||||
|
|
||||||
|
// Determine installation path
|
||||||
|
std::string install_path = m_data_root + "/apps/" + manifest->id;
|
||||||
|
|
||||||
|
// Check if already installed (update path)
|
||||||
|
if (fs::exists(install_path + "/package")) {
|
||||||
|
LOG_INFO("App already installed, updating: %s", manifest->id.c_str());
|
||||||
|
// Backup existing data
|
||||||
|
BackupAppData(manifest->id);
|
||||||
|
// Remove old package
|
||||||
|
fs::remove_all(install_path + "/package");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directories
|
||||||
|
fs::create_directories(install_path + "/package");
|
||||||
|
fs::create_directories(install_path + "/data");
|
||||||
|
fs::create_directories(install_path + "/cache");
|
||||||
|
fs::create_directories(install_path + "/db");
|
||||||
|
|
||||||
|
// Extract package
|
||||||
|
if (!ExtractPackage(package_path, install_path + "/package")) {
|
||||||
|
callback({InstallProgress::Stage::Failed, 0.0f, "Extraction failed"});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({InstallProgress::Stage::Registering, 0.0f, ""});
|
||||||
|
|
||||||
|
// Get package file size
|
||||||
|
int64_t package_size = 0;
|
||||||
|
try {
|
||||||
|
package_size = static_cast<int64_t>(fs::file_size(package_path));
|
||||||
|
} catch (...) {
|
||||||
|
package_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register app
|
||||||
|
InstalledApp app;
|
||||||
|
app.package_id = manifest->id;
|
||||||
|
app.name = manifest->name;
|
||||||
|
app.version_name = manifest->version;
|
||||||
|
app.version_code = manifest->version_code;
|
||||||
|
app.install_path = install_path;
|
||||||
|
app.permissions = manifest->permissions;
|
||||||
|
app.installed_at = std::chrono::system_clock::now();
|
||||||
|
app.updated_at = std::chrono::system_clock::now();
|
||||||
|
app.package_size = package_size;
|
||||||
|
app.data_size = 0;
|
||||||
|
app.is_system_app = false;
|
||||||
|
app.entry_point = manifest->entry;
|
||||||
|
app.icon_path = manifest->icon;
|
||||||
|
app.developer_name = manifest->developer_name;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_installed_apps[manifest->id] = app;
|
||||||
|
SaveInstalledApps();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("App installed successfully: %s", manifest->id.c_str());
|
||||||
|
callback({InstallProgress::Stage::Complete, 1.0f, ""});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::Uninstall(const std::string& package_id, bool keep_data) {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
auto it = m_installed_apps.find(package_id);
|
||||||
|
if (it == m_installed_apps.end()) {
|
||||||
|
LOG_WARN("Cannot uninstall: app not found: %s", package_id.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot uninstall system apps
|
||||||
|
if (it->second.is_system_app) {
|
||||||
|
LOG_WARN("Cannot uninstall system app: %s", package_id.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Uninstalling app: %s (keep_data=%d)", package_id.c_str(), keep_data);
|
||||||
|
|
||||||
|
// Stop app if running
|
||||||
|
if (m_sandbox_manager && m_sandbox_manager->IsAppRunning(package_id)) {
|
||||||
|
m_sandbox_manager->StopApp(package_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove files
|
||||||
|
std::string install_path = it->second.install_path;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::remove_all(install_path + "/package");
|
||||||
|
fs::remove_all(install_path + "/cache");
|
||||||
|
|
||||||
|
if (!keep_data) {
|
||||||
|
fs::remove_all(install_path + "/data");
|
||||||
|
fs::remove_all(install_path + "/db");
|
||||||
|
fs::remove_all(install_path);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Error removing app files: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister
|
||||||
|
m_installed_apps.erase(it);
|
||||||
|
SaveInstalledApps();
|
||||||
|
|
||||||
|
LOG_INFO("App uninstalled: %s", package_id.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::Update(const std::string& package_id,
|
||||||
|
const std::string& package_url,
|
||||||
|
const std::string& signature,
|
||||||
|
ProgressCallback callback) {
|
||||||
|
// Updates use the same flow as Install, which handles existing installations
|
||||||
|
return Install(package_url, signature, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<InstalledApp> AppManager::GetInstalledApps() const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
std::vector<InstalledApp> apps;
|
||||||
|
apps.reserve(m_installed_apps.size());
|
||||||
|
for (const auto& [id, app] : m_installed_apps) {
|
||||||
|
apps.push_back(app);
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InstalledApp> AppManager::GetApp(const std::string& package_id) const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
auto it = m_installed_apps.find(package_id);
|
||||||
|
if (it != m_installed_apps.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::IsInstalled(const std::string& package_id) const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_installed_apps.find(package_id) != m_installed_apps.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t AppManager::GetAppDataSize(const std::string& package_id) const {
|
||||||
|
std::string data_path = GetAppDataPath(package_id);
|
||||||
|
return CalculateDirectorySize(data_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::ClearAppData(const std::string& package_id) {
|
||||||
|
std::string data_path = GetAppDataPath(package_id);
|
||||||
|
std::string db_path = m_data_root + "/apps/" + package_id + "/db";
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::remove_all(data_path);
|
||||||
|
fs::remove_all(db_path);
|
||||||
|
fs::create_directories(data_path);
|
||||||
|
fs::create_directories(db_path);
|
||||||
|
LOG_INFO("Cleared app data: %s", package_id.c_str());
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Error clearing app data: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::ClearAppCache(const std::string& package_id) {
|
||||||
|
std::string cache_path = GetAppCachePath(package_id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::remove_all(cache_path);
|
||||||
|
fs::create_directories(cache_path);
|
||||||
|
LOG_INFO("Cleared app cache: %s", package_id.c_str());
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Error clearing app cache: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::BackupAppData(const std::string& package_id) {
|
||||||
|
std::string data_path = GetAppDataPath(package_id);
|
||||||
|
std::string backup_path = m_data_root + "/backups/" + package_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs::exists(data_path)) {
|
||||||
|
fs::remove_all(backup_path);
|
||||||
|
fs::copy(data_path, backup_path, fs::copy_options::recursive);
|
||||||
|
LOG_INFO("Backed up app data: %s", package_id.c_str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Error backing up app data: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::RestoreAppData(const std::string& package_id) {
|
||||||
|
std::string data_path = GetAppDataPath(package_id);
|
||||||
|
std::string backup_path = m_data_root + "/backups/" + package_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs::exists(backup_path)) {
|
||||||
|
fs::remove_all(data_path);
|
||||||
|
fs::copy(backup_path, data_path, fs::copy_options::recursive);
|
||||||
|
LOG_INFO("Restored app data: %s", package_id.c_str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Error restoring app data: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::LaunchApp(const std::string& package_id) {
|
||||||
|
if (!m_sandbox_manager) {
|
||||||
|
LOG_ERROR("Cannot launch app: sandbox manager not set");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto app = GetApp(package_id);
|
||||||
|
if (!app) {
|
||||||
|
LOG_ERROR("Cannot launch app: not installed: %s", package_id.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string app_path = app->install_path + "/package";
|
||||||
|
return m_sandbox_manager->StartApp(package_id, app_path, app->permissions, app->is_system_app);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::StopApp(const std::string& package_id) {
|
||||||
|
if (!m_sandbox_manager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_sandbox_manager->StopApp(package_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::IsAppRunning(const std::string& package_id) const {
|
||||||
|
if (!m_sandbox_manager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_sandbox_manager->IsAppRunning(package_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppManager::SetSandboxManager(LuaSandboxManager* manager) {
|
||||||
|
m_sandbox_manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AppManager::GetAppPath(const std::string& package_id) const {
|
||||||
|
return m_data_root + "/apps/" + package_id + "/package";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AppManager::GetAppDataPath(const std::string& package_id) const {
|
||||||
|
return m_data_root + "/apps/" + package_id + "/data";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AppManager::GetAppCachePath(const std::string& package_id) const {
|
||||||
|
return m_data_root + "/apps/" + package_id + "/cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppManager::RegisterSystemApp(const InstalledApp& app) {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
InstalledApp system_app = app;
|
||||||
|
system_app.is_system_app = true;
|
||||||
|
m_installed_apps[app.package_id] = system_app;
|
||||||
|
|
||||||
|
LOG_INFO("Registered system app: %s", app.package_id.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::VerifyPackage(const std::string& path) {
|
||||||
|
// Verify ZIP structure and manifest presence
|
||||||
|
unzFile zip = unzOpen(path.c_str());
|
||||||
|
if (!zip) {
|
||||||
|
LOG_ERROR("Cannot open package: %s", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_manifest = false;
|
||||||
|
|
||||||
|
if (unzGoToFirstFile(zip) == UNZ_OK) {
|
||||||
|
do {
|
||||||
|
char filename[256];
|
||||||
|
unz_file_info file_info;
|
||||||
|
if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename),
|
||||||
|
nullptr, 0, nullptr, 0) == UNZ_OK) {
|
||||||
|
if (std::string(filename) == "manifest.json") {
|
||||||
|
has_manifest = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (unzGoToNextFile(zip) == UNZ_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
unzClose(zip);
|
||||||
|
|
||||||
|
if (!has_manifest) {
|
||||||
|
LOG_ERROR("Package missing manifest.json: %s", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::VerifySignature(const std::string& path, const std::string& signature) {
|
||||||
|
// TODO: Implement Ed25519 signature verification
|
||||||
|
// For now, accept packages without strict verification
|
||||||
|
LOG_WARN("Signature verification not yet implemented");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AppManifest> AppManager::ExtractManifest(const std::string& package_path) {
|
||||||
|
unzFile zip = unzOpen(package_path.c_str());
|
||||||
|
if (!zip) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string manifest_content;
|
||||||
|
|
||||||
|
// Find and read manifest.json
|
||||||
|
if (unzLocateFile(zip, "manifest.json", 0) != UNZ_OK) {
|
||||||
|
unzClose(zip);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unzOpenCurrentFile(zip) != UNZ_OK) {
|
||||||
|
unzClose(zip);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buffer[4096];
|
||||||
|
int bytes_read;
|
||||||
|
while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) {
|
||||||
|
manifest_content.append(buffer, bytes_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
unzCloseCurrentFile(zip);
|
||||||
|
unzClose(zip);
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
try {
|
||||||
|
json j = json::parse(manifest_content);
|
||||||
|
|
||||||
|
AppManifest manifest;
|
||||||
|
manifest.id = j.value("id", "");
|
||||||
|
manifest.name = j.value("name", "");
|
||||||
|
manifest.version = j.value("version", "1.0.0");
|
||||||
|
manifest.version_code = j.value("version_code", 1);
|
||||||
|
manifest.entry = j.value("entry", "main.rml");
|
||||||
|
manifest.icon = j.value("icon", "");
|
||||||
|
manifest.description = j.value("description", "");
|
||||||
|
manifest.min_api_version = j.value("min_api_version", 1);
|
||||||
|
|
||||||
|
if (j.contains("developer")) {
|
||||||
|
manifest.developer_name = j["developer"].value("name", "");
|
||||||
|
manifest.developer_email = j["developer"].value("email", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.contains("permissions") && j["permissions"].is_array()) {
|
||||||
|
for (const auto& perm : j["permissions"]) {
|
||||||
|
manifest.permissions.push_back(perm.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.id.empty()) {
|
||||||
|
LOG_ERROR("Manifest missing required 'id' field");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest;
|
||||||
|
|
||||||
|
} catch (const json::exception& e) {
|
||||||
|
LOG_ERROR("Failed to parse manifest: %s", e.what());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::ExtractPackage(const std::string& package_path, const std::string& dest_path) {
|
||||||
|
unzFile zip = unzOpen(package_path.c_str());
|
||||||
|
if (!zip) {
|
||||||
|
LOG_ERROR("Cannot open package for extraction: %s", package_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
if (unzGoToFirstFile(zip) == UNZ_OK) {
|
||||||
|
do {
|
||||||
|
char filename[512];
|
||||||
|
unz_file_info file_info;
|
||||||
|
|
||||||
|
if (unzGetCurrentFileInfo(zip, &file_info, filename, sizeof(filename),
|
||||||
|
nullptr, 0, nullptr, 0) != UNZ_OK) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string full_path = dest_path + "/" + filename;
|
||||||
|
|
||||||
|
// Skip META-INF directory (signatures)
|
||||||
|
if (std::string(filename).rfind("META-INF/", 0) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directories
|
||||||
|
size_t len = strlen(filename);
|
||||||
|
if (len > 0 && filename[len - 1] == '/') {
|
||||||
|
fs::create_directories(full_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
fs::create_directories(fs::path(full_path).parent_path());
|
||||||
|
|
||||||
|
// Extract file
|
||||||
|
if (unzOpenCurrentFile(zip) != UNZ_OK) {
|
||||||
|
LOG_ERROR("Cannot open file in archive: %s", filename);
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream out(full_path, std::ios::binary);
|
||||||
|
if (!out) {
|
||||||
|
LOG_ERROR("Cannot create file: %s", full_path.c_str());
|
||||||
|
unzCloseCurrentFile(zip);
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buffer[8192];
|
||||||
|
int bytes_read;
|
||||||
|
while ((bytes_read = unzReadCurrentFile(zip, buffer, sizeof(buffer))) > 0) {
|
||||||
|
out.write(buffer, bytes_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.close();
|
||||||
|
unzCloseCurrentFile(zip);
|
||||||
|
|
||||||
|
} while (unzGoToNextFile(zip) == UNZ_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
unzClose(zip);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppManager::DownloadFile(const std::string& url, const std::string& dest_path,
|
||||||
|
std::function<void(float)> progress_callback) {
|
||||||
|
// TODO: Implement HTTP download using platform-specific APIs
|
||||||
|
// For now, return false as this is a placeholder
|
||||||
|
LOG_ERROR("HTTP download not yet implemented for: %s", url.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppManager::LoadInstalledApps() {
|
||||||
|
std::string registry_path = m_data_root + "/config/apps.json";
|
||||||
|
|
||||||
|
std::ifstream file(registry_path);
|
||||||
|
if (!file) {
|
||||||
|
LOG_INFO("No existing app registry found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
json j;
|
||||||
|
file >> j;
|
||||||
|
|
||||||
|
if (j.contains("apps") && j["apps"].is_array()) {
|
||||||
|
for (const auto& app_json : j["apps"]) {
|
||||||
|
InstalledApp app;
|
||||||
|
app.package_id = app_json.value("package_id", "");
|
||||||
|
app.name = app_json.value("name", "");
|
||||||
|
app.version_name = app_json.value("version_name", "");
|
||||||
|
app.version_code = app_json.value("version_code", 0);
|
||||||
|
app.install_path = app_json.value("install_path", "");
|
||||||
|
app.package_size = app_json.value("package_size", 0);
|
||||||
|
app.data_size = app_json.value("data_size", 0);
|
||||||
|
app.is_system_app = app_json.value("is_system_app", false);
|
||||||
|
app.entry_point = app_json.value("entry_point", "main.rml");
|
||||||
|
app.icon_path = app_json.value("icon_path", "");
|
||||||
|
app.developer_name = app_json.value("developer_name", "");
|
||||||
|
|
||||||
|
if (app_json.contains("permissions") && app_json["permissions"].is_array()) {
|
||||||
|
for (const auto& perm : app_json["permissions"]) {
|
||||||
|
app.permissions.push_back(perm.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse timestamps
|
||||||
|
if (app_json.contains("installed_at")) {
|
||||||
|
auto ts = app_json["installed_at"].get<int64_t>();
|
||||||
|
app.installed_at = std::chrono::system_clock::time_point(
|
||||||
|
std::chrono::seconds(ts));
|
||||||
|
}
|
||||||
|
if (app_json.contains("updated_at")) {
|
||||||
|
auto ts = app_json["updated_at"].get<int64_t>();
|
||||||
|
app.updated_at = std::chrono::system_clock::time_point(
|
||||||
|
std::chrono::seconds(ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app.package_id.empty()) {
|
||||||
|
m_installed_apps[app.package_id] = app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Loaded %zu installed apps", m_installed_apps.size());
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Error loading app registry: %s", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppManager::SaveInstalledApps() {
|
||||||
|
std::string registry_path = m_data_root + "/config/apps.json";
|
||||||
|
|
||||||
|
json j;
|
||||||
|
j["version"] = 1;
|
||||||
|
j["apps"] = json::array();
|
||||||
|
|
||||||
|
for (const auto& [id, app] : m_installed_apps) {
|
||||||
|
json app_json;
|
||||||
|
app_json["package_id"] = app.package_id;
|
||||||
|
app_json["name"] = app.name;
|
||||||
|
app_json["version_name"] = app.version_name;
|
||||||
|
app_json["version_code"] = app.version_code;
|
||||||
|
app_json["install_path"] = app.install_path;
|
||||||
|
app_json["permissions"] = app.permissions;
|
||||||
|
app_json["package_size"] = app.package_size;
|
||||||
|
app_json["data_size"] = app.data_size;
|
||||||
|
app_json["is_system_app"] = app.is_system_app;
|
||||||
|
app_json["entry_point"] = app.entry_point;
|
||||||
|
app_json["icon_path"] = app.icon_path;
|
||||||
|
app_json["developer_name"] = app.developer_name;
|
||||||
|
|
||||||
|
// Store timestamps as Unix seconds
|
||||||
|
app_json["installed_at"] = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
app.installed_at.time_since_epoch()).count();
|
||||||
|
app_json["updated_at"] = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
app.updated_at.time_since_epoch()).count();
|
||||||
|
|
||||||
|
j["apps"].push_back(app_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream file(registry_path);
|
||||||
|
if (file) {
|
||||||
|
file << j.dump(2);
|
||||||
|
LOG_DEBUG("Saved app registry with %zu apps", m_installed_apps.size());
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Failed to save app registry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t AppManager::CalculateDirectorySize(const std::string& path) const {
|
||||||
|
int64_t size = 0;
|
||||||
|
try {
|
||||||
|
for (const auto& entry : fs::recursive_directory_iterator(path)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
size += static_cast<int64_t>(entry.file_size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Directory may not exist
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AppManager::GenerateUUID() const {
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 gen(rd());
|
||||||
|
std::uniform_int_distribution<> dis(0, 15);
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
for (int i = 0; i < 32; ++i) {
|
||||||
|
if (i == 8 || i == 12 || i == 16 || i == 20) {
|
||||||
|
ss << '-';
|
||||||
|
}
|
||||||
|
ss << std::hex << dis(gen);
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
167
src/main/cpp/apps/app_manager.h
Normal file
167
src/main/cpp/apps/app_manager.h
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// app_manager.h - App installation and management
|
||||||
|
// Milestone 10: Device-Side App Management
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class LuaSandboxManager;
|
||||||
|
|
||||||
|
// Information about an installed app
|
||||||
|
struct InstalledApp {
|
||||||
|
std::string package_id;
|
||||||
|
std::string name;
|
||||||
|
std::string version_name;
|
||||||
|
int version_code = 0;
|
||||||
|
std::string install_path;
|
||||||
|
std::vector<std::string> permissions;
|
||||||
|
std::chrono::system_clock::time_point installed_at;
|
||||||
|
std::chrono::system_clock::time_point updated_at;
|
||||||
|
int64_t package_size = 0;
|
||||||
|
int64_t data_size = 0;
|
||||||
|
bool is_system_app = false;
|
||||||
|
std::string entry_point;
|
||||||
|
std::string icon_path;
|
||||||
|
std::string developer_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Progress stages during installation
|
||||||
|
struct InstallProgress {
|
||||||
|
enum class Stage {
|
||||||
|
Downloading,
|
||||||
|
Verifying,
|
||||||
|
Extracting,
|
||||||
|
Registering,
|
||||||
|
Complete,
|
||||||
|
Failed
|
||||||
|
};
|
||||||
|
|
||||||
|
Stage stage = Stage::Downloading;
|
||||||
|
float progress = 0.0f; // 0.0 - 1.0
|
||||||
|
std::string error;
|
||||||
|
|
||||||
|
static const char* StageName(Stage s) {
|
||||||
|
switch (s) {
|
||||||
|
case Stage::Downloading: return "downloading";
|
||||||
|
case Stage::Verifying: return "verifying";
|
||||||
|
case Stage::Extracting: return "extracting";
|
||||||
|
case Stage::Registering: return "registering";
|
||||||
|
case Stage::Complete: return "complete";
|
||||||
|
case Stage::Failed: return "failed";
|
||||||
|
default: return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using ProgressCallback = std::function<void(const InstallProgress&)>;
|
||||||
|
|
||||||
|
// Manifest parsed from package
|
||||||
|
struct AppManifest {
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
std::string version;
|
||||||
|
int version_code = 0;
|
||||||
|
std::string entry;
|
||||||
|
std::string icon;
|
||||||
|
std::string description;
|
||||||
|
std::string developer_name;
|
||||||
|
std::string developer_email;
|
||||||
|
std::vector<std::string> permissions;
|
||||||
|
int min_api_version = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AppManager {
|
||||||
|
public:
|
||||||
|
explicit AppManager(const std::string& data_root);
|
||||||
|
~AppManager();
|
||||||
|
|
||||||
|
// Prevent copying
|
||||||
|
AppManager(const AppManager&) = delete;
|
||||||
|
AppManager& operator=(const AppManager&) = delete;
|
||||||
|
|
||||||
|
// Installation from URL
|
||||||
|
bool Install(const std::string& package_url,
|
||||||
|
const std::string& signature,
|
||||||
|
ProgressCallback callback);
|
||||||
|
|
||||||
|
// Installation from local file
|
||||||
|
bool InstallFromFile(const std::string& package_path,
|
||||||
|
ProgressCallback callback);
|
||||||
|
|
||||||
|
// Uninstallation
|
||||||
|
bool Uninstall(const std::string& package_id, bool keep_data = false);
|
||||||
|
|
||||||
|
// Updates
|
||||||
|
bool Update(const std::string& package_id,
|
||||||
|
const std::string& package_url,
|
||||||
|
const std::string& signature,
|
||||||
|
ProgressCallback callback);
|
||||||
|
|
||||||
|
// Query installed apps
|
||||||
|
std::vector<InstalledApp> GetInstalledApps() const;
|
||||||
|
std::optional<InstalledApp> GetApp(const std::string& package_id) const;
|
||||||
|
bool IsInstalled(const std::string& package_id) const;
|
||||||
|
|
||||||
|
// Data management
|
||||||
|
int64_t GetAppDataSize(const std::string& package_id) const;
|
||||||
|
bool ClearAppData(const std::string& package_id);
|
||||||
|
bool ClearAppCache(const std::string& package_id);
|
||||||
|
bool BackupAppData(const std::string& package_id);
|
||||||
|
bool RestoreAppData(const std::string& package_id);
|
||||||
|
|
||||||
|
// App launching
|
||||||
|
bool LaunchApp(const std::string& package_id);
|
||||||
|
bool StopApp(const std::string& package_id);
|
||||||
|
bool IsAppRunning(const std::string& package_id) const;
|
||||||
|
|
||||||
|
// Integration with sandbox manager
|
||||||
|
void SetSandboxManager(LuaSandboxManager* manager);
|
||||||
|
|
||||||
|
// Get paths
|
||||||
|
std::string GetDataRoot() const { return m_data_root; }
|
||||||
|
std::string GetAppPath(const std::string& package_id) const;
|
||||||
|
std::string GetAppDataPath(const std::string& package_id) const;
|
||||||
|
std::string GetAppCachePath(const std::string& package_id) const;
|
||||||
|
|
||||||
|
// System apps registration
|
||||||
|
void RegisterSystemApp(const InstalledApp& app);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Package verification
|
||||||
|
bool VerifyPackage(const std::string& path);
|
||||||
|
bool VerifySignature(const std::string& path, const std::string& signature);
|
||||||
|
|
||||||
|
// Package operations
|
||||||
|
std::optional<AppManifest> ExtractManifest(const std::string& package_path);
|
||||||
|
bool ExtractPackage(const std::string& package_path, const std::string& dest_path);
|
||||||
|
|
||||||
|
// Download helper
|
||||||
|
bool DownloadFile(const std::string& url, const std::string& dest_path,
|
||||||
|
std::function<void(float)> progress_callback);
|
||||||
|
|
||||||
|
// Registry persistence
|
||||||
|
void LoadInstalledApps();
|
||||||
|
void SaveInstalledApps();
|
||||||
|
|
||||||
|
// Directory size calculation
|
||||||
|
int64_t CalculateDirectorySize(const std::string& path) const;
|
||||||
|
|
||||||
|
// Generate unique ID
|
||||||
|
std::string GenerateUUID() const;
|
||||||
|
|
||||||
|
std::string m_data_root;
|
||||||
|
LuaSandboxManager* m_sandbox_manager = nullptr;
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
std::map<std::string, InstalledApp> m_installed_apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
288
src/main/cpp/apps/update_service.cpp
Normal file
288
src/main/cpp/apps/update_service.cpp
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
// update_service.cpp - Background app update checker implementation
|
||||||
|
// Milestone 10: Device-Side App Management
|
||||||
|
|
||||||
|
#include "update_service.h"
|
||||||
|
#include "../logger.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <sstream>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
UpdateService::UpdateService(AppManager* app_manager, const std::string& api_base)
|
||||||
|
: m_app_manager(app_manager)
|
||||||
|
, m_api_base(api_base)
|
||||||
|
{
|
||||||
|
LOG_INFO("UpdateService initialized with API: %s", api_base.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateService::~UpdateService() {
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateService::Start(std::chrono::hours interval) {
|
||||||
|
if (m_running) {
|
||||||
|
LOG_WARN("UpdateService already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_check_interval = interval;
|
||||||
|
m_running = true;
|
||||||
|
m_stop_requested = false;
|
||||||
|
|
||||||
|
m_check_thread = std::thread(&UpdateService::CheckLoop, this);
|
||||||
|
|
||||||
|
LOG_INFO("UpdateService started with %lld hour interval",
|
||||||
|
static_cast<long long>(interval.count()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateService::Stop() {
|
||||||
|
if (!m_running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_stop_requested = true;
|
||||||
|
|
||||||
|
if (m_check_thread.joinable()) {
|
||||||
|
m_check_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_running = false;
|
||||||
|
LOG_INFO("UpdateService stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<UpdateInfo> UpdateService::CheckForUpdates() {
|
||||||
|
std::vector<UpdateInfo> updates;
|
||||||
|
|
||||||
|
// Get list of installed apps
|
||||||
|
auto installed = m_app_manager->GetInstalledApps();
|
||||||
|
if (installed.empty()) {
|
||||||
|
LOG_DEBUG("No apps installed, skipping update check");
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query string with package IDs and versions
|
||||||
|
std::stringstream package_params;
|
||||||
|
for (size_t i = 0; i < installed.size(); ++i) {
|
||||||
|
if (i > 0) package_params << ",";
|
||||||
|
package_params << installed[i].package_id << ":" << installed[i].version_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call API
|
||||||
|
std::string url = m_api_base + "/store/apps/updates?packages=" + package_params.str();
|
||||||
|
LOG_DEBUG("Checking for updates: %s", url.c_str());
|
||||||
|
|
||||||
|
std::string response = HttpGet(url);
|
||||||
|
if (response.empty()) {
|
||||||
|
LOG_WARN("Update check failed: no response from server");
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
try {
|
||||||
|
json j = json::parse(response);
|
||||||
|
|
||||||
|
if (j.contains("updates") && j["updates"].is_array()) {
|
||||||
|
for (const auto& update_json : j["updates"]) {
|
||||||
|
UpdateInfo info;
|
||||||
|
info.package_id = update_json.value("package_id", "");
|
||||||
|
info.name = update_json.value("name", "");
|
||||||
|
info.new_version = update_json.value("version", "");
|
||||||
|
info.new_version_code = update_json.value("version_code", 0);
|
||||||
|
info.download_url = update_json.value("download_url", "");
|
||||||
|
info.signature = update_json.value("signature", "");
|
||||||
|
info.download_size = update_json.value("size", 0);
|
||||||
|
info.release_notes = update_json.value("release_notes", "");
|
||||||
|
info.is_critical = update_json.value("critical", false);
|
||||||
|
|
||||||
|
// Get current version from installed apps
|
||||||
|
auto current = m_app_manager->GetApp(info.package_id);
|
||||||
|
if (current) {
|
||||||
|
info.current_version = current->version_name;
|
||||||
|
info.current_version_code = current->version_code;
|
||||||
|
|
||||||
|
// Only add if actually newer
|
||||||
|
if (info.new_version_code > info.current_version_code) {
|
||||||
|
updates.push_back(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Found %zu available updates", updates.size());
|
||||||
|
|
||||||
|
} catch (const json::exception& e) {
|
||||||
|
LOG_ERROR("Failed to parse update response: %s", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last check time
|
||||||
|
m_last_check = std::chrono::system_clock::now();
|
||||||
|
|
||||||
|
// Store pending updates
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_pending_updates = updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateService::CheckForUpdatesAsync(UpdateCheckCallback callback) {
|
||||||
|
std::thread([this, callback]() {
|
||||||
|
auto updates = CheckForUpdates();
|
||||||
|
if (callback) {
|
||||||
|
callback(updates);
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UpdateService::ApplyUpdate(const std::string& package_id, ProgressCallback callback) {
|
||||||
|
// Find update info
|
||||||
|
UpdateInfo update_info;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
for (const auto& update : m_pending_updates) {
|
||||||
|
if (update.package_id == package_id) {
|
||||||
|
update_info = update;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
LOG_ERROR("No pending update found for: %s", package_id.c_str());
|
||||||
|
if (callback) {
|
||||||
|
callback({InstallProgress::Stage::Failed, 0.0f, "No update available"});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Applying update: %s %s -> %s",
|
||||||
|
package_id.c_str(),
|
||||||
|
update_info.current_version.c_str(),
|
||||||
|
update_info.new_version.c_str());
|
||||||
|
|
||||||
|
// Use AppManager to download and install
|
||||||
|
bool success = m_app_manager->Update(package_id,
|
||||||
|
update_info.download_url,
|
||||||
|
update_info.signature,
|
||||||
|
callback);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Remove from pending updates
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_pending_updates.erase(
|
||||||
|
std::remove_if(m_pending_updates.begin(), m_pending_updates.end(),
|
||||||
|
[&](const UpdateInfo& u) { return u.package_id == package_id; }),
|
||||||
|
m_pending_updates.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateService::ApplyUpdateAsync(const std::string& package_id,
|
||||||
|
UpdateAppliedCallback callback) {
|
||||||
|
std::thread([this, package_id, callback]() {
|
||||||
|
bool success = ApplyUpdate(package_id, nullptr);
|
||||||
|
if (callback) {
|
||||||
|
callback(package_id, success);
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateService::ApplyAllUpdates(UpdateAppliedCallback callback) {
|
||||||
|
std::vector<UpdateInfo> updates;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
updates = m_pending_updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& update : updates) {
|
||||||
|
bool success = ApplyUpdate(update.package_id, nullptr);
|
||||||
|
if (callback) {
|
||||||
|
callback(update.package_id, success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<UpdateInfo> UpdateService::GetPendingUpdates() const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_pending_updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UpdateService::HasPendingUpdates() const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return !m_pending_updates.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t UpdateService::GetPendingUpdateCount() const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_pending_updates.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateService::CheckLoop() {
|
||||||
|
LOG_DEBUG("UpdateService check loop started");
|
||||||
|
|
||||||
|
// Wait for initial delay before first check
|
||||||
|
auto wait_until = std::chrono::system_clock::now() + std::chrono::minutes(5);
|
||||||
|
|
||||||
|
while (!m_stop_requested) {
|
||||||
|
// Wait until next check time or stop requested
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
if (now < wait_until) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if on WiFi (if required)
|
||||||
|
if (m_wifi_only && !IsOnWifi()) {
|
||||||
|
LOG_DEBUG("Skipping update check: not on WiFi");
|
||||||
|
wait_until = std::chrono::system_clock::now() + std::chrono::minutes(5);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform update check
|
||||||
|
LOG_DEBUG("Performing scheduled update check");
|
||||||
|
auto updates = CheckForUpdates();
|
||||||
|
|
||||||
|
// Notify callback if updates available
|
||||||
|
if (!updates.empty() && m_on_updates_available) {
|
||||||
|
m_on_updates_available(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-update if enabled
|
||||||
|
if (m_auto_update && !updates.empty()) {
|
||||||
|
if (!m_wifi_only || IsOnWifi()) {
|
||||||
|
LOG_INFO("Auto-updating %zu apps", updates.size());
|
||||||
|
ApplyAllUpdates(nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule next check
|
||||||
|
wait_until = std::chrono::system_clock::now() + m_check_interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("UpdateService check loop ended");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string UpdateService::HttpGet(const std::string& url) {
|
||||||
|
// TODO: Implement HTTP GET using platform-specific APIs
|
||||||
|
// On Android, this would use JNI to call Java HttpURLConnection
|
||||||
|
// For now, return empty string as placeholder
|
||||||
|
LOG_WARN("HTTP GET not yet implemented for: %s", url.c_str());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UpdateService::IsOnWifi() const {
|
||||||
|
// TODO: Implement WiFi check using platform-specific APIs
|
||||||
|
// For now, assume always on WiFi
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
109
src/main/cpp/apps/update_service.h
Normal file
109
src/main/cpp/apps/update_service.h
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// update_service.h - Background app update checker
|
||||||
|
// Milestone 10: Device-Side App Management
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_manager.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
// Information about an available update
|
||||||
|
struct UpdateInfo {
|
||||||
|
std::string package_id;
|
||||||
|
std::string name;
|
||||||
|
std::string current_version;
|
||||||
|
std::string new_version;
|
||||||
|
int current_version_code = 0;
|
||||||
|
int new_version_code = 0;
|
||||||
|
std::string download_url;
|
||||||
|
std::string signature;
|
||||||
|
int64_t download_size = 0;
|
||||||
|
std::string release_notes;
|
||||||
|
bool is_critical = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callback for update events
|
||||||
|
using UpdateCheckCallback = std::function<void(const std::vector<UpdateInfo>& updates)>;
|
||||||
|
using UpdateAppliedCallback = std::function<void(const std::string& package_id, bool success)>;
|
||||||
|
|
||||||
|
class UpdateService {
|
||||||
|
public:
|
||||||
|
UpdateService(AppManager* app_manager, const std::string& api_base);
|
||||||
|
~UpdateService();
|
||||||
|
|
||||||
|
// Prevent copying
|
||||||
|
UpdateService(const UpdateService&) = delete;
|
||||||
|
UpdateService& operator=(const UpdateService&) = delete;
|
||||||
|
|
||||||
|
// Start background update checking
|
||||||
|
void Start(std::chrono::hours interval = std::chrono::hours(24));
|
||||||
|
void Stop();
|
||||||
|
bool IsRunning() const { return m_running; }
|
||||||
|
|
||||||
|
// Manual update check
|
||||||
|
std::vector<UpdateInfo> CheckForUpdates();
|
||||||
|
void CheckForUpdatesAsync(UpdateCheckCallback callback);
|
||||||
|
|
||||||
|
// Download and install update
|
||||||
|
bool ApplyUpdate(const std::string& package_id, ProgressCallback callback);
|
||||||
|
void ApplyUpdateAsync(const std::string& package_id, UpdateAppliedCallback callback);
|
||||||
|
|
||||||
|
// Apply all available updates
|
||||||
|
void ApplyAllUpdates(UpdateAppliedCallback callback);
|
||||||
|
|
||||||
|
// Get pending updates (from last check)
|
||||||
|
std::vector<UpdateInfo> GetPendingUpdates() const;
|
||||||
|
bool HasPendingUpdates() const;
|
||||||
|
size_t GetPendingUpdateCount() const;
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
void SetAutoUpdate(bool enabled) { m_auto_update = enabled; }
|
||||||
|
bool GetAutoUpdate() const { return m_auto_update; }
|
||||||
|
|
||||||
|
void SetWifiOnly(bool wifi_only) { m_wifi_only = wifi_only; }
|
||||||
|
bool GetWifiOnly() const { return m_wifi_only; }
|
||||||
|
|
||||||
|
void SetCheckInterval(std::chrono::hours interval) { m_check_interval = interval; }
|
||||||
|
std::chrono::hours GetCheckInterval() const { return m_check_interval; }
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
void SetOnUpdatesAvailable(UpdateCheckCallback callback) { m_on_updates_available = callback; }
|
||||||
|
|
||||||
|
// Get last check time
|
||||||
|
std::chrono::system_clock::time_point GetLastCheckTime() const { return m_last_check; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Background thread function
|
||||||
|
void CheckLoop();
|
||||||
|
|
||||||
|
// HTTP request helpers
|
||||||
|
std::string HttpGet(const std::string& url);
|
||||||
|
bool IsOnWifi() const;
|
||||||
|
|
||||||
|
AppManager* m_app_manager;
|
||||||
|
std::string m_api_base;
|
||||||
|
|
||||||
|
std::thread m_check_thread;
|
||||||
|
std::atomic<bool> m_running{false};
|
||||||
|
std::atomic<bool> m_stop_requested{false};
|
||||||
|
|
||||||
|
std::chrono::hours m_check_interval{24};
|
||||||
|
std::chrono::system_clock::time_point m_last_check;
|
||||||
|
|
||||||
|
bool m_auto_update = false;
|
||||||
|
bool m_wifi_only = true;
|
||||||
|
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
std::vector<UpdateInfo> m_pending_updates;
|
||||||
|
|
||||||
|
UpdateCheckCallback m_on_updates_available;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
Reference in New Issue
Block a user