├── README.md └── airdroid.py /README.md: -------------------------------------------------------------------------------- 1 | An unofficial library for the Airdroid app which allows full control over an Android device. 2 | 3 | Currently a work in progress to make all features of Airdroid available via this library. 4 | -------------------------------------------------------------------------------- /airdroid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | OverAirdroid 5 | 6 | An unofficial API script to make automating Airdroid functions easier. 7 | 8 | Created by Matthew Bryant 9 | 10 | """ 11 | import datetime 12 | import requests 13 | import base64 14 | import urllib 15 | import time 16 | import json 17 | import md5 18 | import sys 19 | from bs4 import BeautifulSoup 20 | 21 | class overairdroid: 22 | """ 23 | Converts the callback Javascript to a dict of the JSON 24 | 25 | @input_json Inputted JSON packed in Airdroid callback stuff 26 | 27 | return Returns a dict of inputted Airdroid Javascript 28 | """ 29 | def air2json( self, input_json ): 30 | init_json = input_json 31 | init_json = init_json.replace("_jqjsp(", "") 32 | init_json = init_json[:-1] 33 | init_json = json.loads( init_json ) 34 | return init_json 35 | 36 | """ 37 | Make viewing trees a lot easier 38 | 39 | @input_dict The input dictionary for viewing 40 | 41 | return None 42 | """ 43 | def pprint( self, input_dict): 44 | print json.dumps(input_dict, sort_keys=True, indent=4, separators=(',', ': ')) 45 | 46 | """ 47 | Initialize all of the needed variables 48 | 49 | @inut_dict The input dict with all of our needed values 50 | 51 | return None 52 | """ 53 | def initialize_variables( self, input_dict ): 54 | self.channel_token = input_dict['result']['device'][0]['channelToken'] 55 | self.device_id = input_dict['result']['device'][0]['deviceId'] 56 | self.id = str( input_dict['result']['device'][0]['id'] ) 57 | self.imsi = input_dict['result']['device'][0]['imsi'] 58 | self.logic_key = input_dict['result']['device'][0]['logicKey'] 59 | self.content_id = input_dict['result']['device'][0]['id'] 60 | self.account_id = input_dict['result']['id'] 61 | self.manufacturer = input_dict['result']['device'][0]['manu'] 62 | self.model = input_dict['result']['device'][0]['model'] 63 | self.phone_ip = str( input_dict['result']['device'][0]['netOpts']['ip'] ) 64 | self.phone_port = str( input_dict['result']['device'][0]['netOpts']['port'] ) 65 | self.phone_socket_port = str( input_dict['result']['device'][0]['netOpts']['socket_port'] ) 66 | self.phone_ssl_port = str( input_dict['result']['device'][0]['netOpts']['ssl_port'] ) 67 | self.phone_wifi = input_dict['result']['device'][0]['netOpts']['usewifi'] 68 | self.get_bb() 69 | 70 | def statusmsg( self, input_string ): 71 | if self.verbose: 72 | print "[ STATUS ] " + input_string 73 | 74 | def errormsg( self, input_string ): 75 | if self.verbose: 76 | print "[ ERROR ] " + input_string 77 | 78 | """ 79 | Initialize the OverTheAirdroid object 80 | 81 | @in_username The Airdroid username 82 | @in_password The Airdroid password 83 | @in_verbose Should the program be verbose? 84 | 85 | return None 86 | """ 87 | def __init__(self, in_username, in_password, in_verbose = True): 88 | self.username = in_username 89 | self.password = in_password 90 | self.verbose = in_verbose 91 | 92 | global_headers = { 93 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:25.0) Gecko/20100101 Firefox/25.0', 94 | } 95 | 96 | self.s = requests.Session() 97 | self.s.headers.update( global_headers ) 98 | 99 | # Internal variables 100 | self.channel_token = "" 101 | self.device_id = "" 102 | self.logic_key = "" 103 | self.imsi = "" 104 | self.id = "" 105 | self.account_id = "" 106 | self.model = "" 107 | self.manufacturer = "" 108 | self.phone_ip = "" 109 | self.phone_port = "" 110 | self.phone_socket_port = "" 111 | self.phone_ssl_port = "" 112 | self.phone_wifi = False 113 | self.var_7bb = "" 114 | 115 | # Extra phone info variables 116 | self.battery_charge = "" 117 | self.is_charging = "" 118 | self.app_count = "" 119 | self.contact_count = "" 120 | self.music_count = "" 121 | self.ebook_count = "" 122 | self.photo_count = "" 123 | self.video_count = "" 124 | self.external_sd_location = "" 125 | self.gsm_bars = "" 126 | self.model = "" 127 | self.orientation = "" 128 | self.os_version = "" 129 | self.external_sd_size = "" 130 | self.external_sd_free = "" 131 | self.storage_size = "" 132 | self.storage_size_free = "" 133 | self.wifi_name = "" 134 | self.wifi_bars = "" 135 | 136 | # Attempt a log in 137 | self.is_loggedin = self.login() 138 | 139 | """ 140 | Login to Airdroid 141 | 142 | return True on success and False on failure 143 | """ 144 | def login( self ): 145 | self.statusmsg( "Logging in to AirDroid..." ) 146 | r = self.s.get('http://web.airdroid.com/', ) 147 | 148 | get_data = { 149 | 'mail': self.username, 150 | 'pwd': self.password, 151 | 'callback': '_jqjsp', 152 | 'keep': '0', 153 | } 154 | 155 | r = self.s.get('https://id.airdroid.com/p9/user/signIn.html', params=get_data) 156 | 157 | info = self.air2json( r.text ) 158 | 159 | if info['msg'] == "success!": 160 | self.initialize_variables( info ) 161 | self.statusmsg( "Login was successful!" ) 162 | self.statusmsg( "Phone address: " + self.phone_ip + ":" + self.phone_port ) 163 | return True 164 | else: 165 | self.errormsg( "Login failed!" ) 166 | return False 167 | 168 | """ 169 | Wakeup the Android device if an action hasn't been preformed in a while 170 | 171 | return True on success and False on failure 172 | """ 173 | def wakeup( self ): 174 | n = datetime.datetime.now() 175 | get_data = { 176 | "id": self.device_id, 177 | "accountId": self.account_id, 178 | "logicKey": self.logic_key, 179 | "callback": "_jqjsp", 180 | "_" + str( int( time.mktime(n.timetuple()) ) ): "", 181 | } 182 | 183 | r = self.s.get( 'http://lb.airdroid.com:9081/wakePhone', params=get_data ) 184 | 185 | response = self.air2json( r.text ) 186 | 187 | if response['msg'] == "wake phone success": 188 | return True 189 | else: 190 | return False 191 | 192 | """ 193 | Get the "bb" value needed to preform Airdroid actions 194 | 195 | return String of bb value 196 | """ 197 | def get_bb( self ): 198 | n = datetime.datetime.now() 199 | unix_time = str( int( time.mktime(n.timetuple()) ) ) 200 | m = md5.new() 201 | m.update( unix_time + self.device_id + self.logic_key ) 202 | key = unix_time + m.digest().encode("hex") 203 | 204 | get_data = { 205 | "keeplogin": 0, 206 | "type": 2, 207 | "key": key, 208 | "7bb": "null", 209 | "callback": "_jqjsp", 210 | "_" + unix_time: "", 211 | } 212 | 213 | r = self.s.get('http://' + self.phone_ip + ':' + self.phone_port + '/sdctl/comm/checklogin/', params=get_data) 214 | 215 | self.var_7bb = self.air2json( r.text )['7bb'] 216 | 217 | """ 218 | Open URL on Android device 219 | 220 | @url String of the URL to open 221 | 222 | return None 223 | """ 224 | def url_open( self, url ): 225 | n = datetime.datetime.now() 226 | unix_time = str( int( time.mktime(n.timetuple()) ) ) 227 | 228 | get_data = { 229 | "url": url, 230 | "7bb": self.var_7bb, 231 | "callback": "_jqjsp", 232 | "_" + unix_time: "", 233 | } 234 | 235 | r = self.s.get('http://' + self.phone_ip + ':' + self.phone_port + '/sdctl/comm/openurl/', params=get_data) 236 | 237 | """ 238 | Why is this still here? 239 | """ 240 | def display_info( self ): 241 | n = datetime.datetime.now() 242 | unix_time = str( int( time.mktime(n.timetuple()) ) ) 243 | 244 | get_data = { 245 | "7bb": self.var_7bb, 246 | "callback": "_jqjsp", 247 | "_" + unix_time: "", 248 | } 249 | 250 | r = self.s.get('http://' + self.phone_ip + ':' + self.phone_port + '/sdctl/device/overview/', params=get_data) 251 | 252 | info = self.air2json( r.text ) 253 | 254 | self.battery_charge = info['battery'] 255 | self.is_charging = info['batterycharging'] 256 | self.app_count = info['counts']['app'] 257 | self.contact_count = info['counts']['contacts'] 258 | self.music_count = info['counts']['music'] 259 | self.ebook_count = info['counts']['ebook'] 260 | self.photo_count = info['counts']['photo'] 261 | self.video_count = info['counts']['video'] 262 | self.external_sd_location = info['ex_sd'] 263 | self.gsm_bars = info['gsm_signal'] 264 | self.model = info['model'] 265 | self.orientation = info['orientation'] 266 | self.os_version = info['osversion'] 267 | self.external_sd_size = info['size']['ext_sd_avail'] 268 | self.external_sd_free = info['size']['ext_sd_size'] 269 | self.storage_size = info['size']['sys_size'] 270 | self.storage_size_free = info['size']['sys_avail'] 271 | self.wifi_name = info['wifi_name'] 272 | self.wifi_bars = info['wifi_signal'] 273 | 274 | """ 275 | Set the Android clipboard 276 | 277 | @data Data to set the clipboard to 278 | 279 | return True on success and False on failure 280 | """ 281 | def set_clipboard( self, data ): 282 | n = datetime.datetime.now() 283 | unix_time = str( int( time.mktime(n.timetuple()) ) ) 284 | 285 | layer_data = { 286 | "content": base64.b64encode( data ) 287 | } 288 | 289 | post_data = { 290 | "content": urllib.quote_plus( json.dumps( layer_data ) ), 291 | } 292 | 293 | r = self.s.post('http://' + self.phone_ip + ':' + self.phone_port + '/sdctl/comm/clipboard/set?7bb=' + self.var_7bb, data=post_data) 294 | 295 | result = json.loads( r.text ) 296 | 297 | if result['result'] == 0: 298 | return True 299 | else: 300 | return False 301 | 302 | """ 303 | Get the Android device's clipboard 304 | 305 | return The clipboard contents 306 | """ 307 | def get_clipboard( self ): 308 | self.statusmsg( "Grabbing clipboard contents..." ) 309 | n = datetime.datetime.now() 310 | unix_time = str( int( time.mktime(n.timetuple()) ) ) 311 | 312 | get_data = { 313 | "7bb": self.var_7bb, 314 | "callback": "_jqjsp", 315 | "_" + unix_time: "", 316 | } 317 | 318 | r = self.s.get('http://' + self.phone_ip + ':' + self.phone_port + '/sdctl/comm/clipboard/get', params=get_data) 319 | 320 | response = self.air2json( r.text ) 321 | data = json.loads( urllib.unquote_plus( base64.b64decode( response['content'] ) ) ) 322 | 323 | self.statusmsg( "Clipboard contents grabbed!" ) 324 | return data['result'] 325 | 326 | """ 327 | Send an SMS from Android phone 328 | 329 | @phone_number Integer of the phone number to send the SMS to 330 | @message String of the SMS message to be sent 331 | 332 | return True on success and False on failure 333 | """ 334 | def sms( self, phone_number, message ): 335 | self.statusmsg( "Sending SMS..." ) 336 | n = datetime.datetime.now() 337 | unix_time = str( int( time.mktime(n.timetuple()) ) ) 338 | 339 | layer1 = { 340 | "number": phone_number, 341 | "content": str( message ), 342 | "threadId": unix_time, 343 | } 344 | 345 | layer2 = { 346 | "content": base64.b64encode( urllib.quote_plus( urllib.quote_plus( json.dumps( layer1 ) ) ) ), 347 | } 348 | 349 | post_data = { 350 | "params": json.dumps( layer2 ), 351 | } 352 | 353 | r = self.s.post('http://' + self.phone_ip + ':' + self.phone_port + '/sdctl/sms/send/single/?7bb=' + self.var_7bb, data=post_data) 354 | response = json.loads( r.text ) 355 | if "threadId" in base64.b64decode( response['content'] ): 356 | self.statusmsg( "SMS sent!" ) 357 | return True 358 | else: 359 | self.errormsg( "Error sending SMS!" ) 360 | return False 361 | 362 | --------------------------------------------------------------------------------