info.voidstar/tbnl.figurehead0.1.1-SNAPSHOTfigurehead sits on device and is controlled by mastermind dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||||||||
namespaces
| |||||||||||||||||||||||||
(ns figurehead.api.app.activity-controller (:require (figurehead.util [services :as services :refer [get-service]])) (:require [clojure.string :as str] [clojure.java.io :as io]) (:import (android.app IActivityManager IActivityController$Stub) (android.content Intent))) | |||||||||||||||||||||||||
(declare set-activity-controller) | |||||||||||||||||||||||||
set ActivityController | (defn set-activity-controller [{:keys [reset? activity-controller activity-starting activity-resuming app-crashed app-early-not-responding app-not-responding system-not-responding] :as args}] (let [activity-manager ^IActivityManager (get-service :activity-manager) activity-controller (cond reset? nil activity-controller activity-controller :else (proxy [IActivityController$Stub] [] (activityStarting [^Intent intent package] (locking this (if activity-starting (activity-starting intent package) true))) (activityResuming [package] (locking this (if activity-resuming (activity-resuming package) true))) (appCrashed [process-name pid short-msg long-msg time-millis stack-trace] (locking this (if app-crashed (app-crashed process-name pid short-msg long-msg time-millis stack-trace) true))) (appEarlyNotResponding [process-name pid annotation] (locking this (if app-early-not-responding (app-early-not-responding process-name pid annotation) 1))) (appNotResponding [process-name pid process-stats] (locking this (if app-not-responding (app-not-responding process-name pid process-stats) 1))) (systemNotResponding [msg] (locking this (if system-not-responding (system-not-responding msg) 1)))))] (.setActivityController activity-manager activity-controller))) | ||||||||||||||||||||||||
am (Activity Manager) wrapper | (ns figurehead.api.app.activity-manager (:require (core [state :as state :refer [defcommand]])) (:require (figurehead.util [services :as services :refer [get-service]]) (figurehead.api.content [intent :as intent])) (:require [clojure.string :as str] [clojure.java.io :as io]) (:import (android.app IActivityManager AppOpsManager) (android.content Intent IIntentReceiver$Stub ComponentName) (android.net Uri) (android.os Bundle Binder))) | ||||||||||||||||||||||||
(declare start-activity start-service force-stop kill kill-all send-broadcast hang intent-to-uri) | |||||||||||||||||||||||||
start an Activity (accept all figurehead.api.content.intent/make-intent arguments) | (defcommand start-activity [{:keys [wait?] :as args}] (let [intent ^Intent (intent/make-intent args) activity-manager ^IActivityManager (get-service :activity-manager) mime-type (atom nil)] (when intent (reset! mime-type (.getType intent)) (when (and (not @mime-type) (.getData intent) (= (.. intent getData getScheme) "content")) (reset! mime-type (.getProviderMimeType activity-manager (.getData intent) 0))) (if wait? (.. activity-manager ^IactivityManager$WaitResult (startActivityAndWait nil nil ^Intent intent ^String @mime-type nil nil 0 0 nil nil nil 0) result) (.. activity-manager (startActivityAsUser nil nil ^Intent intent ^String @mime-type nil nil 0 0 nil nil nil 0)))))) | ||||||||||||||||||||||||
start Service (accept all figurehead.api.content.intent/make-intent arguments) | (defcommand start-service [{:keys [] :as args}] (let [intent ^Intent (intent/make-intent args) activity-manager ^IActivityManager (get-service :activity-manager)] (when intent (.. activity-manager ^ComponentName (startService nil intent (.getType intent) 0))))) | ||||||||||||||||||||||||
force stop a Package | (defcommand force-stop [{:keys [package] :as args}] (let [activity-manager ^IActivityManager (get-service :activity-manager)] (.forceStopPackage activity-manager package 0))) | ||||||||||||||||||||||||
kill a Package | (defcommand kill [{:keys [package] :as args}] (let [activity-manager ^IActivityManager (get-service :activity-manager)] (.killBackgroundProcesses activity-manager package 0))) | ||||||||||||||||||||||||
kill all Packages | (defcommand kill-all [{:keys [] :as args}] (let [activity-manager ^IActivityManager (get-service :activity-manager)] (.killAllBackgroundProcesses activity-manager))) | ||||||||||||||||||||||||
send broadcast | (defcommand send-broadcast [{:keys [perform-receive receiver-permission] :as args}] (let [intent ^Intent (intent/make-intent args) activity-manager ^IActivityManager (get-service :activity-manager) intent-receiver (proxy [IIntentReceiver$Stub] [] (performReceive [^Intent intent result-code ^String data ^Bundle extras ordered sticky sending-user] (when perform-receive (perform-receive {:intent intent :result-code result-code :data data :extras extras :ordered ordered :sticky sticky :sending-user sending-user}))))] (when (and intent intent-receiver) (.broadcastIntent activity-manager nil intent nil intent-receiver 0 nil nil receiver-permission AppOpsManager/OP_NONE true false 0)))) | ||||||||||||||||||||||||
hang | (defcommand hang [{:keys [allow-restart] :as args}] (let [activity-manager ^IActivityManager (get-service :activity-manager)] (.hang activity-manager (Binder.) allow-restart))) | ||||||||||||||||||||||||
convert intent to URI | (defcommand intent-to-uri [{:keys [intent-scheme?] :as args}] (let [intent ^Intent (intent/make-intent args)] (when (and intent) (.toUri intent (if intent-scheme? Intent/URI_INTENT_SCHEME 0))))) | ||||||||||||||||||||||||
create android.content.Intent https://github.com/android/platformframeworksbase/blob/android-4.3_r3.1/cmds/am/src/com/android/commands/am/Am.java#L479 | (ns figurehead.api.content.intent (:require (core [state :as state :refer [defcommand]])) (:import (android.content Intent ComponentName) (android.os Bundle) (android.net Uri))) | ||||||||||||||||||||||||
(declare make-intent) | |||||||||||||||||||||||||
make an Intent object | (defcommand make-intent [{:keys [action categories extras package component flags ^Uri data type wild-card ] :as args}] (let [intent (Intent.)] ;; action (when action (.setAction ^Intent intent action)) ;; a seq of categories (when categories (doseq [category categories] (.addCategory ^Intent intent category))) ;; a seq of extras (when extras (doseq [[key val] extras] (.putExtra ^Intent intent key val))) ;; package (when package (.setPackage ^Intent intent package)) ;; component (when component (.setComponent ^Intent intent (ComponentName/unflattenFromString component))) ;; flags can be either a number or a seq of individual flags (Intent/FLAG_*) (when flags (cond (number? flags) (.setFlags ^Intent intent flags) (seq? flags) (doseq [flag flags] (.addFlags ^Intent intent flag)))) ;; data and type (.setDataAndType ^Intent intent data type) ;; free-form wild-card (when wild-card (let [wild-card (str wild-card)] (cond (>= (.indexOf wild-card ":") 0) (do ;; wild-card is a URI; fully parse it (.setData intent (Intent/parseUri wild-card Intent/URI_INTENT_SCHEME))) (>= (.indexOf wild-card "/") 0) (do ;; wild-card is a component name; build an intent to launch it (.setAction ^Intent intent Intent/ACTION_MAIN) (.addCategory ^Intent intent Intent/CATEGORY_LAUNCHER) (.setComponent ^Intent intent (ComponentName/unflattenFromString wild-card))) :else (do ;; assume wild-card is a package name (.setAction ^Intent intent Intent/ACTION_MAIN) (.addCategory ^Intent intent Intent/CATEGORY_LAUNCHER) (.setPackage ^Intent intent wild-card))))) intent)) | ||||||||||||||||||||||||
pm (Package Manager) wrapper https://github.com/android/platformframeworksbase/blob/android-4.3_r3.1/cmds/pm/src/com/android/commands/pm/Pm.java | (ns figurehead.api.content.pm.package-manager (:require (core [state :as state :refer [defcommand]])) (:require (figurehead.util [services :as services :refer [get-service]])) (:require [figurehead.api.content.pm.package-manager-parser :as parser] [figurehead.api.util.file :as util-file]) (:require [clojure.string :as str] [clojure.java.io :as io]) (:import (android.app IActivityManager) (android.content ComponentName) (android.content.pm ActivityInfo ApplicationInfo ContainerEncryptionParams FeatureInfo IPackageDataObserver IPackageDataObserver$Stub IPackageDeleteObserver IPackageDeleteObserver$Stub IPackageInstallObserver IPackageInstallObserver$Stub IPackageManager InstrumentationInfo PackageInfo PackageItemInfo PackageManager ParceledListSlice PermissionGroupInfo PermissionInfo ProviderInfo ServiceInfo UserInfo VerificationParams) (android.content.res AssetManager Resources) (android.net Uri) (android.os IUserManager RemoteException ServiceManager UserHandle UserManager) (android.util Base64) (com.android.internal.content PackageHelper) (java.io File) (javax.crypto SecretKey) (javax.crypto.spec IvParameterSpec SecretKeySpec) (org.apache.commons.io FileUtils))) | ||||||||||||||||||||||||
(declare get-raw-packages get-packages get-all-package-names get-package-components get-raw-features get-features get-raw-libraries get-libraries get-raw-instrumentations get-instrumentations get-raw-permission-groups get-permissions-by-group get-install-location set-install-location push-file pull-file make-package-install-observer install-package make-package-delete-observer uninstall-package make-package-data-observer clear-package-data) | |||||||||||||||||||||||||
get all packages on this device | (defcommand get-raw-packages [{:keys [] :as args}] (let [^IPackageManager package-manager (get-service :package-manager)] (let [packages (.. package-manager (getInstalledPackages (bit-or PackageManager/GET_ACTIVITIES PackageManager/GET_CONFIGURATIONS PackageManager/GET_DISABLED_COMPONENTS PackageManager/GET_DISABLED_UNTIL_USED_COMPONENTS PackageManager/GET_GIDS PackageManager/GET_INSTRUMENTATION PackageManager/GET_INTENT_FILTERS PackageManager/GET_PERMISSIONS PackageManager/GET_PROVIDERS PackageManager/GET_RECEIVERS PackageManager/GET_SERVICES PackageManager/GET_SIGNATURES) 0) getList)] (seq packages)))) | ||||||||||||||||||||||||
get all packages on this device | (defcommand get-packages [{:keys [brief?] :as args}] (let [packages (get-raw-packages {}) result (atom {})] (doseq [^PackageInfo package packages] (let [package (parser/parse-package-info package)] (swap! result assoc (keyword (:package-name package)) (when-not brief? package)))) @result)) | ||||||||||||||||||||||||
get list of package names | (defcommand get-all-package-names [{:keys [] :as args}] (let [packages (get-raw-packages {}) result (atom #{})] (doseq [^PackageInfo package packages] (swap! result conj (keyword (.packageName package)))) @result)) | ||||||||||||||||||||||||
get app components for a specific package | (defcommand get-package-components [{:keys [package] :as args}] (when package (let [package-manager ^IPackageManager (get-service :package-manager)] (when-let [pkg-info (.getPackageInfo package-manager (cond (keyword? package) (name package) (sequential? package) (str/join "." (map #(cond (keyword? %) (name %) :else (str %)) package)) :else (str package)) (bit-or PackageManager/GET_ACTIVITIES PackageManager/GET_PROVIDERS PackageManager/GET_RECEIVERS PackageManager/GET_SERVICES PackageManager/GET_PERMISSIONS) 0)] {:activities (set (for [^ActivityInfo activity (.activities pkg-info)] (keyword (.name activity)))) :services (set (for [^ServiceInfo service (.services pkg-info)] (keyword (.name service)))) :providers (set (for [^ProviderInfo provider (.providers pkg-info)] (keyword (.name provider)))) :receivers (set (for [^ActivityInfo receiver (.receivers pkg-info)] (keyword (.name receiver)))) :permissions (set (for [^PermissionInfo permission (.permissions pkg-info)] (keyword (.name permission))))})))) | ||||||||||||||||||||||||
get all features on this device | (defcommand get-raw-features [{:keys [] :as args}] (let [^IPackageManager package-manager (get-service :package-manager)] (let [features (.. package-manager getSystemAvailableFeatures)] (seq features)))) | ||||||||||||||||||||||||
get all features on this device | (defcommand get-features [{:keys [brief?] :as args}] (let [features (get-raw-features {}) result (atom {})] (doseq [^FeatureInfo feature features] (let [feature (parser/parse-feature-info feature)] (swap! result assoc (keyword (:name feature)) (when-not brief? feature)))) @result)) | ||||||||||||||||||||||||
get all libraries on this device | (defcommand get-raw-libraries [{:keys [] :as args}] (let [^IPackageManager package-manager (get-service :package-manager)] (let [libraries (.. package-manager getSystemSharedLibraryNames)] (seq libraries)))) | ||||||||||||||||||||||||
get all libraries on this device | (defcommand get-libraries [{:keys [] :as args}] (let [libraries (get-raw-libraries {}) result (atom [])] (doseq [^String library libraries] (swap! result conj library)) @result)) | ||||||||||||||||||||||||
get installed instrumentations on this device, optional for a specific package | (defcommand get-raw-instrumentations [{:keys [^String package] :as args}] (let [^IPackageManager package-manager (get-service :package-manager)] (let [instrumentations (.. package-manager (queryInstrumentation package 0))] (seq instrumentations)))) | ||||||||||||||||||||||||
get installed instrumentations on this device, optional for a specific package | (defcommand get-instrumentations [{:keys [^String package brief?] :as args}] (let [instrumentations (get-raw-instrumentations {:package package}) result (atom {})] (doseq [^InstrumentationInfo instrumentation instrumentations] (let [instrumentation (parser/parse-instrumentation-info instrumentation)] (swap! result assoc (keyword (:name instrumentation)) (when-not brief? instrumentation)))) @result)) | ||||||||||||||||||||||||
get permission groups | (defcommand get-raw-permission-groups [{:keys [] :as args}] (let [^IPackageManager package-manager (get-service :package-manager)] (let [permission-groups (.. package-manager (getAllPermissionGroups 0))] (seq permission-groups)))) | ||||||||||||||||||||||||
get all permissions by group | (defcommand get-permissions-by-group [{:keys [brief?] :as args}] (let [permission-groups (get-raw-permission-groups {}) result (atom {})] (doseq [^PermissionGroupInfo permission-group permission-groups] (let [permission-group (parser/parse-permission-group-info permission-group)] (swap! result assoc (keyword (:name permission-group)) (merge {} (when-not brief? permission-group) {:permissions (let [^IPackageManager package-manager (get-service :package-manager) result (atom {})] (doseq [^PermissionInfo permission (.queryPermissionsByGroup package-manager (:name permission-group) 0)] (let [permission (parser/parse-permission-info permission)] (swap! result assoc (keyword (:name permission)) (when-not brief? permission)))) @result)})))) @result)) | ||||||||||||||||||||||||
get install location | (defcommand get-install-location [{:keys [] :as args}] (let [^IPackageManager package-manager (get-service :package-manager) location (.getInstallLocation package-manager)] (cond (= location PackageHelper/APP_INSTALL_AUTO) :auto (= location PackageHelper/APP_INSTALL_EXTERNAL) :external (= location PackageHelper/APP_INSTALL_INTERNAL) :internal :else location))) | ||||||||||||||||||||||||
set install location | (defcommand set-install-location [{:keys [location] :or {location 0} :as args}] {:pre [(contains? #{PackageHelper/APP_INSTALL_AUTO PackageHelper/APP_INSTALL_EXTERNAL PackageHelper/APP_INSTALL_INTERNAL :auto :internal :external} location)]} (let [location (cond (contains? #{PackageHelper/APP_INSTALL_AUTO PackageHelper/APP_INSTALL_EXTERNAL PackageHelper/APP_INSTALL_INTERNAL} location) location (contains? #{:auto :internal :external} location) ({:auto PackageHelper/APP_INSTALL_AUTO :internal PackageHelper/APP_INSTALL_INTERNAL :external PackageHelper/APP_INSTALL_EXTERNAL} location))] (when location (let [^IPackageManager package-manager (get-service :package-manager)] (.setInstallLocation package-manager location))))) | ||||||||||||||||||||||||
push Base64-encoded content to device and write to file | (defcommand push-file [{:keys [^String content file] :as args}] {:pre [content file]} (when (and content file) (util-file/write-file {:file file :content content}))) | ||||||||||||||||||||||||
pull file from device and encode to Base64-encoded content | (defcommand pull-file [{:keys [file] :as args}] {:pre [file]} (when (and file) (util-file/read-file {:file file :content? true}))) | ||||||||||||||||||||||||
make instance of IPackageInstallObserver$Stub | (defcommand make-package-install-observer [{:keys [package-installed] :as args}] (proxy [IPackageInstallObserver$Stub] [] (packageInstalled [package-name status] (locking this (when package-installed (package-installed package-name status)))))) | ||||||||||||||||||||||||
install package | (defcommand install-package [{:keys [apk-file-name package-name forward-lock? replace-existing? allow-test? external? internal? allow-downgrade?] :as args}] (when (and apk-file-name package-name) (let [^IPackageManager package-manager (get-service :package-manager) flags (atom 0) apk-uri (Uri/fromFile (io/file apk-file-name))] (when (and apk-uri) (when forward-lock? (swap! flags bit-or PackageManager/INSTALL_FORWARD_LOCK)) (when replace-existing? (swap! flags bit-or PackageManager/INSTALL_REPLACE_EXISTING)) (when allow-test? (swap! flags bit-or PackageManager/INSTALL_ALLOW_TEST)) (when external? (swap! flags bit-or PackageManager/INSTALL_EXTERNAL)) (when internal? (swap! flags bit-or PackageManager/INSTALL_INTERNAL)) (when allow-downgrade? (swap! flags bit-or PackageManager/INSTALL_ALLOW_DOWNGRADE)) (let [finished? (promise) result (atom 0)] (.installPackage package-manager apk-uri (make-package-install-observer {:package-installed (fn [package-name status] (reset! result status) (deliver finished? true))}) @flags package-name) @finished? @result))))) | ||||||||||||||||||||||||
make instance of IPackageDeleteObserver$Stub | (defcommand make-package-delete-observer [{:keys [package-deleted] :as args}] (proxy [IPackageDeleteObserver$Stub] [] (packageDeleted [package-name return-code] (locking this (when package-deleted (package-deleted package-name return-code)))))) | ||||||||||||||||||||||||
uninstall package | (defcommand uninstall-package [{:keys [package keep-data?] :or {keep-data? true} :as args}] (when package (let [^IPackageManager package-manager (get-service :package-manager) flags (atom PackageManager/DELETE_ALL_USERS) finished? (promise) successful? (atom false)] (when keep-data? (swap! flags bit-or PackageManager/DELETE_KEEP_DATA)) (.deletePackageAsUser package-manager package (make-package-delete-observer {:package-deleted (fn [package-name return-code] (reset! successful? (= return-code PackageManager/DELETE_SUCCEEDED)) (deliver finished? true))}) UserHandle/USER_OWNER @flags) @finished? @successful?))) | ||||||||||||||||||||||||
make instance of IPackageDataObserver$Stub | (defcommand make-package-data-observer [{:keys [on-remove-completed] :as args}] (proxy [IPackageDataObserver$Stub] [] (onRemoveCompleted [package-name succeeded] (locking this (when on-remove-completed (on-remove-completed package-name succeeded)))))) | ||||||||||||||||||||||||
clear package data | (defcommand clear-package-data [{:keys [package] :as args}] (when package (let [^IActivityManager activity-manager (get-service :activity-manager) finished? (promise) successful? (atom false)] (.clearApplicationUserData activity-manager package (make-package-data-observer {:on-remove-completed (fn [package-name succeeded] (reset! successful? succeeded) (deliver finished? true))}) UserHandle/USER_OWNER) @finished? @successful?))) | ||||||||||||||||||||||||
parse Package Manager objects into Clojure data structures | (ns figurehead.api.content.pm.package-manager-parser (:require (figurehead.util [services :as services :refer [get-service]])) (:import (android.content ComponentName) (android.content.pm ActivityInfo ApplicationInfo ComponentInfo ConfigurationInfo ContainerEncryptionParams FeatureInfo IPackageDataObserver IPackageDeleteObserver IPackageInstallObserver IPackageInstallObserver$Stub IPackageManager InstrumentationInfo PackageInfo PackageItemInfo PackageManager ParceledListSlice PermissionGroupInfo PermissionInfo ProviderInfo ServiceInfo UserInfo VerificationParams) (android.content.res AssetManager Resources) (android.net Uri) (android.os IUserManager RemoteException ServiceManager UserHandle UserManager) (android.graphics.drawable Drawable) (java.util WeakHashMap) (javax.crypto SecretKey) (javax.crypto.spec IvParameterSpec SecretKeySpec))) | ||||||||||||||||||||||||
(declare resource-cache get-resources-by-package-name get-resources load-res-string load-res-resource-name load-res-drawable parse-package-info parse-package-item-info parse-component-info parse-activity-info parse-application-info parse-configuration-info parse-instrumentation-info parse-permission-group-info parse-permission-info parse-provider-info parse-feature-info parse-service-info) | |||||||||||||||||||||||||
resource cache {package-name resources} | (def resource-cache (WeakHashMap.)) | ||||||||||||||||||||||||
get resources by package name | (defn ^Resources get-resources-by-package-name [^String package-name] (if-let [res (.get ^WeakHashMap resource-cache package-name)] res (let [^IPackageManager package-manager (get-service :package-manager) ^ApplicationInfo application-info (.getApplicationInfo package-manager package-name 0 0) ^AssetManager asset-manager (AssetManager.)] (.addAssetPath asset-manager (.publicSourceDir application-info)) (let [res (Resources. asset-manager nil nil)] (.put ^WeakHashMap resource-cache package-name res) res)))) | ||||||||||||||||||||||||
get resources | (defn ^Resources get-resources [^PackageItemInfo package-item-info] (let [package-name (.packageName package-item-info)] (get-resources-by-package-name package-name))) | ||||||||||||||||||||||||
load string from resource | (defn ^String load-res-string [^PackageItemInfo package-item res non-localized] (let [resource (get-resources package-item)] (cond non-localized (str non-localized) (and resource res (not= res 0)) (.getString resource res)))) | ||||||||||||||||||||||||
load resource name from resource | (defn ^String load-res-resource-name [^PackageItemInfo package-item res] (let [resource (get-resources package-item)] (when (and resource res (not= res 0)) (.getResourceName resource res)))) | ||||||||||||||||||||||||
load drawable from resource | (defn ^Drawable load-res-drawable [^PackageItemInfo package-item res] (let [resource (get-resources package-item)] (when (and resource res (not= res 0)) (.getDrawable resource res)))) | ||||||||||||||||||||||||
parse PackageInfo | (defn parse-package-info [^PackageInfo package] (merge {} {:activities (set (map parse-activity-info (.activities package))) :application-info (parse-application-info (.applicationInfo package)) :config-preferences (set (map parse-configuration-info (.configPreferences package))) :first-install-time (.firstInstallTime package) :gids (set (.gids package)) :install-location (#(case % PackageInfo/INSTALL_LOCATION_AUTO :auto PackageInfo/INSTALL_LOCATION_INTERNAL_ONLY :internal-only PackageInfo/INSTALL_LOCATION_PREFER_EXTERNAL :prefer-external PackageInfo/INSTALL_LOCATION_UNSPECIFIED :unspecified %) (.installLocation package)) :instrumentation (set (map parse-instrumentation-info (.instrumentation package))) :last-update-time (.lastUpdateTime package) :package-name (.packageName package) :permissions (set (map parse-permission-info (.permissions package))) :providers (set (map parse-provider-info (.providers package))) :receivers (set (map parse-activity-info (.receivers package))) :req-features (set (map parse-feature-info (.reqFeatures package))) :requested-permissions (zipmap (vec (.requestedPermissions package)) (vec (map #(let [flags (atom #{})] (when (bit-and % PackageInfo/REQUESTED_PERMISSION_GRANTED) (swap! flags conj :granted)) (when (bit-and % PackageInfo/REQUESTED_PERMISSION_REQUIRED) (swap! flags conj :required)) @flags) (.requestedPermissionsFlags package)))) :required-account-type (.requiredAccountType package) :required-for-all-users (.requiredForAllUsers package) :restricted-account-type (.restrictedAccountType package) :services (set (map parse-service-info (.services package))) :shared-user-id (.sharedUserId package) :shared-user-label (.sharedUserLabel package) :signatures (set (.signatures package)) :version-code (.versionCode package) :version-name (.versionName package)})) | ||||||||||||||||||||||||
parse PackageItemInfo | (defn parse-package-item-info [^PackageItemInfo package-item] (merge {} {:icon (load-res-drawable package-item (.icon package-item)) :label (load-res-string package-item (.labelRes package-item) (.nonLocalizedLabel package-item)) :label-res (.labelRes package-item) :logo (load-res-drawable package-item (.logo package-item)) :meta-data (.metaData package-item) :name (.name package-item) :non-localized-label (str (.nonLocalizedLabel package-item)) :package-name (.packageName package-item)})) | ||||||||||||||||||||||||
parse ComponentInfo | (defn parse-component-info [^ComponentInfo component] (merge {} (parse-package-item-info component) {:application-info (parse-application-info (.applicationInfo component)) :description (load-res-string component (.descriptionRes component) nil) :description-res (.descriptionRes component) :enabled (.enabled component) :exported (.exported component) :process-name (.processName component)})) | ||||||||||||||||||||||||
parse ActivityInfo | (defn parse-activity-info [^ActivityInfo activity] (merge {} (parse-component-info activity) {:config-changes (#(let [flags (atom #{})] (when (bit-and % ActivityInfo/CONFIG_DENSITY) (swap! flags conj :density)) (when (bit-and % ActivityInfo/CONFIG_FONT_SCALE) (swap! flags conj :font-scal)) (when (bit-and % ActivityInfo/CONFIG_KEYBOARD) (swap! flags conj :keyboard)) (when (bit-and % ActivityInfo/CONFIG_KEYBOARD_HIDDEN) (swap! flags conj :keyboard-hidden)) (when (bit-and % ActivityInfo/CONFIG_LAYOUT_DIRECTION) (swap! flags conj :layout-direction)) (when (bit-and % ActivityInfo/CONFIG_LOCALE) (swap! flags conj :locale)) (when (bit-and % ActivityInfo/CONFIG_MCC) (swap! flags conj :mcc)) (when (bit-and % ActivityInfo/CONFIG_MNC) (swap! flags conj :mnc)) (when (bit-and % ActivityInfo/CONFIG_NAVIGATION) (swap! flags conj :navigation)) (when (bit-and % ActivityInfo/CONFIG_ORIENTATION) (swap! flags conj :orientation)) (when (bit-and % ActivityInfo/CONFIG_SCREEN_LAYOUT) (swap! flags conj :screen-layout)) (when (bit-and % ActivityInfo/CONFIG_SCREEN_SIZE) (swap! flags conj :screen-size)) (when (bit-and % ActivityInfo/CONFIG_SMALLEST_SCREEN_SIZE) (swap! flags conj :smallest-screen-size)) (when (bit-and % ActivityInfo/CONFIG_TOUCHSCREEN) (swap! flags conj :touchscreen)) (when (bit-and % ActivityInfo/CONFIG_UI_MODE) (swap! flags conj :ui-mode)) @flags) (.configChanges activity)) :flags (#(let [flags (atom #{})] (when (bit-and % ActivityInfo/FLAG_ALLOW_TASK_REPARENTING) (swap! flags conj :allow-task-reparenting)) (when (bit-and % ActivityInfo/FLAG_ALWAYS_RETAIN_TASK_STATE) (swap! flags conj :always-retain-task-state)) (when (bit-and % ActivityInfo/FLAG_CLEAR_TASK_ON_LAUNCH) (swap! flags conj :clear-task-on-launch)) (when (bit-and % ActivityInfo/FLAG_EXCLUDE_FROM_RECENTS) (swap! flags conj :exclude-from-recents)) (when (bit-and % ActivityInfo/FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS) (swap! flags conj :finish-on-close-system-dialogs)) (when (bit-and % ActivityInfo/FLAG_FINISH_ON_TASK_LAUNCH) (swap! flags conj :finish-on-task-launch)) (when (bit-and % ActivityInfo/FLAG_HARDWARE_ACCELERATED) (swap! flags conj :hardware-accelerated)) (when (bit-and % ActivityInfo/FLAG_IMMERSIVE) (swap! flags conj :immersive)) (when (bit-and % ActivityInfo/FLAG_MULTIPROCESS) (swap! flags conj :multiprocess)) (when (bit-and % ActivityInfo/FLAG_NO_HISTORY) (swap! flags conj :no-history)) (when (bit-and % ActivityInfo/FLAG_PRIMARY_USER_ONLY) (swap! flags conj :primary-user-only)) (when (bit-and % ActivityInfo/FLAG_SHOW_ON_LOCK_SCREEN) (swap! flags conj :show-on-lock-screen)) (when (bit-and % ActivityInfo/FLAG_SINGLE_USER) (swap! flags conj :single-user)) (when (bit-and % ActivityInfo/FLAG_STATE_NOT_NEEDED) (swap! flags conj :state-not-needed)) @flags) (.flags activity)) :launch-mode (#(case % ActivityInfo/LAUNCH_MULTIPLE :multiple ActivityInfo/LAUNCH_SINGLE_INSTANCE :single-instance ActivityInfo/LAUNCH_SINGLE_TASK :single-task ActivityInfo/LAUNCH_SINGLE_TOP :single-top %) (.launchMode activity)) :parent-activity-name (.parentActivityName activity) :permission (.permission activity) :screen-orientation (#(case % ActivityInfo/SCREEN_ORIENTATION_BEHIND :behind ActivityInfo/SCREEN_ORIENTATION_FULL_SENSOR :full-sensor ActivityInfo/SCREEN_ORIENTATION_FULL_USER :full-user ActivityInfo/SCREEN_ORIENTATION_LANDSCAPE :landscape ActivityInfo/SCREEN_ORIENTATION_LOCKED :locked ActivityInfo/SCREEN_ORIENTATION_NOSENSOR :nosensor ActivityInfo/SCREEN_ORIENTATION_PORTRAIT :portrait ActivityInfo/SCREEN_ORIENTATION_REVERSE_LANDSCAPE :reverse-landscape ActivityInfo/SCREEN_ORIENTATION_REVERSE_PORTRAIT :reverse-potrait ActivityInfo/SCREEN_ORIENTATION_SENSOR :sensor ActivityInfo/SCREEN_ORIENTATION_SENSOR_LANDSCAPE :sensor-landscape ActivityInfo/SCREEN_ORIENTATION_SENSOR_PORTRAIT :sensor-portrait ActivityInfo/SCREEN_ORIENTATION_UNSPECIFIED :unspecified ActivityInfo/SCREEN_ORIENTATION_USER :user ActivityInfo/SCREEN_ORIENTATION_USER_LANDSCAPE :user-landscape ActivityInfo/SCREEN_ORIENTATION_USER_PORTRAIT :user-portrait %) (.screenOrientation activity)) :soft-input-mode (.softInputMode activity) :target-activity (.targetActivity activity) :task-affinity (.taskAffinity activity) :theme (load-res-resource-name activity (.theme activity)) :ui-options (#(case % ActivityInfo/UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW :split-action-bar-when-narrow %) (.uiOptions activity))})) | ||||||||||||||||||||||||
parse ApplicationInfo | (defn parse-application-info [^ApplicationInfo application] (merge {} (parse-package-item-info application) {:backup-agent-name (.backupAgentName application) :class-name (.className application) :compatible-width-limit-dp (.compatibleWidthLimitDp application) :data-dir (.dataDir application) :description (load-res-string application (.descriptionRes application) nil) :description-res (.descriptionRes application) :enabled (.enabled application) :enabled-setting (.enabledSetting application) :flags (#(let [flags (atom #{})] (when (bit-and % ApplicationInfo/FLAG_ALLOW_BACKUP) (swap! flags conj :allow-backup)) (when (bit-and % ApplicationInfo/FLAG_ALLOW_CLEAR_USER_DATA) (swap! flags conj :allow-clear-user-data)) (when (bit-and % ApplicationInfo/FLAG_ALLOW_TASK_REPARENTING) (swap! flags conj :allow-task-reparenting)) (when (bit-and % ApplicationInfo/FLAG_CANT_SAVE_STATE) (swap! flags conj :cant-save-state)) (when (bit-and % ApplicationInfo/FLAG_DEBUGGABLE) (swap! flags conj :debuggable)) (when (bit-and % ApplicationInfo/FLAG_EXTERNAL_STORAGE) (swap! flags conj :external-storage)) (when (bit-and % ApplicationInfo/FLAG_FACTORY_TEST) (swap! flags conj :factory-test)) (when (bit-and % ApplicationInfo/FLAG_FORWARD_LOCK) (swap! flags conj :forward-lock)) (when (bit-and % ApplicationInfo/FLAG_HAS_CODE) (swap! flags conj :has-code)) (when (bit-and % ApplicationInfo/FLAG_INSTALLED) (swap! flags conj :installed)) (when (bit-and % ApplicationInfo/FLAG_IS_DATA_ONLY) (swap! flags conj :is-data-only)) (when (bit-and % ApplicationInfo/FLAG_KILL_AFTER_RESTORE) (swap! flags conj :kill-after-restore)) (when (bit-and % ApplicationInfo/FLAG_LARGE_HEAP) (swap! flags conj :large-heap)) (when (bit-and % ApplicationInfo/FLAG_PERSISTENT) (swap! flags conj :persistent)) (when (bit-and % ApplicationInfo/FLAG_RESIZEABLE_FOR_SCREENS) (swap! flags conj :resizeable-for-screens)) (when (bit-and % ApplicationInfo/FLAG_RESTORE_ANY_VERSION) (swap! flags conj :restore-any-version)) (when (bit-and % ApplicationInfo/FLAG_STOPPED) (swap! flags conj :stopped)) (when (bit-and % ApplicationInfo/FLAG_SUPPORTS_LARGE_SCREENS) (swap! flags conj :supports-large-screens)) (when (bit-and % ApplicationInfo/FLAG_SUPPORTS_NORMAL_SCREENS) (swap! flags conj :supports-normal-screens)) (when (bit-and % ApplicationInfo/FLAG_SUPPORTS_RTL) (swap! flags conj :supports-rtl)) (when (bit-and % ApplicationInfo/FLAG_SUPPORTS_SCREEN_DENSITIES) (swap! flags conj :supports-screen-densities)) (when (bit-and % ApplicationInfo/FLAG_SUPPORTS_SMALL_SCREENS) (swap! flags conj :supports-small-screen)) (when (bit-and % ApplicationInfo/FLAG_SUPPORTS_XLARGE_SCREENS) (swap! flags conj :supports-xlarge-screen)) (when (bit-and % ApplicationInfo/FLAG_SYSTEM) (swap! flags conj :system)) (when (bit-and % ApplicationInfo/FLAG_TEST_ONLY) (swap! flags conj :test-only)) (when (bit-and % ApplicationInfo/FLAG_UPDATED_SYSTEM_APP) (swap! flags conj :updated-system-app)) (when (bit-and % ApplicationInfo/FLAG_VM_SAFE_MODE) (swap! flags conj :vm-safe-mode)) @flags) (.flags application)) :install-location (.installLocation application) :largest-width-limit-dp (.largestWidthLimitDp application) :manage-space-activity-name (.manageSpaceActivityName application) :native-library-dir (.nativeLibraryDir application) :permission (.permission application) :process-name (.processName application) :public-source-dir (.publicSourceDir application) :requires-smallest-width-dp (.requiresSmallestWidthDp application) :resource-dirs (set (.resourceDirs application)) :seinfo (.seinfo application) :shared-library-files (set (.sharedLibraryFiles application)) :source-dir (.sourceDir application) :target-sdk-version (.targetSdkVersion application) :task-affinity (.taskAffinity application) :theme (load-res-resource-name application (.theme application)) :uid (.uid application) :ui-options (.uiOptions application)})) | ||||||||||||||||||||||||
parse ConfigurationInfo | (defn parse-configuration-info [^ConfigurationInfo configuration] (merge {} {:req-gles-version (#(case % ConfigurationInfo/GL_ES_VERSION_UNDEFINED :undefined %) (.reqGlEsVersion configuration)) :req-input-features (#(let [flags (atom #{})] (when (bit-and % ConfigurationInfo/INPUT_FEATURE_FIVE_WAY_NAV) (swap! flags conj :five-way-nav)) (when (bit-and % ConfigurationInfo/INPUT_FEATURE_HARD_KEYBOARD) (swap! flags conj :hard-keyboard)) @flags) (.reqInputFeatures configuration)) :req-keyboard-type (.reqKeyboardType configuration) :req-navigation (.reqNavigation configuration) :req-touch-screen (.reqTouchScreen configuration)})) | ||||||||||||||||||||||||
parse InstrumentationInfo | (defn parse-instrumentation-info [^InstrumentationInfo instrumentation] (merge {} (parse-package-item-info instrumentation) {:data-dir (.dataDir instrumentation) :functional-test (.functionalTest instrumentation) :handle-profiling (.handleProfiling instrumentation) :native-library-dir (.nativeLibraryDir instrumentation) :public-source-dir (.publicSourceDir instrumentation) :source-dir (.sourceDir instrumentation) :target-package (.targetPackage instrumentation)})) | ||||||||||||||||||||||||
parse PermissionGroupInfo | (defn parse-permission-group-info [^PermissionGroupInfo permission-group] (merge {} (parse-package-item-info permission-group) {:description (load-res-string permission-group (.descriptionRes permission-group) (.nonLocalizedDescription permission-group)) :description-res (.descriptionRes permission-group) :flags (#(let [flags (atom #{})] (swap! flags conj (case % PermissionGroupInfo/FLAG_PERSONAL_INFO :personal-info %)) @flags) (.flags permission-group)) :non-localized-description (.nonLocalizedDescription permission-group) :priority (.priority permission-group)})) | ||||||||||||||||||||||||
parse PermissionInfo | (defn parse-permission-info [^PermissionInfo permission] (merge {} (parse-package-item-info permission) {:description (load-res-string permission (.descriptionRes permission) (.nonLocalizedDescription permission)) :description-res (.descriptionRes permission) :flags (#(let [flags (atom #{})] (when (bit-and % PermissionInfo/FLAG_COSTS_MONEY) (swap! flags conj :costs-money)) @flags) (.flags permission)) :group (.group permission) :non-localized-description (str (.nonLocalizedDescription permission)) :protection-level (#(let [flags (atom #{}) protection-basic (bit-and % PermissionInfo/PROTECTION_MASK_BASE) protection-flags (bit-and % PermissionInfo/PROTECTION_MASK_FLAGS)] (swap! flags conj (case protection-basic PermissionInfo/PROTECTION_DANGEROUS :dangerous PermissionInfo/PROTECTION_NORMAL :normal PermissionInfo/PROTECTION_SIGNATURE :signature PermissionInfo/PROTECTION_SIGNATURE_OR_SYSTEM :signature-or-system :unknown-protection-basic-level)) (when (bit-and protection-flags PermissionInfo/PROTECTION_FLAG_DEVELOPMENT) (swap! flags conj :development)) (when (bit-and protection-flags PermissionInfo/PROTECTION_FLAG_SYSTEM) (swap! flags conj :system)) @flags) (.protectionLevel permission))})) | ||||||||||||||||||||||||
parse ProviderInfo | (defn parse-provider-info [^ProviderInfo provider] (merge {} (parse-component-info provider) {:authority (.authority provider) :flags (#(let [flags (atom #{})] (when (bit-and % ProviderInfo/FLAG_SINGLE_USER) (swap! flags conj :single-user)) @flags) (.flags provider)) :grant-uri-permissions (.grantUriPermissions provider) :init-order (.initOrder provider) :multiprocess (.multiprocess provider) :path-permissions (set (.pathPermissions provider)) :read-permission (.readPermission provider) :uri-permission-patterns (set (.uriPermissionPatterns provider)) :write-permission (.writePermission provider)})) | ||||||||||||||||||||||||
parse FeatureInfo | (defn parse-feature-info [^FeatureInfo feature] (merge {} {:flags (set (#(let [flags (atom #{})] (when (bit-and % FeatureInfo/FLAG_REQUIRED) (swap! flags conj :required)) @flags) (.flags feature))) :name (.name feature) :req-gles-version (#(case % FeatureInfo/GL_ES_VERSION_UNDEFINED :undefined %) (.reqGlEsVersion feature))})) | ||||||||||||||||||||||||
parse ServiceInfo | (defn parse-service-info [^ServiceInfo service] (merge {} (parse-component-info service) {:flags (#(let [flags (atom #{})] (when (bit-and % ServiceInfo/FLAG_ISOLATED_PROCESS) (swap! flags conj :isolated-process)) (when (bit-and % ServiceInfo/FLAG_SINGLE_USER) (swap! flags conj :single-user)) (when (bit-and % ServiceInfo/FLAG_STOP_WITH_TASK) (swap! flags conj :stop-with-task))) (.flags service)) :permission (.permission service)})) | ||||||||||||||||||||||||
manager users | (ns figurehead.api.os.user-manager.user-manager (:require (core [state :as state :refer [defcommand]])) (:require (figurehead.util [services :as services :refer [get-service]])) (:require [figurehead.api.os.user-manager.user-manager-parser :as parser]) (:require [clojure.string :as str] [clojure.java.io :as io] [clojure.set :as set :refer [subset?]]) (:import (android.content.pm UserInfo) (android.os IUserManager UserHandle UserManager))) | ||||||||||||||||||||||||
(declare create-user remove-user wipe-user set-user-name find-users find-user get-user-handle list-users get-max-users set-guest-enabled enable-guest disable-guest) | |||||||||||||||||||||||||
create a user | (defcommand create-user [{:keys [name] :as args}] (when name (let [^IUserManager user-manager (get-service :user-manager)] (let [name (str name) ^UserInfo info (.createUser user-manager name 0)] (when info (parser/parse-user-info info)))))) | ||||||||||||||||||||||||
remove a user | (defcommand remove-user [{:keys [name serial-number id handle] :as args}] (when (or name serial-number id handle) (let [^IUserManager user-manager (get-service :user-manager) handle (get-user-handle args)] (when handle (.removeUser user-manager handle))))) | ||||||||||||||||||||||||
wipe a user | (defcommand wipe-user [{:keys [name serial-number id handle] :as args}] (when (or name serial-number id handle) (let [^IUserManager user-manager (get-service :user-manager) handle (get-user-handle args)] (when handle (.wipeUser user-manager handle))))) | ||||||||||||||||||||||||
set user to a new name | (defcommand set-user-name [{:keys [name serial-number id handle new-name] :as args}] (when (and new-name (or name serial-number id handle)) (let [^IUserManager user-manager (get-service :user-manager) handle (get-user-handle args)] (when handle (.setUserName user-manager handle new-name))))) | ||||||||||||||||||||||||
find users by name, serial-number, id, or flags | (defcommand find-users [{:keys [name serial-number id flags]}] (when (or name serial-number id flags) (let [users (list-users {})] (cond name (filter #(= name (:name %)) users) serial-number (filter #(= serial-number (:serial-number %)) users) id (filter #(= id (:id %)) users) flags (filter #(subset? flags (:flags %)) users))))) | ||||||||||||||||||||||||
find a user by name, serial-number, or id | (defcommand find-user [{:keys [name serial-number id] :as args}] (when (or name serial-number id) (first (find-users args)))) | ||||||||||||||||||||||||
get user handle | (defcommand get-user-handle [{:keys [name serial-number id handle] :as args}] (cond handle handle (or name serial-number id) (let [^IUserManager user-manager (get-service :user-manager) user (find-user (merge {} (when name {:name name}) (when serial-number {:serial-number serial-number}) (when id {:id id})))] (when user (.getUserHandle user-manager (:serial-number user)))))) | ||||||||||||||||||||||||
list all users | (defcommand list-users [{:keys [] :as args}] (let [^IUserManager user-manager (get-service :user-manager)] (into #{} (map parser/parse-user-info (.getUsers user-manager false))))) | ||||||||||||||||||||||||
get max number of supported users | (defcommand get-max-users [{:keys [] :as args}] (UserManager/getMaxSupportedUsers)) | ||||||||||||||||||||||||
set guest enable flag | (defcommand set-guest-enabled [{:keys [enabled?] :as args}] (let [^IUserManager user-manager (get-service :user-manager)] (.setGuestEnabled user-manager enabled?))) | ||||||||||||||||||||||||
enable guest | (defcommand enable-guest [{:keys [] :as args}] (set-guest-enabled {:enabled? true})) | ||||||||||||||||||||||||
disable guest | (defcommand disable-guest [{:keys [] :as args}] (set-guest-enabled {:enabled? false})) | ||||||||||||||||||||||||
parse User Manager objects into Clojure data structures | (ns figurehead.api.os.user-manager.user-manager-parser (:require (figurehead.util [services :as services :refer [get-service]])) (import (android.content.pm UserInfo))) | ||||||||||||||||||||||||
(declare parse-user-info) | |||||||||||||||||||||||||
parse UserInfo | (defn parse-user-info [^UserInfo user-info] (merge {} {:creation-time (.creationTime user-info) :flags (#(let [flags (atom #{}) user-type (bit-and % UserInfo/FLAG_MASK_USER_TYPE)] (when (bit-and user-type UserInfo/FLAG_ADMIN) (swap! flags conj :admin)) (when (bit-and user-type UserInfo/FLAG_GUEST) (swap! flags conj :guest)) (when (bit-and user-type UserInfo/FLAG_INITIALIZED) (swap! flags conj :initialized)) (when (bit-and user-type UserInfo/FLAG_PRIMARY) (swap! flags conj :primary)) (when (bit-and user-type UserInfo/FLAG_RESTRICTED) (swap! flags conj :restricted)) @flags) (.flags user-info)) :icon-path ^String (.iconPath user-info) :id (.id user-info) :last-logged-in-time (.lastLoggedInTime user-info) :name ^String (.name user-info) :partial (.partial user-info) :serial-number (.serialNumber user-info)})) | ||||||||||||||||||||||||
Android OS misc utils | (ns figurehead.api.os.util (:require (core [state :as state :refer [defcommand]])) (:require (figurehead.util [services :as services :refer [get-service]])) (:require [clojure.string :as str] [clojure.java.io :as io] [clojure.set :as set :refer [subset?]]) (:import (android.os SystemProperties))) | ||||||||||||||||||||||||
(declare get-system-property set-system-property get-my-pid kill-process test-process) | |||||||||||||||||||||||||
get system property by name | (defcommand get-system-property [{:keys [name def int? long? boolean?] :as args}] {:pre [name]} (when-let [name (str name)] (cond int? (SystemProperties/getInt name (if def def -1)) long? (SystemProperties/getLong name (if def def -1)) boolean? (SystemProperties/getBoolean name (if def def false)) :else (SystemProperties/get name (if def def ""))))) | ||||||||||||||||||||||||
set system property by name to value | (defcommand set-system-property [{:keys [name value] :as args}] {:pre [name value]} (let [name (str name) value (str value)] (when (and name value) (SystemProperties/set name value)))) | ||||||||||||||||||||||||
get my process id (pid) | (defcommand get-my-pid [{:keys [] :as args}] {:pre []} (android.os.Process/myPid)) | ||||||||||||||||||||||||
kill process by pid | (defcommand kill-process [{:keys [pid] :as args}] {:pre [pid]} (when pid (android.os.Process/killProcess pid))) | ||||||||||||||||||||||||
test whether process is running by pid | (defcommand test-process [{:keys [pid] :as args}] {:pre [pid]} (when pid (let [ppid (android.os.Process/getParentPid pid)] (if (> ppid 0) true false)))) | ||||||||||||||||||||||||
file utils | (ns figurehead.api.util.file (:require (core [state :as state :refer [defcommand]])) (:require (figurehead.util [services :as services :refer [get-service]])) (:require [clojure.string :as str] [clojure.java.io :as io]) (:import (android.content.res AssetManager Resources) (android.util Base64) (java.io File) (org.apache.commons.io FileUtils))) | ||||||||||||||||||||||||
(declare decode-content-to-bytes encode-content-from-bytes decode-content-to-string encode-content-from-string decode-bytes-to-string encode-bytes-from-string write-file read-file) | |||||||||||||||||||||||||
decode from Base64-encoded content to bytes | (defcommand decode-content-to-bytes [{:keys [^String content] :as args}] {:pre [content]} (when (and content) ^bytes (Base64/decode content Base64/DEFAULT))) | ||||||||||||||||||||||||
encode to Base64-encoded content from bytes | (defcommand encode-content-from-bytes [{:keys [^bytes bytes] :as args}] {:pre [bytes]} (when (and bytes) (Base64/encodeToString bytes (bit-or Base64/NO_WRAP 0)))) | ||||||||||||||||||||||||
decode from Base64-encoded content to string | (defcommand decode-content-to-string [{:keys [^String content] :as args}] {:pre [content]} (when (and content) (decode-bytes-to-string {:bytes (decode-content-to-bytes {:content content})}))) | ||||||||||||||||||||||||
encode to Base64-encoded content from string | (defcommand encode-content-from-string [{:keys [^String string] :as args}] {:pre [string]} (when (and string) (encode-content-from-bytes {:bytes (encode-bytes-from-string {:string string})}))) | ||||||||||||||||||||||||
decode from bytes to UTF-8 string | (defcommand decode-bytes-to-string [{:keys [^bytes bytes] :as args}] {:pre [bytes]} (when (and bytes) (String. bytes "UTF-8"))) | ||||||||||||||||||||||||
encode to bytes from UTF-8 string | (defcommand encode-bytes-from-string [{:keys [^String string] :as args}] {:pre [string]} (when (and string) (.getBytes string "UTF-8"))) | ||||||||||||||||||||||||
write to file from string, Base64-encoded content, or bytes | (defcommand write-file [{:keys [file ^String string ^String content ^bytes bytes] :as args}] {:pre [file (or content string bytes)]} (when (and file (or content string bytes)) (with-open [the-file (io/output-stream (io/file file))] (let [bytes (cond string (encode-bytes-from-string string) content (decode-content-to-bytes content) bytes bytes)] (.write the-file ^bytes bytes))))) | ||||||||||||||||||||||||
read from file to string, Base64-encoded content, or bytes | (defcommand read-file [{:keys [file string? content? bytes?] :as args}] {:pre [file (or string? content? bytes?)]} (when (and file (or string? content? bytes?)) (let [^bytes bytes (FileUtils/readFileToByteArray (io/file file))] (cond string? (decode-bytes-to-string {:bytes bytes}) content? (encode-content-from-bytes {:bytes bytes}) bytes? bytes)))) | ||||||||||||||||||||||||
input (Input Events) wrapper https://github.com/android/platformframeworksbase/blob/android-4.3_r3.1/cmds/input/src/com/android/commands/input/Input.java | (ns figurehead.api.view.input (:require (core [state :as state :refer [defcommand]])) (:require (figurehead.util [services :as services :refer [get-service]])) (:require [clojure.string :as str] [clojure.java.io :as io]) (:import (android.hardware.input InputManager) (android.os SystemClock) (android.view InputDevice KeyCharacterMap KeyEvent MotionEvent))) | ||||||||||||||||||||||||
(declare ;; porcelain text key-event tap swipe touchscreen touchpad touch-navigation trackball ;; plumbing send-text send-key-event send-tap send-swipe send-move inject-key-event inject-motion-event) | |||||||||||||||||||||||||
porcelain | |||||||||||||||||||||||||
(defcommand text [{:keys [^String text] :as args}] (when text (send-text (merge args {:text text})))) | |||||||||||||||||||||||||
(defcommand key-event [{:keys [^String key-code meta-state] :or {meta-state 0} :as args}] (let [str-to-key-code (fn [str-key-code prefix] (let [key-code (KeyEvent/keyCodeFromString str-key-code)] (if (= key-code KeyEvent/KEYCODE_UNKNOWN) (KeyEvent/keyCodeFromString (str prefix (str/upper-case str-key-code))) key-code)))] (let [key-code (cond (number? key-code) key-code (string? key-code) (str-to-key-code key-code "KEYCODE_")) meta-state (cond (number? meta-state) meta-state (string? meta-state) (str-to-key-code meta-state "META_") (sequential? meta-state) (reduce bit-or 0 (map #(str-to-key-code % "META_") meta-state)))] (when (and key-code meta-state) (send-key-event (merge args {:key-code key-code :meta-state meta-state})))))) | |||||||||||||||||||||||||
(defcommand tap [{:keys [x y] :as args}] (when (and x y) (send-tap (merge args {:input-source InputDevice/SOURCE_TOUCHSCREEN :x x :y y})))) | |||||||||||||||||||||||||
(defcommand swipe [{:keys [x1 y1 x2 y2] :as args}] (when (and x1 y1 x2 y2) (send-swipe (merge args {:input-source InputDevice/SOURCE_TOUCHSCREEN :x1 x1 :y1 y1 :x2 x2 :y2 y2 :duration -1})))) | |||||||||||||||||||||||||
(let [common-procedure (fn [input-source action args] (case action :tap (do (let [{:keys [x y]} args] (when (and x y) (send-tap (merge args {:input-source input-source :x x :y y}))))) :swipe (do (let [{:keys [x1 y1 x2 y2 duration]} args] (when (and x1 y1 x2 y2) (send-swipe (merge args {:input-source input-source :x1 x1 :y1 y1 :x2 x2 :y2 y2 :duration (if duration duration -1)}))))) :else))] (defcommand touchscreen [{:keys [action] :as args}] (common-procedure InputDevice/SOURCE_TOUCHSCREEN (or action :tap) args)) (defcommand touchpad [{:keys [action] :as args}] (common-procedure InputDevice/SOURCE_TOUCHPAD (or action :tap) args)) (defcommand touch-navigation [{:keys [action] :as args}] (common-procedure InputDevice/SOURCE_TOUCH_NAVIGATION (or action :tap) args))) | |||||||||||||||||||||||||
(defcommand trackball [{:keys [action] :as args}] (let [input-source InputDevice/SOURCE_TRACKBALL] (case action :press (do (send-tap (merge args {:input-source input-source :x 0.0 :y 0.0}))) :roll (do (let [{:keys [dx dy]} args] (when (and dx dy) (send-move (merge args {:input-source input-source :dx dx :dy dy}))))) :else))) | |||||||||||||||||||||||||
plumbing | |||||||||||||||||||||||||
convert the characters of string text into key events and send to the device | (defcommand send-text [{:keys [^String text] :as args}] (let [kcm ^KeyCharacterMap (KeyCharacterMap/load KeyCharacterMap/VIRTUAL_KEYBOARD)] (doseq [^KeyEvent event (.getEvents kcm (.toCharArray text))] (inject-key-event {:event event})))) | ||||||||||||||||||||||||
send key event | (defcommand send-key-event [{:keys [key-code meta-state] :or {meta-state 0} :as args}] (let [now (SystemClock/uptimeMillis) ] (inject-key-event {:event (KeyEvent. now now KeyEvent/ACTION_DOWN key-code 0 meta-state KeyCharacterMap/VIRTUAL_KEYBOARD 0 0 InputDevice/SOURCE_KEYBOARD)}) (inject-key-event {:event (KeyEvent. now now KeyEvent/ACTION_UP key-code 0 meta-state KeyCharacterMap/VIRTUAL_KEYBOARD 0 0 InputDevice/SOURCE_KEYBOARD)}))) | ||||||||||||||||||||||||
send tap event | (defcommand send-tap [{:keys [input-source x y] :or {input-source InputDevice/SOURCE_TOUCHSCREEN} :as args}] (let [now (SystemClock/uptimeMillis)] (inject-motion-event {:input-source input-source :action MotionEvent/ACTION_DOWN :when now :x x :y y :pressure 1.0}) (inject-motion-event {:input-source input-source :action MotionEvent/ACTION_UP :when now :x x :y y :pressure 0.0}))) | ||||||||||||||||||||||||
send swipe event | (defcommand send-swipe [{:keys [input-source x1 y1 x2 y2 duration] :or {input-source InputDevice/SOURCE_TOUCHSCREEN} :as args}] (let [now (SystemClock/uptimeMillis) duration (if (>= duration 0) duration 300) start-time now end-time (+ now duration) lerp (fn [x y alpha] (+ x (* alpha (- y x))))] (inject-motion-event {:input-source input-source :action MotionEvent/ACTION_DOWN :when now :x x1 :y y1 :pressure 1.0}) (loop [now now] (if (< now end-time) (do (let [elapsed-time (- now start-time) alpha (/ (float elapsed-time) duration)] (inject-motion-event {:input-source input-source :action MotionEvent/ACTION_MOVE :when now :x (lerp x1 x2 alpha) :y (lerp y1 y2 alpha) :pressure 1.0})) (recur (SystemClock/uptimeMillis))) (inject-motion-event {:input-source input-source :action MotionEvent/ACTION_UP :when now :x x2 :y y2 :pressure 0.0}))))) | ||||||||||||||||||||||||
send a zero-pressure move event | (defcommand send-move [{:keys [input-source dx dy] :or {input-source InputDevice/SOURCE_TOUCHSCREEN} :as args}] (let [now (SystemClock/uptimeMillis)] (inject-motion-event {:input-source input-source :action MotionEvent/ACTION_MOVE :when now :x dx :y dy :pressure 0.0}))) | ||||||||||||||||||||||||
inject a key event | (defcommand inject-key-event [{:keys [^KeyEvent event] :as args}] (.. (InputManager/getInstance) (injectInputEvent event InputManager/INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH))) | ||||||||||||||||||||||||
inject a motion event | (defcommand inject-motion-event [{:keys [input-source action when meta-state x y pressure] :or {input-source InputDevice/SOURCE_TOUCHSCREEN meta-state 0} :as args}] (let [size 1.0 precision-x 1.0 precision-y 1.0 device-id 0 edge-flags 0 event (MotionEvent/obtain when when action x y pressure size meta-state precision-x precision-y device-id edge-flags)] (.setSource event input-source) (.. (InputManager/getInstance) (injectInputEvent event InputManager/INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH)))) | ||||||||||||||||||||||||
main entry into Figurehead | (ns figurehead.main (:require (figurehead.util [init :as init])) (:require (core main state init)) ;; these "require" are needed to handle lein-droid's :aot "removing unused namespace from classpath" feature (:require core.plugin.echo.main core.plugin.command-executor.main figurehead.plugin.unique-instance.main figurehead.plugin.nrepl.main figurehead.plugin.monitor.main figurehead.plugin.mastermind.main) (:import (android.os SystemProperties)) (:gen-class)) | ||||||||||||||||||||||||
the main entry | (defn -main [& args] (init/init) (core.init/require-and-set-default-plugins core.plugin.echo core.plugin.command-executor figurehead.plugin.unique-instance figurehead.plugin.nrepl figurehead.plugin.monitor figurehead.plugin.mastermind) (apply core.main/main args)) | ||||||||||||||||||||||||
connect to Mastermind | (ns figurehead.plugin.mastermind.main (:require (core [init :as init] [state :as state] [bus :as bus] [plugin :as plugin])) (:require (figurehead.util [unique-instance :refer [set-meta-data-entry register-meta-data-entry]])) (:require [clojure.string :as str] [clojure.java.io :as io] [clojure.stacktrace :refer [print-stack-trace]] [clojure.pprint :refer [pprint]] [clojure.core.async :as async :refer [thread chan <!! >!!]]) (:import (java.net Socket SocketTimeoutException))) | ||||||||||||||||||||||||
(def defaults (atom { :stop-unblock-tag :stop-figurehead.plugin.mastermind :mastermind-port 4321 :socket-timeout 15000 :writer-buffer 1000 })) | |||||||||||||||||||||||||
(defn populate-parse-opts-vector [current-parse-opts-vector] (init/add-to-parse-opts-vector [ ["-a" "--mastermind-address ADDR" "mastermind address"] (let [option :mastermind-port default (option @defaults)] ["-p" (str "--" (name option) " [PORT]") (str "mastermind port") :default default :parse-fn (get-in @init/parse-opts-vector-helper [:parse-fn :inet-port])]) ])) | |||||||||||||||||||||||||
(defn init [options] (register-meta-data-entry :mastermind-address) (register-meta-data-entry :mastermind-port) (when (and (:mastermind-address options) (:mastermind-port options)) true)) | |||||||||||||||||||||||||
(defn run [options] (let [verbose (:verbose options) mastermind-address (:mastermind-address options) mastermind-port (:mastermind-port options) instance-id (state/get-state :instance-id)] (set-meta-data-entry :mastermind-address mastermind-address) (set-meta-data-entry :mastermind-port mastermind-port) (let [sock (Socket. ^String mastermind-address ^int mastermind-port)] (plugin/blocking-jail [ ;; timeout nil ;; unblock-tag (:stop-unblock-tag @defaults) ;; finalization (do (.close sock)) ;; verbose verbose ] (.setSoTimeout sock (:socket-timeout @defaults)) ;; reader thread (thread (with-open [^java.io.BufferedReader reader (io/reader sock)] (plugin/looping-jail [ ;; stop condition (plugin/get-state-entry :stop) ;; finalization (do (.close sock)) ;; verbose verbose ] (try (when-let [line (.readLine reader)] (try (let [message (read-string line) topic (bus/get-message-topic message) content (bus/remove-message-topic message)] (when verbose (pprint [:mastermind :reader message])) (case topic :command (do (bus/say!! :command content)) :else)) (catch RuntimeException e (when verbose (print-stack-trace e))))) (catch SocketTimeoutException e (when verbose (print-stack-trace e))))))) ;; writer thread (thread (with-open [^java.io.BufferedWriter writer (io/writer sock)] (let [ch (chan (:writer-buffer @defaults))] (bus/register-listener ch) (plugin/looping-jail [ ;; stop condition (plugin/get-state-entry :stop) ;; finalization (do (bus/unregister-listener ch) (.close sock)) ;; verbose verbose ] (let [message (<!! ch) topic (bus/get-message-topic message) content (bus/remove-message-topic message)] (cond ;; do NOT echo these topics back (not (contains? #{:command} topic)) (let [message (bus/build-message topic (cond (map? content) (merge content {:instance instance-id}) :else {:instance instance-id :content message}))] (when verbose (pprint [:mastermind :writer message])) (.write writer (prn-str message)) (.flush writer)))))))))))) | |||||||||||||||||||||||||
(defn stop [] (plugin/set-state-entry :figurehead.plugin.mastermind :stop true) (plugin/unblock-thread (:stop-unblock-tag @defaults))) | |||||||||||||||||||||||||
the config map | (def config-map {:populate-parse-opts-vector populate-parse-opts-vector :init init :run run :stop stop :param {:priority 90 :auto-restart true}}) | ||||||||||||||||||||||||
monitor Activities | (ns figurehead.plugin.monitor.main (:require (core [init :as init] [state :as state] [bus :as bus] [plugin :as plugin])) (:require (figurehead.util [services :as services :refer [get-service]])) (:require (figurehead.api.app [activity-controller :as activity-controller])) (:require (figurehead.util [unique-instance :refer [set-meta-data-entry register-meta-data-entry]])) (:require [clojure.string :as str] [clojure.core.async :as async]) (:import (android.text.format Time) (android.app IActivityManager IActivityController$Stub ActivityManager$RunningAppProcessInfo) (android.content Intent) (android.content.pm IPackageManager PackageManager ActivityInfo ServiceInfo ProviderInfo))) | ||||||||||||||||||||||||
(def defaults (atom { :stop-unblock-tag :stop-figurehead.plugin.monitor })) | |||||||||||||||||||||||||
(defn populate-parse-opts-vector [current-parse-opts-vector] (init/add-to-parse-opts-vector [ ["-m" "--monitor" "enter monitor mode"] ])) | |||||||||||||||||||||||||
(defn init [options] (register-meta-data-entry :monitor) (when (:monitor options) true)) | |||||||||||||||||||||||||
(defn run [options] (let [verbose (:verbose options) now (Time.) activity-manager ^IActivityManager (get-service :activity-manager) activity-starting (fn [^Intent intent package] (locking intent (bus/say!! :activity-controller {:event :starting :timestamp (do (.setToNow now) (.toMillis now true)) :package (-> package keyword) :intent-action (-> intent .getAction keyword) ;; the "/" prevents straightforward keyword-ize :intent-component (str (.. intent getComponent getPackageName) "/" (.. intent getComponent getShortClassName)) :intent-category (into #{} (map keyword (.getCategories intent))) ;; data and extras may contain non-keyword-izable content :intent-data (-> intent .getDataString) :intent-extras (-> intent .getExtras) :intent-flags (-> intent .getFlags) } verbose)) true) activity-resuming (fn [package] (bus/say!! :activity-controller {:event :resuming :timestamp (do (.setToNow now) (.toMillis now true)) :package (-> package keyword) }) true) app-crashed (fn [process-name pid short-msg long-msg time-millis stack-trace] (doseq [^ActivityManager$RunningAppProcessInfo app-proc (.getRunningAppProcesses activity-manager)] (when (and (= pid (.pid app-proc)) (= process-name (.processName app-proc))) (bus/say!! :activity-controller {:event :crashed :timestamp (do (.setToNow now) (.toMillis now true)) :packages (into #{} (map keyword (.pkgList app-proc)))}))) true) app-early-not-responding (fn [process-name pid annotation] 1) app-not-responding (fn [process-name pid process-stats] 1) system-not-responding (fn [msg] 1)] (set-meta-data-entry :monitor true) (plugin/blocking-jail [ ;; timeout nil ;; unblock-tag (:stop-unblock-tag @defaults) ;; finalization (do (activity-controller/set-activity-controller {:reset? true})) ;; verbose verbose ] (activity-controller/set-activity-controller {:activity-starting activity-starting :activity-resuming activity-resuming :app-crashed app-crashed :app-early-not-responding app-early-not-responding :app-not-responding app-not-responding :system-not-responding system-not-responding})))) | |||||||||||||||||||||||||
(defn stop [options] (plugin/set-state-entry :figurehead.plugin.monitor :stop true) (plugin/unblock-thread (:stop-unblock-tag @defaults))) | |||||||||||||||||||||||||
the config map | (def config-map {:populate-parse-opts-vector populate-parse-opts-vector :init init :run run :stop stop :param {:priority 1 :auto-restart false}}) | ||||||||||||||||||||||||
helper for starting nREPL server acknowledgement: https://github.com/clojure-android/neko | (ns figurehead.plugin.nrepl.helper (:require (core [plugin :as plugin])) (:require [clojure.java.io :refer [file delete-file]] [clojure.tools.nrepl.server :as nrepl-server] clojure.tools.nrepl.middleware.interruptible-eval) (:import java.io.File java.util.concurrent.atomic.AtomicLong java.util.concurrent.ThreadFactory)) | ||||||||||||||||||||||||
(declare enable-dynamic-compilation clean-cache start-repl) | |||||||||||||||||||||||||
(def defaults (atom { :repl-worker-thread-stack-size 8388608 ; nrepl 8M })) | |||||||||||||||||||||||||
ref: neko.compilation | (def ^{:doc :private true} cache-path (atom nil)) | ||||||||||||||||||||||||
(defn- android-thread-factory [] (let [counter (AtomicLong. 0)] (reify ThreadFactory (newThread [_ runnable] (doto (Thread. (.getThreadGroup (Thread/currentThread)) runnable (format "nREPL-worker-%s" (.getAndIncrement counter)) (:repl-worker-thread-stack-size @defaults)) (.setDaemon true)))))) | |||||||||||||||||||||||||
get absolute path of name | (defn- get-absolute-path-from-cwd [& path-components] (clojure.string/join File/separator ;; http://developer.android.com/reference/java/lang/System.html ;; (into [(System/getProperty "java.io.tmpdir")] ;; path-components) ;; http://developer.android.com/reference/dalvik/system/DexClassLoader.html ;; Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks. (into ["/data/dalvik-cache/"] path-components))) | ||||||||||||||||||||||||
enable dynamic compilation; adapt from neko.compilation/init | (defn enable-dynamic-compilation [clojure-cache-dir] (let [path (get-absolute-path-from-cwd clojure-cache-dir)] (.mkdir (file path)) (plugin/set-state-entry :repl-dynamic-compilation-path path) (reset! cache-path path) (System/setProperty "clojure.compile.path" path) (alter-var-root #'clojure.core/*compile-path* (constantly path)) ;; clean staled cache (clean-cache))) | ||||||||||||||||||||||||
from neko.compilation | (defn- cache-file? [^String name] (and (.startsWith name "repl-") (or (.endsWith name ".dex") (.endsWith name ".jar")))) | ||||||||||||||||||||||||
clean compilation cache | (defn clean-cache [] (locking cache-path (when-let [cache-path @cache-path] (doseq [^File f (file-seq (file cache-path))] (try (when (and (.isFile f) (cache-file? (.getName f))) ;; wierd EBUSY error: http://stackoverflow.com/a/11776458 (let [^File tmp (file (str (.getAbsolutePath f) (System/currentTimeMillis)))] (.renameTo f tmp) (delete-file tmp))) (catch Exception e))) ;; remake the directory if necessary (let [^File compile-path (file cache-path)] (when-not (.exists compile-path) (.mkdir compile-path)))))) | ||||||||||||||||||||||||
neko.init/start-repl | (defn start-repl [& repl-args] (binding [*ns* (create-ns 'user)] (refer-clojure) (use 'clojure.repl) (use 'clojure.pprint) (use 'clojure.java.io) (require '(clojure [string :as str] [set :as set] [stacktrace :as stacktrace :refer [print-stack-trace]])) ;; Android API wrapper (require '[figurehead.api.app.activity-manager :as activity-manager]) (require '[figurehead.api.content.intent :as intent]) (require '[figurehead.api.content.pm.package-manager :as package-manager]) (require '[figurehead.api.content.pm.package-manager-parser :as package-manager-parser]) (require '[figurehead.api.view.input :as input]) (require '[figurehead.api.os.user-manager.user-manager :as user-manager]) (require '[figurehead.api.os.user-manager.user-manager-parser :as user-manager-parser]) (require '[figurehead.api.os.util :as os-util]) (require '[figurehead.api.util.file :as util-file]) (require '(core [bus :as bus] [plugin :as plugin] [state :as state])) (try (require 'compliment.core) (catch Exception e)) (use 'clojure.tools.nrepl.server) (require '[clojure.tools.nrepl.middleware.interruptible-eval :as ie]) (with-redefs-fn {(resolve 'ie/configure-thread-factory) android-thread-factory} #(apply (resolve 'start-server) repl-args)))) | ||||||||||||||||||||||||
nREPL server with cider-nrepl middleware support | (ns figurehead.plugin.nrepl.main (:require (core [init :as init] [state :as state] [bus :as bus] [plugin :as plugin])) (:require [figurehead.plugin.nrepl.helper :as helper]) (:require (figurehead.util [unique-instance :refer [set-meta-data-entry register-meta-data-entry]])) (:require [clojure.tools.nrepl.server :as nrepl-server] ;; comment out the middlewares that are incompatible with Dalvik (cider.nrepl.middleware apropos classpath complete ;;info inspect macroexpand resource stacktrace test ;;trace) ;;[cider.nrepl :refer [cider-nrepl-handler]] compliment.core complete.core [clojure.stacktrace :refer [print-stack-trace]] [clojure.core.async :as async :refer [<!! chan]]) (:import (java.util.concurrent TimeUnit ScheduledThreadPoolExecutor Executors ScheduledFuture))) | ||||||||||||||||||||||||
(def defaults (atom { :repl-clojure-cache-dir "figurehead-cache" :stop-unblock-tag :stop-figurehead.plugin.nrepl :clean-cache-interval (* 15 60) })) | |||||||||||||||||||||||||
(defn populate-parse-opts-vector [current-parse-opts-vector] (init/add-to-parse-opts-vector [ (let [option :nrepl-port] ["-R" (str "--" (name option) " [PORT]") (str "nREPL port") :parse-fn (get-in @init/parse-opts-vector-helper [:parse-fn :inet-port])]) ])) | |||||||||||||||||||||||||
(defn init [options] (register-meta-data-entry :nrepl-port) (when (and (:nrepl-port options)) true)) | |||||||||||||||||||||||||
A vector containing all CIDER middleware. | (def ^:private cider-middleware '[cider.nrepl.middleware.apropos/wrap-apropos cider.nrepl.middleware.classpath/wrap-classpath cider.nrepl.middleware.complete/wrap-complete ;;cider.nrepl.middleware.info/wrap-info cider.nrepl.middleware.inspect/wrap-inspect cider.nrepl.middleware.macroexpand/wrap-macroexpand cider.nrepl.middleware.resource/wrap-resource cider.nrepl.middleware.stacktrace/wrap-stacktrace cider.nrepl.middleware.test/wrap-test ;;cider.nrepl.middleware.trace/wrap-trace ]) | ||||||||||||||||||||||||
CIDER's nREPL handler. | (def ^:private cider-nrepl-handler (apply nrepl-server/default-handler (map resolve cider-middleware))) | ||||||||||||||||||||||||
(defn run [options] (let [verbose (:verbose options) nrepl-port (:nrepl-port options) scheduler ^ScheduledThreadPoolExecutor (Executors/newScheduledThreadPool 1) clean-cache-task (delay ^ScheduledFuture (.scheduleAtFixedRate scheduler #(helper/clean-cache) (:clean-cache-interval @defaults) (:clean-cache-interval @defaults) TimeUnit/SECONDS))] (set-meta-data-entry :nrepl-port nrepl-port) (plugin/blocking-jail [ ;; timeout nil ;; unblock-tag (:stop-unblock-tag @defaults) ;; finalization (do (nrepl-server/stop-server (plugin/get-state-entry :nrepl-server)) (.cancel ^ScheduledFuture @clean-cache-task true) (helper/clean-cache)) ;; verbose verbose ] (helper/enable-dynamic-compilation (:repl-clojure-cache-dir @defaults)) (when verbose (prn [:repl-dynamic-compilation-path (plugin/get-state-entry :repl-dynamic-compilation-path)])) (plugin/set-state-entry :nrepl-server (helper/start-repl :port nrepl-port :handler cider-nrepl-handler)) (plugin/register-exit-hook :figurehead.plugin.nrepl.clean-cache #(helper/clean-cache)) ;; trigger the delayed task @clean-cache-task))) | |||||||||||||||||||||||||
(defn stop [options] (plugin/set-state-entry :core.plugin.nrepl :stop true) (plugin/unblock-thread (:stop-unblock-tag @defaults))) | |||||||||||||||||||||||||
the config map | (def config-map { :populate-parse-opts-vector populate-parse-opts-vector :init init :run run :stop stop :param {:priority 1 :auto-restart true}}) | ||||||||||||||||||||||||
ensuring a unique instance of figurehead | (ns figurehead.plugin.unique-instance.main (:require (core [init :as init] [state :as state] [bus :as bus] [plugin :as plugin])) (:use (figurehead.util unique-instance)) (:require (clojure [pprint :refer [pprint]])) (:import (android.os SystemProperties))) | ||||||||||||||||||||||||
(def defaults (atom {})) | |||||||||||||||||||||||||
(defn populate-parse-opts-vector [current-parse-opts-vector] (init/add-to-parse-opts-vector [ (let [option :kill] [nil (str "--" (name option)) (str "kill existing instance and exit")]) (let [option :replace] [nil (str "--" (name option)) (str "replace existing instance and continue")]) (let [option :status] [nil (str "--" (name option)) (str "return status of existing instance and exit")]) ])) | |||||||||||||||||||||||||
(defn init [options] true) | |||||||||||||||||||||||||
(defn run [options] (let [kill? (:kill options) replace? (:replace options) status? (:status options)] (cond status? (do (let [is-running? (is-running?)] (pprint (if is-running? {:is-running? true :state (get-meta-data)} (do (unset-meta-data) {:is-running? false})))) (System/exit 0)) kill? (do (kill-existing-instance) (System/exit 0))) (if replace? (replace-existing-instance) (keep-existing-instance)))) | |||||||||||||||||||||||||
the config map | (def config-map { :populate-parse-opts-vector populate-parse-opts-vector :init init :run run ;;:stop stop :param {:priority 1 ;;:auto-restart true }}) | ||||||||||||||||||||||||
figurehead helpers | (ns figurehead.ui.helper.figurehead (:use (figurehead.ui su util)) (:require (neko [threading :refer [on-ui]] [notify :refer [toast]])) (:require (clojure [string :as str] [set :as set] [pprint :refer [pprint]] [stacktrace :refer [print-stack-trace]])) (:import (android.content Context)) (:import (android.widget Switch Button CheckBox EditText TextView ScrollView) (android.view View) (java.util List))) | ||||||||||||||||||||||||
(declare get-figurehead-apk-path build-figurehead-command figurehead-is-running? get-running-figurehead-state) | |||||||||||||||||||||||||
get path to the backing APK | (defn get-figurehead-apk-path [^Context context] (let [apk-path (get-app-info-entry :apk-path)] ;; if already (if apk-path apk-path (when-let [package-manager (.getPackageManager context)] (let [package-name (.getPackageName context)] (when-let [app-info (.getApplicationInfo package-manager package-name 0)] (let [apk-path (.publicSourceDir app-info)] (set-app-info-entry :apk-path apk-path) apk-path))))))) | ||||||||||||||||||||||||
build figurehead command to feed SU | (defn build-figurehead-command [& commands] (when-let [figurehead-script @(get-app-info-entry :figurehead-script)] (str/join " " (into [figurehead-script] commands)))) | ||||||||||||||||||||||||
fast check of whether figurehead is running based on external commands | |||||||||||||||||||||||||
the SU instance for figurehead-is-running? | (def ^:private su-figurehead-is-running (atom nil)) | ||||||||||||||||||||||||
return whether figurehead is running | (defn figurehead-is-running? [] (let [is-running? (promise) commands ["pgrep -f figurehead.main"] timeout 60] (execute-root-command :commands commands :timeout timeout :callback? true :buffered? true :on-normal (do (try (deliver is-running? (and output (not (empty? (.trim (str/join " " output)))))) (catch Exception e (print-stack-trace e) (deliver is-running? false)))) :on-error (do (deliver is-running? false)) :error-message "Cannot determine whether Figurehead is running") @is-running?)) | ||||||||||||||||||||||||
get the running figurehead session's state | (defn get-running-figurehead-state [] (let [state (promise)] (if (figurehead-is-running?) (do (let [commands [(build-figurehead-command "--status")] timeout 120] (execute-root-command :commands commands :timeout timeout :callback? true :buffered? true :on-normal (do (let [output (str/join " " output)] (try (deliver state (read-string output)) (catch Exception e (print-stack-trace e) (deliver state nil))))) :on-error (do (deliver state nil)) :error-message "Cannot access Figurehead running state"))) (do (deliver state nil))) @state)) | ||||||||||||||||||||||||
REPL helpers | (ns figurehead.ui.helper.repl (:use (figurehead.ui su util)) (:require (neko init [threading :refer [on-ui]] [notify :refer [toast]])) (:require (clojure [string :as str] [set :as set] [pprint :refer [pprint]] [stacktrace :refer [print-stack-trace]])) (:require [clojure.core.async :as async]) (:require [clojure.tools.nrepl.server :as nrepl-server] (cider.nrepl.middleware apropos classpath ;;complete ;;info inspect macroexpand resource stacktrace test ;;trace))) | ||||||||||||||||||||||||
(declare start-repl stop-repl) | |||||||||||||||||||||||||
the sole REPL session to figurehead.ui | (def repl-session (atom nil)) | ||||||||||||||||||||||||
A vector containing all CIDER middleware. | (def ^:private cider-middleware '[cider.nrepl.middleware.apropos/wrap-apropos cider.nrepl.middleware.classpath/wrap-classpath ;;cider.nrepl.middleware.complete/wrap-complete ;;cider.nrepl.middleware.info/wrap-info cider.nrepl.middleware.inspect/wrap-inspect cider.nrepl.middleware.macroexpand/wrap-macroexpand cider.nrepl.middleware.resource/wrap-resource cider.nrepl.middleware.stacktrace/wrap-stacktrace cider.nrepl.middleware.test/wrap-test ;;cider.nrepl.middleware.trace/wrap-trace ]) | ||||||||||||||||||||||||
CIDER's nREPL handler. | (def ^:private cider-nrepl-handler (apply nrepl-server/default-handler (map resolve cider-middleware))) | ||||||||||||||||||||||||
start REPL is there is none | (defn start-repl [& {:keys [port] :or {port 9999} :as args}] (when-not @repl-session (background-thread (reset! repl-session (neko.init/start-repl :port port :handler cider-nrepl-handler)) ;; set up 'user ns (do (in-ns 'user) (use 'clojure.repl) (use 'clojure.pprint) (use 'clojure.java.io) (require 'clojure.set :as set) (use 'neko.doc) (use 'neko.debug))))) | ||||||||||||||||||||||||
stop the sole | (defn stop-repl [] (let [session @repl-session] (when session (background-thread (reset! repl-session nil) (nrepl-server/stop-server session))))) | ||||||||||||||||||||||||
widgets helpers | (ns figurehead.ui.helper.widgets (:use (figurehead.ui su util) (figurehead.ui.helper figurehead)) (:require (neko [threading :refer [on-ui]] [notify :refer [toast]])) (:require (clojure [string :as str] [set :as set] [pprint :refer [pprint]] [stacktrace :refer [print-stack-trace]])) (:import (android.widget Switch Button CheckBox EditText TextView ScrollView) (android.view View))) | ||||||||||||||||||||||||
(declare with-widgets set-enabled sync-widgets-to-state sync-widgets-to-figurehead widgets-to-arg-map widgets-to-figurehead-args save-widget-state get-saved-widget-state) | |||||||||||||||||||||||||
wrap body with widgets tagged bindings | (defmacro with-widgets [widgets & body] `(let [widgets# ~widgets ~'widget-figurehead-switch ^Switch (:figurehead-switch widgets#) ~'widget-monitor ^CheckBox (:monitor widgets#) ~'widget-verbose ^CheckBox (:verbose widgets#) ~'widget-wifi-if ^TextView (:wifi-if widgets#) ~'widget-repl-port ^EditText (:repl-port widgets#) ~'widget-mastermind-address ^EditText (:mastermind-address widgets#) ~'widget-mastermind-port ^EditText (:mastermind-port widgets#) ~'widget-extra-args ^EditText (:extra-args widgets#) ~'widget-status ^TextView (:status widgets#) ~'widget-scroll-status ^ScrollView (:scroll-status widgets#) ~'widget-clear-status ^Button (:clear-status widgets#)] ~@body)) | ||||||||||||||||||||||||
set enabled status of the widgets | (defn set-enabled [widgets enabled] (on-ui (doseq [[_ ^View widget] widgets] (.setEnabled widget enabled)) ;; special cases (with-widgets widgets (.setEnabled widget-wifi-if true)))) | ||||||||||||||||||||||||
sync widgets to state | (defn sync-widgets-to-state [widgets state] (on-ui (with-widgets widgets (try ;; temporarily disable all widgets during state transition (set-enabled widgets false) (try (when-not (nil? (:monitor state)) (.setChecked widget-monitor (Boolean/parseBoolean ^String (str (:monitor state))))) (catch Exception e)) (try (when-not (nil? (:verbose state)) (.setChecked widget-verbose (Boolean/parseBoolean ^String (str (:verbose state))))) (catch Exception e)) (try (when-not (nil? (:nrepl-port state)) (.setText widget-repl-port ^String (let [nrepl-port ^String (str (:nrepl-port state))] (if nrepl-port nrepl-port "")))) (catch Exception e)) (try (when-not (nil? (:mastermind-address state)) (.setText widget-mastermind-address ^String (let [mastermind-address ^String (str (:mastermind-address state))] (if mastermind-address mastermind-address "")))) (catch Exception e)) (try (when-not (nil? (:mastermind-port state)) (.setText widget-mastermind-port ^String (let [mastermind-port ^String (str (:mastermind-port state))] (if mastermind-port mastermind-port "")))) (catch Exception e)) (try (when-not (nil? (:extra-args state)) (.setText widget-extra-args ^String (let [extra-args ^String (str (:extra-args state))] (if extra-args extra-args "")))) (catch Exception e)) (try (when-not (nil? (:is-running? state)) (.setChecked widget-figurehead-switch (Boolean/parseBoolean ^String (str (:is-running? state))))) (catch Exception e)) (catch Exception e (print-stack-trace e)) (finally ;; enable needed widgets (if (.isChecked widget-figurehead-switch) (do (.setEnabled widget-figurehead-switch true) (.setEnabled widget-scroll-status true) (.setEnabled widget-status true) (.setEnabled widget-clear-status true)) (do (set-enabled widgets true)))))))) | ||||||||||||||||||||||||
the first sync should be from SU figurehead; later can from saved widget state | (def ^:private first-sync? (atom true)) | ||||||||||||||||||||||||
sync widgets status to figurehead | (defn sync-widgets-to-figurehead [widgets] (background-thread (let [saved-widget-state (get-saved-widget-state)] (sync-widgets-to-state widgets (if (and saved-widget-state ;; force to sync at least once (not @first-sync?)) saved-widget-state (do (let [state (get-running-figurehead-state) is-running? (if (:is-running? state) true false)] (assoc (:state state) :is-running? is-running?)) (reset! first-sync? false))))))) | ||||||||||||||||||||||||
convert widgets to Figurehead argument map | (defn widgets-to-arg-map [widgets] (let [args (atom {})] (with-widgets widgets (try (let [checked (.isChecked widget-monitor)] (swap! args assoc :monitor checked)) (catch Exception e (print-stack-trace e))) (try (let [checked (.isChecked widget-verbose)] (swap! args assoc :verbose checked)) (catch Exception e (print-stack-trace e))) (try (let [port (int (read-string (.. widget-repl-port getText toString trim)))] (if (< 0 port 65536) (do (swap! args assoc :repl-port port)) (do (on-ui (.setText widget-repl-port ""))))) (catch Exception e (print-stack-trace e) (on-ui (.setText widget-repl-port "")))) (try (let [text (read-string (str "\"" (.. widget-mastermind-address getText toString trim) "\""))] (when-not (empty? text) (swap! args assoc :mastermind-address text))) (catch Exception e (print-stack-trace e) (on-ui (.setText widget-mastermind-address "")))) (try (let [port (int (read-string (.. widget-mastermind-port getText toString trim)))] (if (< 0 port 65536) (do (swap! args assoc :mastermind-port port)) (do (on-ui (.setText widget-mastermind-port ""))))) (catch Exception e (print-stack-trace e) (on-ui (.setText widget-mastermind-port "")))) (try (let [text (read-string (str "\"" (.. widget-extra-args getText toString trim) "\""))] (when-not (empty? text) (swap! args assoc :extra-args text))) (catch Exception e (print-stack-trace e) (on-ui (.setText widget-extra-args ""))))) @args)) | ||||||||||||||||||||||||
construct figurehead command-line args from widgets | (defn widgets-to-figurehead-args [widgets] (let [args (atom []) arg-map (widgets-to-arg-map widgets) monitor (:monitor arg-map) verbose (:verbose arg-map) repl-port (:repl-port arg-map) mastermind-address (:mastermind-address arg-map) mastermind-port (:mastermind-port arg-map) extra-args (:extra-args arg-map)] (when monitor (swap! args conj "--monitor")) (when verbose (swap! args conj "--verbose")) (when repl-port (swap! args conj (str "--nrepl-port " repl-port))) (when mastermind-address (swap! args conj (str "--mastermind-address " mastermind-address))) (when mastermind-port (swap! args conj (str "--mastermind-port " mastermind-port))) (when extra-args (swap! args conj (str extra-args))) @args)) | ||||||||||||||||||||||||
the saved widget state | (def ^:private saved-widget-state (atom {})) | ||||||||||||||||||||||||
(defn save-widget-state [widgets] (with-widgets widgets (try (swap! saved-widget-state assoc :monitor (.isChecked widget-monitor)) (swap! saved-widget-state assoc :verbose (.isChecked widget-verbose)) (swap! saved-widget-state assoc :nrepl-port (str (.getText widget-repl-port))) (swap! saved-widget-state assoc :mastermind-address (str (.getText widget-mastermind-address))) (swap! saved-widget-state assoc :mastermind-port (str (.getText widget-mastermind-port))) (swap! saved-widget-state assoc :extra-args (str (.getText widget-extra-args))) (swap! saved-widget-state assoc :is-running? (.isChecked widget-figurehead-switch)) (catch Exception e (reset! saved-widget-state nil))))) | |||||||||||||||||||||||||
obtain the saved widget state | (defn get-saved-widget-state [] @saved-widget-state) | ||||||||||||||||||||||||
(ns figurehead.ui.main (:use (figurehead.ui su util) (figurehead.ui.helper figurehead widgets)) (:require (neko [activity :refer [defactivity set-content-view! with-activity]] [notify :refer [toast]] [threading :refer [on-ui]] [find-view :refer [find-view]] log) (neko.listeners [view :refer [on-click]])) (:require (clojure [string :as str] [pprint :refer [pprint]])) (:require [clojure.stacktrace :refer [print-stack-trace]]) (:import (android.app Activity) (android.widget Switch Button CheckBox EditText TextView ScrollView) (android.view View) (android.content Context)) (:import (android.net.wifi WifiManager) (java.net InetAddress) (java.nio ByteOrder) (java.math BigInteger)) (:import (java.util List)) (:import (org.apache.commons.io FilenameUtils)) (:import (figurehead.ui R$layout R$id)) (:import eu.chainfire.libsuperuser.Shell$Interactive)) | |||||||||||||||||||||||||
(declare update-wifi-if) | |||||||||||||||||||||||||
all the widgets on this activity | (def widgets (atom nil)) | ||||||||||||||||||||||||
(defactivity figurehead.ui.main :on-create (fn [^Activity this bundle] (do ;; UI initialization (on-ui (set-content-view! this R$layout/main)) (with-activity this (reset! widgets {:figurehead-switch ^Switch (find-view R$id/figurehead_switch) :monitor ^CheckBox (find-view R$id/monitor) :verbose ^CheckBox (find-view R$id/verbose) :wifi-if ^TextView (find-view R$id/wifi_if) :repl-port ^EditText (find-view R$id/repl_port) :mastermind-address ^EditText (find-view R$id/mastermind_address) :mastermind-port ^EditText (find-view R$id/mastermind_port) :extra-args ^EditText (find-view R$id/extra_args) :scroll-status ^ScrollView (find-view R$id/scroll_status) :status ^TextView (find-view R$id/status) :clear-status ^Button (find-view R$id/clear_status)}))) (set-app-info-entry :figurehead-script (promise)) (if (su?) (do (try (when-not (get-app-info-entry :apk-path) (set-app-info-entry :apk-path (get-figurehead-apk-path this))) (let [apk-path (get-app-info-entry :apk-path)] (if apk-path (do (let [path (FilenameUtils/getFullPath apk-path) figurehead-script "/system/bin/figurehead"] (let [commands [ ;; http://stackoverflow.com/a/13366444 (str "mount -o rw,remount /system") ;; create the script in one write (str "echo \ (str/join "\\n" ["# bootstrapping Figurehead" (str "export CLASSPATH=" apk-path) (str "exec app_process " path " figurehead.main \\\"\\$@\\\)]) "\" > " figurehead-script) (str "chmod 700 " figurehead-script)] ;; factor in the time it takes for user to authorize SU timeout 120] (execute-root-command :commands commands :timeout timeout :callback? true :buffered? false :on-normal (do (deliver (get-app-info-entry :figurehead-script) figurehead-script)) :on-error (do (deliver (get-app-info-entry :figurehead-script) nil)) :error-message (str/join " " ["Cannot create" figurehead-script]))))) (do (on-ui (toast "Figurehead cannot find its own APK.")) (deliver (get-app-info-entry :figurehead-script) nil)))) (catch Exception e (print-stack-trace e)))) (do ;; no SU (on-ui (toast "Superuser needed but not available!"))))) :on-resume (fn [^Activity this] (let [widgets @widgets context this] (update-wifi-if context widgets) (with-widgets widgets (sync-widgets-to-figurehead widgets) (on-ui (.setOnClickListener widget-clear-status (on-click (update-wifi-if context widgets) ;; clear text (.setText widget-status ))) (.setOnClickListener widget-wifi-if (on-click (update-wifi-if context widgets))) (.setOnCheckedChangeListener widget-figurehead-switch (proxy [android.widget.CompoundButton$OnCheckedChangeListener] [] (onCheckedChanged [^android.widget.CompoundButton button-view is-checked?] (update-wifi-if context widgets) (background-looper-thread (let [figurehead-is-running? (figurehead-is-running?)] (when (not= is-checked? figurehead-is-running?) (on-ui ;; temporarily disable widgets during state transition (set-enabled widgets false) ;; allow user to change her mind (.setEnabled widget-figurehead-switch true)) (if is-checked? (do ;; turn on (let [figurehead-args (into ["--replace"] (widgets-to-figurehead-args widgets)) commands [(apply build-figurehead-command figurehead-args)] ;; this is supposed to be a long running command timeout 0] (execute-root-command :commands commands :timeout timeout :callback? true :buffered? false :command-line-listener (do (on-ui (.append widget-status (with-out-str (println line))) (.post widget-scroll-status #(.fullScroll widget-scroll-status View/FOCUS_DOWN)))) :on-normal (do ;; Figurehead returns (set-enabled widgets true) (on-ui (.setChecked widget-figurehead-switch false))) :on-error (do (.setEnabled widget-figurehead-switch true) (.setEnabled widget-scroll-status true) (.setEnabled widget-status true) (.setEnabled widget-clear-status true)) :error-message "Cannot start Figurehead")) ;; enable needed widgets (on-ui (do (.setEnabled widget-figurehead-switch true) (.setEnabled widget-scroll-status true) (.setEnabled widget-status true) (.setEnabled widget-clear-status true)))) (do ;; turn off (let [commands [(build-figurehead-command "--kill")] timeout 120] (execute-root-command :commands commands :timeout timeout :callback? true :buffered? false :on-normal (do (on-ui (set-enabled widgets true))) :error-message "Cannot turn off Figurehead")))))))))))))) :on-pause (fn [^Activity this] (let [widgets @widgets] (with-widgets widgets (save-widget-state widgets)))) :on-stop (fn [^Activity this] (let [widgets @widgets] (with-widgets widgets (save-widget-state widgets)))) :on-destroy (fn [^Activity this] (let [widgets @widgets] (with-widgets widgets (save-widget-state widgets))))) | |||||||||||||||||||||||||
update wifi-if widget based on current WiFi address http://stackoverflow.com/a/18638588 | (defn update-wifi-if [^Context context widgets] (with-widgets widgets (let [wifi-manager ^WifiManager (.getSystemService context Context/WIFI_SERVICE)] (if wifi-manager (let [ip (.. wifi-manager getConnectionInfo getIpAddress) ip-byte-array (.. (BigInteger/valueOf (if (= (ByteOrder/nativeOrder) ByteOrder/BIG_ENDIAN) ip (Integer/reverseBytes ip))) toByteArray)] (try (let [ip (.. (InetAddress/getByAddress ip-byte-array) getHostAddress)] (on-ui (.setText widget-wifi-if ip))) (catch Exception e (print-stack-trace e) (on-ui (.setText widget-wifi-if ""))))) (do (on-ui (.setText widget-wifi-if ""))))))) | ||||||||||||||||||||||||
(ns figurehead.ui.su (:use (figurehead.ui util)) (:require (neko [notify :refer [toast]] [threading :refer [on-ui]] [log :as log])) (:require (clojure [string :as str] [stacktrace :refer [print-stack-trace]])) (:import (android.content Context)) (:import (java.util Collection ArrayList List)) (:import eu.chainfire.libsuperuser.Shell eu.chainfire.libsuperuser.Shell$SU eu.chainfire.libsuperuser.Shell$SH eu.chainfire.libsuperuser.Shell$Interactive eu.chainfire.libsuperuser.Shell$Builder eu.chainfire.libsuperuser.Shell$OnCommandLineListener eu.chainfire.libsuperuser.Shell$OnCommandResultListener)) | |||||||||||||||||||||||||
(declare su? su sh open-root-shell execute-root-command) | |||||||||||||||||||||||||
check whether SuperUser is available | (defn su? [] (Shell$SU/available)) | ||||||||||||||||||||||||
run cmds with Super User | (defn su [& commands] (when commands (Shell$SU/run (ArrayList. ^Collection commands)))) | ||||||||||||||||||||||||
run cmds with SHell | (defn sh [& commands] (when commands (Shell$SH/run (ArrayList. ^Collection commands)))) | ||||||||||||||||||||||||
open a new root shell | (defmacro open-root-shell [& {:keys [timeout want-stderr? minimal-logging? callback? command-result-listener on-shell-running on-watchdog-exit on-shell-died on-shell-exec-failed on-shell-wrong-uid on-default on-error on-normal error-message] :or {timeout 0 want-stderr? false minimal-logging? true callback? false error-message "open-root-shell"} :as args}] `(let [timeout# ~timeout want-stderr?# ~want-stderr? minimal-logging?# ~minimal-logging? ~'error-message ~error-message su# (promise)] (background-looper-thread (deliver su# (.. (Shell$Builder.) (useSU) (setAutoHandler true) (setWatchdogTimeout timeout#) (setWantSTDERR want-stderr?#) (setMinimalLogging minimal-logging?#) (open ~(when callback? `(proxy [Shell$OnCommandResultListener] [] (onCommandResult [~'command-code ~'exit-code ^List ~'output] ~command-result-listener (if (>= ~'exit-code 0) (do ;; normal ~on-normal) (do ;; error (let [error# (str ~'error-message " " ~'exit-code)] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-error)) (case ~'exit-code Shell$OnCommandResultListener/SHELL_RUNNING (do ~on-shell-running) Shell$OnCommandResultListener/WATCHDOG_EXIT (do (let [error# (str ~'error-message " (timeout)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-watchdog-exit) Shell$OnCommandResultListener/SHELL_DIED (do (let [error# (str ~'error-message " (died)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-died) Shell$OnCommandResultListener/SHELL_EXEC_FAILED (do (let [error# (str ~'error-message " (exec failed)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-exec-failed) Shell$OnCommandResultListener/SHELL_WRONG_UID (do (let [error# (str ~'error-message " (wrong uid)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-wrong-uid) ;; default clause ;; should not reach here (do ~on-default))))))))) @su#)) | ||||||||||||||||||||||||
execute root command | (defmacro execute-root-command [& {:keys [commands timeout command-code callback? buffered? command-result-listener command-line-listener on-shell-running on-watchdog-exit on-shell-died on-shell-exec-failed on-shell-wrong-uid on-default on-error on-normal error-message] :or {commands [] timeout 0 command-code 0 callback? false buffered? true error-message "execute-root-command"} :as args}] `(background-looper-thread (try (let [commands# ~commands timeout# ~timeout ~'command-code ~command-code ~'error-message ~error-message] (let [^Shell$Interactive ~'su-instance (open-root-shell :timeout timeout#)] (try (let [commands# (if (sequential? commands#) commands# [commands#])] (doseq [command# commands#] (let [~'command (str command#)] (let [info# (str "SU: " ~'command)] (log/i info#)) ~(if callback? (do (if buffered? (do ;; process buffered output `(.addCommand ^Shell$Interactive ~'su-instance ^String ~'command ~'command-code (proxy [Shell$OnCommandResultListener] [] (onCommandResult [~'command-code ~'exit-code ^List ~'output] ~command-result-listener (if (>= ~'exit-code 0) (do ;; normal ~on-normal) (do ;; error (let [error# (str ~'error-message " " ~'exit-code)] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-error)) (case ~'exit-code Shell$OnCommandResultListener/SHELL_RUNNING (do ~on-shell-running) Shell$OnCommandResultListener/WATCHDOG_EXIT (do (let [error# (str ~'error-message " (timeout)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-watchdog-exit) Shell$OnCommandResultListener/SHELL_DIED (do (let [error# (str ~'error-message " (died)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-died) Shell$OnCommandResultListener/SHELL_EXEC_FAILED (do (let [error# (str ~'error-message " (exec failed)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-exec-failed) Shell$OnCommandResultListener/SHELL_WRONG_UID (do (let [error# (str ~'error-message " (wrong uid)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-wrong-uid) ;; default clause ;; should not reach here (do ~on-default)))))) (do ;; process output line by line `(.addCommand ^Shell$Interactive ~'su-instance ^String ~'command ~'command-code (proxy [Shell$OnCommandLineListener] [] (onLine [^String ~'line] ~command-line-listener) (onCommandResult [~'command-code ~'exit-code] ~command-result-listener (if (>= ~'exit-code 0) (do ;; normal ~on-normal) (do ;; error (let [error# (str ~'error-message " " ~'exit-code)] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-error)) (case ~'exit-code Shell$OnCommandLineListener/SHELL_RUNNING (do ~on-shell-running) Shell$OnCommandLineListener/WATCHDOG_EXIT (do (let [error# (str ~'error-message " (timeout)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-watchdog-exit) Shell$OnCommandLineListener/SHELL_DIED (do (let [error# (str ~'error-message " (died)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-died) Shell$OnCommandLineListener/SHELL_EXEC_FAILED (do (let [error# (str ~'error-message " (exec failed)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-exec-failed) Shell$OnCommandLineListener/SHELL_WRONG_UID (do (let [error# (str ~'error-message " (wrong uid)")] (neko.threading/on-ui (neko.notify/toast error#)) (neko.log/e error#)) ~on-shell-wrong-uid) ;; default clause ;; should not reach here (do ~on-default)))))))) (do `(.addCommand ^Shell$Interactive ~'su-instance ^String ~'command)))))) (finally (.close ^Shell$Interactive ~'su-instance))))) (catch Exception e# (print-stack-trace e#))))) | ||||||||||||||||||||||||
utilities | (ns figurehead.ui.util (:require (clojure [string :as str] [set :as set]))) | ||||||||||||||||||||||||
(declare ;; threading background-thread background-looper-thread ;; app-info app-info get-app-info-entry update-app-info-entry set-app-info-entry unset-app-info-entry) | |||||||||||||||||||||||||
threading | |||||||||||||||||||||||||
run the body in a background thread | (defmacro background-thread [& body] `(let [thread# (Thread. (fn [] (android.os.Process/setThreadPriority (android.os.Process/myTid) android.os.Process/THREAD_PRIORITY_BACKGROUND) ~@body))] (.start thread#) thread#)) | ||||||||||||||||||||||||
run the body in a background thread with a Looper | (defmacro background-looper-thread [& body] `(let [looper# (promise) thread# (Thread. (fn [] (android.os.Looper/prepare) (deliver looper# (android.os.Looper/myLooper)) (android.os.Process/setThreadPriority (android.os.Process/myTid) android.os.Process/THREAD_PRIORITY_BACKGROUND) ~@body (android.os.Looper/loop)))] (.start thread#) looper#)) | ||||||||||||||||||||||||
app info | |||||||||||||||||||||||||
app info | (def app-info (atom {})) | ||||||||||||||||||||||||
get app info entry with | (defn get-app-info-entry [name] (get @app-info name)) | ||||||||||||||||||||||||
update app info entry with | (defn update-app-info-entry [name f & args] (apply swap! app-info update-in [name] f args)) | ||||||||||||||||||||||||
set app info entry with | (defn set-app-info-entry [name value] (swap! app-info assoc name value)) | ||||||||||||||||||||||||
unset app info entry with | (defn unset-app-info-entry [name] (swap! app-info dissoc name)) | ||||||||||||||||||||||||
(ns figurehead.util.init (:require (figurehead.util [services :as services :refer [register-service]])) (:import (android.os ServiceManager IUserManager IUserManager$Stub) (android.app ActivityManagerNative IActivityManager) (android.content.pm IPackageManager IPackageManager$Stub))) | |||||||||||||||||||||||||
(declare init) | |||||||||||||||||||||||||
register services | (defn- register-services [] (register-service :activity-manager ^IActivityManager #(ActivityManagerNative/getDefault)) (register-service :user-manager ^IUserManager #(->> (ServiceManager/getService "user") (IUserManager$Stub/asInterface))) (register-service :package-manager ^IPackageManager #(->> (ServiceManager/getService "package") (IPackageManager$Stub/asInterface)))) | ||||||||||||||||||||||||
initialization | (defn init [] (register-services)) | ||||||||||||||||||||||||
(ns figurehead.util.routines) | |||||||||||||||||||||||||
(declare vector-to-map) | |||||||||||||||||||||||||
convert [:key1 val1 :key2 val2 ...] into {:key1 val1, key2 val2, ...} | (defn vector-to-map [v] (into {} (map vec (partition 2 v)))) | ||||||||||||||||||||||||
(ns figurehead.util.services (:require (core [bus :as bus]))) | |||||||||||||||||||||||||
(declare policy services get-service list-service register-service) | |||||||||||||||||||||||||
internal policy of service procedures | (def policy (atom {:register-retry-interval 500})) | ||||||||||||||||||||||||
map from service tag to its (delayed) instance | (def services (atom {})) | ||||||||||||||||||||||||
get service by tag | (defn get-service [tag] (when-let [service (tag @services)] ;; get the (delayed) service @service)) | ||||||||||||||||||||||||
get tags of all services | (defn list-services [] (keys @services)) | ||||||||||||||||||||||||
register a service with tag by (obtain-fn) | (defn register-service [tag obtain-fn] (swap! services assoc tag (delay (loop [h (obtain-fn)] (if h h (do (bus/say!! :error (str "obtaining " (name tag) "...")) (Thread/sleep (:register-retry-interval @policy)) (recur (obtain-fn)))))))) | ||||||||||||||||||||||||
work with a unique Figurehead instance | (ns figurehead.util.unique-instance (:require (core [init :as init] [state :as state] [bus :as bus] [plugin :as plugin])) (:require (figurehead.api.os [util :as os-util])) (:import (android.os SystemProperties))) | ||||||||||||||||||||||||
(declare ;; test whether running already is-running? ;; existing instance kill-existing-instance replace-existing-instance keep-existing-instance ;; pid get-pid set-pid ;; meta data meta-data get-meta-data-entry-sysprop get-meta-data-entry set-meta-data-entry unset-meta-data-entry get-meta-data unset-meta-data) | |||||||||||||||||||||||||
(def ^:private defaults (atom { :sysprop-pid "figurehead.pid" :sysprop-meta-data "figurehead.meta-data" :exit-code-on-existing 19 })) | |||||||||||||||||||||||||
test whether running already | |||||||||||||||||||||||||
test whether a Figurehead instance is already running | (defn is-running? [] (let [sys-pid (get-pid)] (os-util/test-process {:pid sys-pid}))) | ||||||||||||||||||||||||
existing instance | |||||||||||||||||||||||||
kill the existing Figurehead instance | (defn kill-existing-instance [] (let [sys-pid (get-pid)] (when (and (not= sys-pid 0) (is-running?)) (os-util/kill-process {:pid sys-pid}) (set-pid 0) (unset-meta-data)))) | ||||||||||||||||||||||||
replace the existing Figurehead instance with the current oneexit if there is an existing Figurehead instance, continue otherwise | (let [run (fn [] (let [cur-pid (os-util/get-my-pid {})] (set-pid cur-pid) (.addShutdownHook ^Runtime (Runtime/getRuntime) (Thread. #(let [sys-pid (get-pid)] (when (= sys-pid cur-pid) (set-pid 0) (unset-meta-data)))))))] (defn replace-existing-instance [] (kill-existing-instance) (run)) (defn keep-existing-instance [] (if (is-running?) (System/exit (:exit-code-on-existing @defaults)) (run)))) | ||||||||||||||||||||||||
pid | |||||||||||||||||||||||||
get pid of the unique instance | (defn get-pid [] (let [sysprop-pid (:sysprop-pid @defaults) pid (os-util/get-system-property {:name sysprop-pid :def 0 :int? true})] pid)) | ||||||||||||||||||||||||
set pid of the unique instance | (defn set-pid [pid] (let [sysprop-pid (:sysprop-pid @defaults)] (os-util/set-system-property {:name sysprop-pid :value pid}))) | ||||||||||||||||||||||||
meta data | |||||||||||||||||||||||||
(def meta-data (atom {})) | |||||||||||||||||||||||||
get meta data entry's sysprop | (defn get-meta-data-entry-sysprop [entry-name] (str "figurehead." (cond (keyword? entry-name) (name entry-name) :else name))) | ||||||||||||||||||||||||
get meta data entry | (defn get-meta-data-entry [name] (get @meta-data name)) | ||||||||||||||||||||||||
register meta data entry | (defn register-meta-data-entry [name] (swap! meta-data assoc name nil)) | ||||||||||||||||||||||||
set meta data entry | (defn set-meta-data-entry [name value] (let [sysprop (get-meta-data-entry-sysprop name)] (os-util/set-system-property {:name sysprop :value value}) (swap! meta-data assoc name value))) | ||||||||||||||||||||||||
unset meta data entry | (defn unset-meta-data-entry [name] (let [sysprop (get-meta-data-entry-sysprop name)] (swap! meta-data dissoc name) (os-util/set-system-property {:name sysprop :value ""}))) | ||||||||||||||||||||||||
get all meta data of the unique instance | (defn get-meta-data [] (into {} (for [name (keys @meta-data)] (let [sysprop (get-meta-data-entry-sysprop name) entry (os-util/get-system-property {:name sysprop})] [name entry])))) | ||||||||||||||||||||||||
unset all meta data of the unique instance | (defn unset-meta-data [] (doseq [name (keys @meta-data)] (let [sysprop (get-meta-data-entry-sysprop name)] (os-util/set-system-property {:name sysprop :value ""})))) | ||||||||||||||||||||||||