├── LICENSE ├── README.md ├── android-wsbridge ├── .gitignore ├── .gradle │ ├── 4.1 │ │ ├── fileChanges │ │ │ └── last-build.bin │ │ └── fileContent │ │ │ └── fileContent.lock │ └── buildOutputCleanup │ │ └── cache.properties ├── LICENSE ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── dji │ │ │ └── wsbridge │ │ │ ├── BridgeActivity.java │ │ │ ├── SettingsActivity.java │ │ │ └── lib │ │ │ ├── AppBlockCanaryContext.java │ │ │ ├── BridgeApplication.java │ │ │ ├── BridgeToUSBRunner.java │ │ │ ├── BridgeUpdateService.java │ │ │ ├── DJILogger.java │ │ │ ├── DJIPluginRingBufferParser.java │ │ │ ├── NetworkServerInputStream.java │ │ │ ├── NetworkServerOutputStream.java │ │ │ ├── PackBufferObject.java │ │ │ ├── StreamRunner.java │ │ │ ├── Utils.java │ │ │ └── connection │ │ │ ├── ConnectionManager.java │ │ │ ├── USBConnectionManager.java │ │ │ └── WSConnectionManager.java │ │ └── res │ │ ├── drawable-hdpi │ │ ├── icon_dji.png │ │ ├── icon_navgation_bar_2x.png │ │ ├── icon_rc.png │ │ ├── icon_rect.png │ │ ├── icon_tab_bkgnd.png │ │ ├── icon_tab_left_1.png │ │ ├── icon_tab_right.png │ │ └── icon_wifi.png │ │ ├── drawable-ldpi │ │ ├── icon_dji.png │ │ ├── icon_navgation_bar_2x.png │ │ ├── icon_rc.png │ │ ├── icon_rect.png │ │ ├── icon_tab_bkgnd.png │ │ ├── icon_tab_left_1.png │ │ ├── icon_tab_right.png │ │ └── icon_wifi.png │ │ ├── drawable-mdpi │ │ ├── icon_dji.png │ │ ├── icon_navgation_bar_2x.png │ │ ├── icon_rc.png │ │ ├── icon_rect.png │ │ ├── icon_tab_bkgnd.png │ │ ├── icon_tab_left_1.png │ │ ├── icon_tab_right.png │ │ └── icon_wifi.png │ │ ├── layout │ │ ├── activity_bridge.xml │ │ └── activity_settings.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── accessory_filter.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle └── images ├── appLayout.png ├── rcGreen.png ├── rcRed.png ├── rc_purple.png ├── signalGreen.png └── signalRed.png /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2017 DJI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android-Bridge-App 2 | 3 | ## What is this? 4 | 5 | The Android Bridge App allows efficient development of Android applications. 6 | 7 | Many DJI products require the Android device to be connected directly through USB to a remote controller, which means the Android device can only be connected to the PC through WiFi debugging. While workable, development is slow as resource intensive tasks such as profiling or transferring a new build to the mobile device can take a long time. In addition, the Android Studio emulator cannot be used to do any development. 8 | 9 | The Android bridge app runs on an Android device connected to the remote controller and accepts a network connection from another Android device running an SDK based application. It acts as a bridge between the remote controller and the Android device running the SDK based application. 10 | 11 | This means: 12 | 13 | * The Android device running the SDK based application can connect with USB to the PC, while connecting over WiFi to the bridge app 14 | * Or the Android Studio emulator can be used on the PC and connect to the bridge app over WiFi 15 | 16 | This makes it easier develop, debug, setup CI environments, share devices in a team, or even do remote development with devices. 17 | 18 | ## Compatibility 19 | 20 | * The Bridge App is compatible with the Android DJI Mobile SDK v4.0 and above. 21 | * The Bridge App is compatible with all DJI RC 22 | 23 | ## Setup 24 | 25 | When using the bridge app, two Android devices are used (or one device with the bridge app and the emulator): 26 | 27 | 1. An Android device with the BridgeApp apk that is connected directly to the remote controller 28 | 2. An Android device running an SDK based application 29 | 30 | **Both devices** must be able to resolve each other's IP address - and should be **used on the same network**. 31 | Only wi-fi and ethernet connections have been tested to work. 32 | 33 | The steps to make the bridge app work are: 34 | 35 | * Device 1: 36 | 37 | * Start the bridge app 38 | * Connect the Android device to remote controller 39 | * Grant USB access to the bridge app 40 | * Make sure the red light in the top left corner of the screen turns green - this shows the bridge app is connected with the DJI product 41 | * There will be an IP address in the middle of the screen - which is used with the second device. 42 | 43 | * Device 2: 44 | 45 | * Device 2 should be running a DJI Mobile SDK based application. 46 | * The IP address of Device 1 should be passed to `enableBridgeModeWithBridgeAppIP` in `SDKManager` to connect with Device 1. `enableBridgeModeWithBridgeAppIP` must be called after the sdk has registered. 47 | ```java 48 | new DJISDKManager.SDKManagerCallback() { 49 | 50 | @Override 51 | public void onRegister(DJIError error) { 52 | isRegistrationInProgress.set(false); 53 | if (error == DJISDKError.REGISTRATION_SUCCESS) { 54 | // ... 55 | // ADD CALL HERE 56 | DJISDKManager.getInstance().enableBridgeModeWithBridgeAppIP("YOUR IP"); 57 | } else { 58 | // ... 59 | } 60 | } 61 | @Override 62 | public void onProductDisconnect() { 63 | // ... 64 | } 65 | 66 | @Override 67 | public void onProductConnect(BaseProduct product) { 68 | // ... 69 | } 70 | 71 | @Override 72 | public void onComponentChange(BaseProduct.ComponentKey key, 73 | BaseComponent oldComponent, 74 | BaseComponent newComponent) { 75 | // ... 76 | } 77 | }; 78 | ``` 79 | * Make sure the red light in the top left corner of Device 2's screen turns green - this shows Device 2 is connected with Device 1. 80 | * The application can now be run remotely. 81 | 82 | ## App Features 83 | 84 | * Layout: 85 | 86 | ![alt text](./images/appLayout.png) 87 | 88 | * RC indicator: 89 | 90 | * ![alt text](./images/rcRed.png) No USB device connected to BridgeApp 91 | * ![alt text](./images/rc_purple.png) Connected device is not supported or not working properly 92 | * ![alt text](./images/rcGreen.png) DJI RC is connected 93 | 94 | 95 | * Connection indicator 96 | * ![alt text](./images/signalRed.png) No SDK Application is connected to Bridge 97 | * ![alt text](./images/signalGreen.png) There is at least one SDK Application connected to BridgeApp 98 | 99 | * Host IP Address: This is the address of the BridgeApp. To connect to it, pass in the displayed ip value to `enableBridgeModeWithBridgeAppIP` method in `SDKManager`. 100 | 101 | * App Version: Displays the current version of the BridgeApp. This is not the version of the DJI SDK. 102 | 103 | * Functionalities: 104 | 105 | * BridgeApp supports conntection via Wifi or Eithernet ( using USB Adapter and CrystalSky) 106 | * BirdgeApp acceptes multiple simultinous connections. This is helpful to share one Aircraft with many developers. 107 | 108 | ## Feedback 109 | 110 | This is a beta version of the Bridge App. Please provide feedback in areas you think it could be improved or is unstable. 111 | 112 | Please use **Github Issue** or **email** [hai.vo@dji.com](hai.vo@dji.com) when you meet any problems of using this project. At a minimum please let us know: 113 | 114 | * Which DJI Product you are using? 115 | * Which Android Device and Android System version you are using? 116 | * Which Android Studio version you are using? 117 | * A short description of your problem includes debugging logs or screenshots. 118 | * Any bugs or typos you come across. 119 | 120 | ## License 121 | 122 | Android-Bridge-App is available under the MIT license. Please see the LICENSE file for more info. 123 | 124 | ## Join Us 125 | 126 | DJI is looking for all kinds of Software Engineers to continue building the Future of Possible. Available positions in Shenzhen, China and around the world. If you are interested, please send your resume to . For more details, and list of all our global offices, please check . 127 | 128 | DJI 招软件工程师啦,based在深圳,如果你想和我们一起把DJI产品做得更好,请发送简历到 . 详情请浏览 . 129 | -------------------------------------------------------------------------------- /android-wsbridge/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Eclipse project files 27 | .classpath 28 | .project 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/ 42 | .idea/*.xml 43 | .idea/dictionaries 44 | .idea/libraries 45 | .idea/inspectionProfiles 46 | app/*.iml 47 | 48 | 49 | # Keystore files 50 | # Uncomment the following line if you do not want to check your keystore files in. 51 | #*.jks 52 | 53 | # External native build folder generated in Android Studio 2.2 and later 54 | .externalNativeBuild 55 | 56 | # Google Services (e.g. APIs or Firebase) 57 | google-services.json 58 | 59 | # Freeline 60 | freeline.py 61 | freeline/ 62 | freeline_project_description.json 63 | 64 | # Windows thumbnail db 65 | Thumbs.db 66 | 67 | # OSX files 68 | .DS_Store 69 | 70 | .gradle/ 71 | .gradle/buildOutputCleanup 72 | build/ 73 | -------------------------------------------------------------------------------- /android-wsbridge/.gradle/4.1/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android-wsbridge/.gradle/4.1/fileContent/fileContent.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/.gradle/4.1/fileContent/fileContent.lock -------------------------------------------------------------------------------- /android-wsbridge/.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Fri Dec 04 13:27:59 PST 2020 2 | gradle.version=6.5 3 | -------------------------------------------------------------------------------- /android-wsbridge/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2017 DJI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /android-wsbridge/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android-wsbridge/app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | 4 | } 5 | 6 | dependencies { 7 | 8 | } 9 | } 10 | apply plugin: 'com.android.application' 11 | //TODO Add back firebase crashlytics for internal use 12 | //apply plugin: 'com.google.gms.google-services' 13 | //apply plugin: 'com.google.firebase.crashlytics' 14 | 15 | repositories { 16 | 17 | } 18 | 19 | 20 | android { 21 | compileSdkVersion 30 22 | buildToolsVersion '27.0.3' 23 | defaultConfig { 24 | applicationId "com.dji.wsbridge" 25 | minSdkVersion 16 26 | targetSdkVersion 30 27 | versionCode 1 28 | versionName "1.2.6" 29 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 30 | buildConfigField "String", "BASE_URL", getBaseUrl() 31 | } 32 | buildTypes { 33 | // Only used internally by DJI 34 | internal { 35 | initWith buildTypes.debug 36 | } 37 | release { 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | all { 42 | buildConfigField "String", "REMOTE_LOGGER_URL", "\"your_url_goes_here.com\"" 43 | } 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | 50 | implementation 'com.amitshekhar.android:android-networking:0.3.0' 51 | implementation 'org.java-websocket:java-websocket:1.3.3' 52 | implementation fileTree(dir: 'libs', include: ['*.jar']) 53 | implementation 'com.squareup:otto:1.3.8' 54 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' 55 | // Because RxAndroid releases are few and far between, it is recommended you also 56 | // explicitly depend on RxJava's latest version for bug fixes and new features. 57 | implementation 'io.reactivex.rxjava2:rxjava:2.1.12' 58 | implementation 'androidx.annotation:annotation:1.1.0' 59 | implementation 'androidx.appcompat:appcompat:1.2.0' 60 | implementation 'androidx.core:core:1.3.2' 61 | 62 | implementation 'com.github.markzhai:blockcanary-android:1.5.0' 63 | 64 | // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) 65 | implementation platform('com.google.firebase:firebase-bom:26.1.1') 66 | 67 | // Firebase Crashlytics (Java) //TODO Add back Firebase for internal use 68 | //implementation 'com.google.firebase:firebase-crashlytics' 69 | 70 | 71 | } 72 | 73 | /** 74 | * e.g, ./gradlew -PbaseUrl=yoururl.com assembleDebug 75 | * @return return the entered value if there is any, if not empty string 76 | */ 77 | def getBaseUrl() { 78 | def value = project.getProperties().get("baseUrl") 79 | return value != null ? "\"" + value + "\"" : "\"\"" 80 | } 81 | -------------------------------------------------------------------------------- /android-wsbridge/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/dhanush_b/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 32 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/BridgeActivity.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge; 2 | 3 | import android.app.Activity; 4 | import android.app.ActivityManager; 5 | import android.app.AlertDialog; 6 | import android.content.BroadcastReceiver; 7 | import android.content.Context; 8 | import android.content.DialogInterface; 9 | import android.content.Intent; 10 | import android.content.IntentFilter; 11 | import android.content.SharedPreferences; 12 | import android.content.pm.PackageInfo; 13 | import android.content.pm.PackageManager; 14 | import android.graphics.PorterDuff; 15 | import android.hardware.usb.UsbManager; 16 | import android.net.Uri; 17 | import android.os.Build; 18 | import android.os.Bundle; 19 | import android.os.Handler; 20 | import android.preference.PreferenceManager; 21 | import android.util.Log; 22 | import android.view.View; 23 | import android.view.Window; 24 | import android.view.animation.AlphaAnimation; 25 | import android.view.animation.Animation; 26 | import android.view.animation.LinearInterpolator; 27 | import android.widget.ImageButton; 28 | import android.widget.ImageView; 29 | import android.widget.TextView; 30 | import android.widget.Toast; 31 | 32 | import com.dji.wsbridge.lib.BridgeApplication; 33 | import com.dji.wsbridge.lib.BridgeToUSBRunner; 34 | import com.dji.wsbridge.lib.BridgeUpdateService; 35 | import com.dji.wsbridge.lib.DJILogger; 36 | import com.dji.wsbridge.lib.StreamRunner; 37 | import com.dji.wsbridge.lib.Utils; 38 | import com.dji.wsbridge.lib.connection.USBConnectionManager; 39 | import com.dji.wsbridge.lib.connection.WSConnectionManager; 40 | import com.squareup.otto.Subscribe; 41 | 42 | import java.io.InputStream; 43 | import java.io.OutputStream; 44 | import java.net.UnknownHostException; 45 | import java.util.ArrayList; 46 | import java.util.concurrent.TimeUnit; 47 | import java.util.concurrent.atomic.AtomicBoolean; 48 | 49 | import io.reactivex.Observable; 50 | import io.reactivex.android.schedulers.AndroidSchedulers; 51 | import io.reactivex.functions.Consumer; 52 | 53 | import static com.dji.wsbridge.lib.Utils.isInternalVersion; 54 | //import static com.dji.wsbridge.lib.Utils.logToFirebase; 55 | //import static com.dji.wsbridge.lib.Utils.recordExceptionToFirebase; 56 | 57 | 58 | public class BridgeActivity extends Activity { 59 | 60 | public static final String TAG = "AndroidBridge"; 61 | private static final int WEB_SOCKET_PORT = 9007; 62 | private static final Observable HEART_BEAT = Observable.timer(2, TimeUnit.SECONDS).repeat().observeOn(AndroidSchedulers.mainThread()); 63 | public static AtomicBoolean isStarted = new AtomicBoolean(false); 64 | BroadcastReceiver updateAvailableReceiver; 65 | IntentFilter updateAvailableFilter; 66 | SharedPreferences sharedPreferences; 67 | Context ctx; 68 | private Intent bridgeServiceIntent; 69 | private TextView mIPTextView; 70 | private ImageView mRCIconView; 71 | private ImageView mWifiIconView; 72 | private final AtomicBoolean isUSBConnected = new AtomicBoolean(false); 73 | private final AtomicBoolean isRCConnected = new AtomicBoolean(false); 74 | private final AtomicBoolean isWiFiConnected = new AtomicBoolean(false); 75 | private final AtomicBoolean isWSTrafficSlow = new AtomicBoolean(false); 76 | private final AtomicBoolean isStreamRunnerActive = new AtomicBoolean(false); 77 | private InputStream usbInputStream; 78 | private OutputStream usbOutputStream; 79 | private InputStream wsInputStream; 80 | private OutputStream wsOutputStream; 81 | private BridgeToUSBRunner deviceToWSRunner; 82 | private StreamRunner wsToDeviceRunner; 83 | private ImageButton btnSettings, btnInstallUpdate; 84 | private BridgeUpdateService bridgeUpdateService; 85 | 86 | //region -------------------------------------- Activity Callbacks and Helpers --------------------------------------------- 87 | @Override 88 | protected void onCreate(Bundle savedInstanceState) { 89 | super.onCreate(savedInstanceState); 90 | DJILogger.init(); 91 | setupViews(); 92 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(BridgeActivity.this); 93 | setupWSConnectionManager(); 94 | startHeartBeat(); 95 | setupUpdateService(); 96 | } 97 | 98 | private void setupUpdateService() { 99 | if (isInternalVersion()) { 100 | updateAvailableReceiver = new BroadcastReceiver() { 101 | @Override 102 | public void onReceive(Context context, Intent intent) { 103 | btnInstallUpdate.setVisibility(View.VISIBLE); 104 | Animation mAnimation = new AlphaAnimation(1, 0); 105 | mAnimation.setDuration(200); 106 | mAnimation.setInterpolator(new LinearInterpolator()); 107 | mAnimation.setRepeatCount(Animation.INFINITE); 108 | mAnimation.setRepeatMode(Animation.REVERSE); 109 | 110 | btnInstallUpdate.startAnimation(mAnimation); 111 | } 112 | }; 113 | updateAvailableFilter = new IntentFilter(getResources().getString(R.string.intent_filter_update_available)); 114 | 115 | bridgeUpdateService = new BridgeUpdateService(); 116 | bridgeServiceIntent = new Intent(this, bridgeUpdateService.getClass()); 117 | if (!isMyServiceRunning(bridgeUpdateService.getClass())) { 118 | startService(bridgeServiceIntent); 119 | } 120 | } 121 | } 122 | 123 | 124 | @Override 125 | protected void onStart() { 126 | super.onStart(); 127 | refresh(); 128 | DJILogger.v(TAG, "Started"); 129 | isStarted.set(true); 130 | } 131 | 132 | @Override 133 | protected void onStop() { 134 | DJILogger.v(TAG, "Stopped"); 135 | isStarted.set(false); 136 | super.onStop(); 137 | } 138 | 139 | @Override 140 | protected void onResume() { 141 | super.onResume(); 142 | BridgeApplication.getInstance().getBus().register(this); 143 | DJILogger.init(); 144 | USBConnectionManager.getInstance().init(); 145 | if (isInternalVersion()) { 146 | BridgeActivity.this.registerReceiver(updateAvailableReceiver, updateAvailableFilter); 147 | } 148 | DJILogger.v(TAG, "Resumed"); 149 | } 150 | 151 | @Override 152 | protected void onPause() { 153 | DJILogger.v(TAG, "Paused"); 154 | USBConnectionManager.getInstance().destroy(); 155 | stopStreamTransfer(); 156 | BridgeApplication.getInstance().getBus().unregister(this); 157 | 158 | super.onPause(); 159 | 160 | } 161 | 162 | @Override 163 | protected void onDestroy() { 164 | DJILogger.v(TAG, "Destroyed"); 165 | if (isInternalVersion()) { 166 | this.unregisterReceiver(updateAvailableReceiver); 167 | } 168 | super.onDestroy(); 169 | } 170 | 171 | /** 172 | * ACTION_USB_ACCESSORY_ATTACHED is an Activity Broadcast. 173 | * Thus this needs to be here not inside {@link USBConnectionManager} 174 | */ 175 | @Override 176 | protected void onNewIntent(Intent intent) { 177 | if (intent.getAction() != null) 178 | switch (intent.getAction()) { 179 | case UsbManager.ACTION_USB_ACCESSORY_ATTACHED: 180 | BridgeApplication.getInstance().getBus().post(new USBConnectionManager.USBConnectionEvent(true)); 181 | break; 182 | } 183 | super.onNewIntent(intent); 184 | } 185 | 186 | private void setupViews() { 187 | this.requestWindowFeature(Window.FEATURE_NO_TITLE); 188 | // Init all view elements 189 | setContentView(R.layout.activity_bridge); 190 | mIPTextView = (TextView) findViewById(R.id.iptextView); 191 | mRCIconView = (ImageView) findViewById(R.id.imageViewRC); 192 | mWifiIconView = (ImageView) findViewById(R.id.imageViewWifi); 193 | TextView mVersionTextView = (TextView) findViewById(R.id.versionText); 194 | setUpUpdateViews(); 195 | 196 | // Show version number 197 | try { 198 | PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); 199 | String version = pInfo.versionName; 200 | mVersionTextView.setText(version); 201 | } catch (PackageManager.NameNotFoundException e) { 202 | mVersionTextView.setText("N/A"); 203 | } 204 | } 205 | 206 | private void setUpUpdateViews() { 207 | btnSettings = (ImageButton) findViewById(R.id.btnSettings); 208 | btnInstallUpdate = (ImageButton) findViewById(R.id.btnInstallUpdate); 209 | if (isInternalVersion()) { 210 | btnSettings.setVisibility(View.VISIBLE); 211 | btnSettings.setOnClickListener(new View.OnClickListener() { 212 | @Override 213 | public void onClick(View v) { 214 | Intent settingsIntent = new Intent(BridgeActivity.this, SettingsActivity.class); 215 | startActivity(settingsIntent); 216 | 217 | } 218 | }); 219 | btnInstallUpdate.setOnClickListener(new View.OnClickListener() { 220 | @Override 221 | public void onClick(View v) { 222 | AlertDialog.Builder builder; 223 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 224 | builder = new AlertDialog.Builder(BridgeActivity.this, android.R.style.Theme_Material_Dialog_Alert); 225 | } else { 226 | builder = new AlertDialog.Builder(BridgeActivity.this); 227 | } 228 | builder.setTitle("Update avaialble !") 229 | .setMessage("Update is available. Do you want to update the app?") 230 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 231 | public void onClick(DialogInterface dialog, int which) { 232 | btnInstallUpdate.clearAnimation(); 233 | btnInstallUpdate.setVisibility(View.GONE); 234 | Intent install = new Intent(Intent.ACTION_VIEW); 235 | install.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 236 | install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 237 | Uri uri = Uri.parse(sharedPreferences.getString(getResources().getString(R.string.preference_update_uri), "")); 238 | install.setDataAndType(uri, 239 | sharedPreferences.getString(getResources().getString(R.string.preference_update_mimetype), "")); 240 | startActivity(install); 241 | } 242 | }) 243 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 244 | public void onClick(DialogInterface dialog, int which) { 245 | // do nothing 246 | } 247 | }) 248 | .setIcon(android.R.drawable.ic_dialog_alert) 249 | .show(); 250 | 251 | } 252 | }); 253 | } 254 | } 255 | 256 | private void refresh() { 257 | refreshViews(); 258 | refreshRunners(); 259 | } 260 | 261 | private void refreshViews() { 262 | runOnUiThread(new Runnable() { 263 | @Override 264 | public void run() { 265 | mIPTextView.setText(Utils.getIPAddress(true)); 266 | if (isUSBConnected.get()) { 267 | if (isRCConnected.get()) { 268 | mRCIconView.setColorFilter(getResources().getColor(android.R.color.holo_green_light), 269 | PorterDuff.Mode.MULTIPLY); 270 | DJILogger.logConnectionChange(DJILogger.CONNECTION_RC, DJILogger.CONNECTION_GOOD); 271 | } else { 272 | mRCIconView.setColorFilter(getResources().getColor(android.R.color.holo_purple), 273 | android.graphics.PorterDuff.Mode.MULTIPLY); 274 | DJILogger.logConnectionChange(DJILogger.CONNECTION_RC, DJILogger.CONNTECTION_WARNING); 275 | } 276 | } else { 277 | if (isRCConnected.get()) { 278 | mRCIconView.setColorFilter(getResources().getColor(android.R.color.holo_purple), 279 | android.graphics.PorterDuff.Mode.MULTIPLY); 280 | DJILogger.logConnectionChange(DJILogger.CONNECTION_RC, DJILogger.CONNTECTION_WARNING); 281 | } else { 282 | mRCIconView.setColorFilter(getResources().getColor(android.R.color.holo_red_light), 283 | PorterDuff.Mode.MULTIPLY); 284 | DJILogger.logConnectionChange(DJILogger.CONNECTION_RC, DJILogger.CONNECTION_BAD); 285 | } 286 | } 287 | if (isWiFiConnected.get()) { 288 | mWifiIconView.setColorFilter(getResources().getColor(android.R.color.holo_green_light), 289 | PorterDuff.Mode.MULTIPLY); 290 | DJILogger.logConnectionChange(DJILogger.CONNECTION_BRIDGE, DJILogger.CONNECTION_GOOD); 291 | } else { 292 | mWifiIconView.setColorFilter(getResources().getColor(android.R.color.holo_red_light), 293 | PorterDuff.Mode.MULTIPLY); 294 | DJILogger.logConnectionChange(DJILogger.CONNECTION_BRIDGE, DJILogger.CONNECTION_BAD); 295 | 296 | } 297 | } 298 | }); 299 | } 300 | 301 | /** 302 | * This is to make sure the app keeps sending some message every 2 seconds to the server 303 | */ 304 | private void startHeartBeat() { 305 | HEART_BEAT.subscribe(new Consumer() { 306 | @Override 307 | public void accept(Object o) throws Exception { 308 | refreshViews(); 309 | } 310 | }); 311 | } 312 | 313 | private void refreshRunners() { 314 | if (!runnerStateIsIncorrect()) { 315 | return; 316 | } 317 | if (isUSBConnected.get() && isRCConnected.get() && isWiFiConnected.get() && !isStreamRunnerActive.get()) { 318 | if (setupStreams()) { 319 | Log.d(TAG, "Starting Runners"); 320 | isStreamRunnerActive.set(true); 321 | deviceToWSRunner = new BridgeToUSBRunner(wsInputStream, usbOutputStream, "Bridge to USB"); 322 | wsToDeviceRunner = new StreamRunner(usbInputStream, wsOutputStream, "USB to Bridge"); 323 | try { 324 | //import static com.dji.wsbridge.lib.Utils.recordExceptionToFirebase;Firebase("Device to WS Runner alive " + deviceToWSRunner.isAlive()); 325 | //logToFirebase("WS to Device Runner alive " + wsToDeviceRunner.isAlive()); 326 | deviceToWSRunner.start(); 327 | wsToDeviceRunner.start(); 328 | } catch (IllegalThreadStateException exception) { 329 | //recordExceptionToFirebase(exception); 330 | stopStreamTransfer(); 331 | new Handler().postDelayed(new Runnable() { 332 | @Override 333 | public void run() { 334 | refresh(); 335 | } 336 | }, 1500); 337 | } 338 | 339 | } else { 340 | Log.d(TAG, "Stream Transfers NOT started"); 341 | DJILogger.e(TAG, "Stream Transfers NOT started"); 342 | } 343 | } else { 344 | if (isStreamRunnerActive.get() && (!isUSBConnected.get() || !isRCConnected.get()) 345 | || !isWiFiConnected.get()) { 346 | Log.d(TAG, "Stopping Runners"); 347 | stopStreamTransfer(); 348 | } else { 349 | Log.d(TAG, "Nothing is running to stop"); 350 | } 351 | } 352 | } 353 | 354 | /** 355 | * Runner should and should only run when all connections are green 356 | */ 357 | private boolean runnerStateIsIncorrect() { 358 | return areAllConnectionsGreen() && !isStreamRunnerActive.get() 359 | || !areAllConnectionsGreen() && isStreamRunnerActive.get(); 360 | } 361 | 362 | /** 363 | * Check weather all the connections are connected 364 | */ 365 | private boolean areAllConnectionsGreen() { 366 | return isUSBConnected.get() && isRCConnected.get() && isWiFiConnected.get(); 367 | } 368 | 369 | private boolean setupStreams() { 370 | 371 | ArrayList deviceStreams = USBConnectionManager.getInstance().getStreams(); 372 | if (deviceStreams.size() == 2) { 373 | usbInputStream = (InputStream) deviceStreams.get(0); 374 | usbOutputStream = (OutputStream) deviceStreams.get(1); 375 | } else { 376 | Log.d(TAG, "USB Streams not available"); 377 | DJILogger.e(TAG, "USB Streams not available"); 378 | return false; 379 | } 380 | 381 | ArrayList wsStreams = WSConnectionManager.getInstance().getStreams(); 382 | if (wsStreams.size() == 2) { 383 | wsInputStream = (InputStream) wsStreams.get(0); 384 | wsOutputStream = (OutputStream) wsStreams.get(1); 385 | } else { 386 | Log.d(TAG, "WS Streams not available"); 387 | DJILogger.e(TAG, "WS Streams not available"); 388 | return false; 389 | } 390 | 391 | return true; 392 | } 393 | 394 | private void stopStreamTransfer() { 395 | if (wsToDeviceRunner != null) { 396 | wsToDeviceRunner.cleanup(); 397 | wsToDeviceRunner = null; 398 | } 399 | if (deviceToWSRunner != null) { 400 | deviceToWSRunner.cleanup(); 401 | deviceToWSRunner = null; 402 | } 403 | isStreamRunnerActive.set(false); 404 | 405 | USBConnectionManager.getInstance().closeStreams(); 406 | WSConnectionManager.getInstance().closeStreams(); 407 | } 408 | //endregion ----------------------------------------------------------------------------------------------------- 409 | 410 | //region -------------------------------------- USB Helper Methods --------------------------------------------- 411 | @Subscribe 412 | public void onUSBConnectionEvent(USBConnectionManager.USBConnectionEvent event) { 413 | if (isUSBConnected.compareAndSet(!event.isConnected(), event.isConnected())) { 414 | refresh(); 415 | showToast("", isUSBConnected.get(), "USB"); 416 | } 417 | } 418 | 419 | @Subscribe 420 | public void onRCConnectionEvent(USBConnectionManager.RCConnectionEvent event) { 421 | if (isRCConnected.compareAndSet(!event.isConnected(), event.isConnected())) { 422 | refresh(); 423 | showToast("", isRCConnected.get(), "RC"); 424 | } else { 425 | // In some this event comes after runners have been already started so we need to restart runner to make bridge work 426 | refreshRunners(); 427 | } 428 | } 429 | //endregion ----------------------------------------------------------------------------------------------------- 430 | 431 | //region -------------------------------------- WS Server Helper Methods ------------------------------------------ 432 | private void setupWSConnectionManager() { 433 | try { 434 | WSConnectionManager.setupInstance(WEB_SOCKET_PORT); 435 | } catch (UnknownHostException e) { 436 | //recordExceptionToFirebase(e); 437 | } 438 | } 439 | 440 | @Subscribe 441 | public void onWSConnectionEvent(WSConnectionManager.WSConnectionEvent event) { 442 | boolean shouldRefresh = false; 443 | if (event.getActiveConnectionCount() > 0) { 444 | if (isWiFiConnected.compareAndSet(false, true)) { 445 | shouldRefresh = true; 446 | } 447 | } else { 448 | if (isWiFiConnected.compareAndSet(true, false)) { 449 | shouldRefresh = true; 450 | } 451 | } 452 | if (shouldRefresh) { 453 | refresh(); 454 | } 455 | showToast("Network ", isWiFiConnected.get(), event.getMessage()); 456 | } 457 | 458 | @Subscribe 459 | public void onWSTrafficEvent(WSConnectionManager.WSTrafficEvent event) { 460 | 461 | boolean shouldRefresh = false; 462 | if (event.isSlowConnection()) { 463 | if (isWSTrafficSlow.compareAndSet(false, true)) { 464 | shouldRefresh = true; 465 | showToast("Bad Network Connection: " + event.getMessage() + "!", isWiFiConnected.get(), event.getMessage()); 466 | } 467 | } else { 468 | if (isWSTrafficSlow.compareAndSet(true, false)) { 469 | shouldRefresh = true; 470 | } 471 | } 472 | if (shouldRefresh) { 473 | refresh(); 474 | } 475 | } 476 | //endregion ----------------------------------------------------------------------------------------------------- 477 | 478 | //region Internal Utility Methods 479 | private void showToast(String connection, boolean isConnected, String message) { 480 | 481 | final String finalMessage = 482 | isConnected ? connection + "Connected to " + message : connection + "Disconnected from " + message; 483 | 484 | runOnUiThread(new Runnable() { 485 | @Override 486 | public void run() { 487 | Toast.makeText(getApplicationContext(), finalMessage, Toast.LENGTH_SHORT).show(); 488 | DJILogger.i(TAG, finalMessage); 489 | } 490 | }); 491 | } 492 | 493 | private boolean isMyServiceRunning(Class serviceClass) { 494 | ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); 495 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 496 | if (serviceClass.getName().equals(service.service.getClassName())) { 497 | Log.i("isMyServiceRunning?", true + ""); 498 | return true; 499 | } 500 | } 501 | Log.i("isMyServiceRunning?", false + ""); 502 | return false; 503 | } 504 | 505 | //endregion 506 | } 507 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge; 2 | 3 | import android.app.Activity; 4 | import android.app.AlarmManager; 5 | import android.app.DownloadManager; 6 | import android.app.PendingIntent; 7 | import android.content.BroadcastReceiver; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.IntentFilter; 11 | import android.content.SharedPreferences; 12 | import android.net.Uri; 13 | import android.os.Bundle; 14 | import android.os.Environment; 15 | import android.preference.PreferenceManager; 16 | import android.util.Log; 17 | import android.view.View; 18 | import android.widget.Button; 19 | import android.widget.CompoundButton; 20 | import android.widget.Switch; 21 | import android.widget.TextView; 22 | import android.widget.Toast; 23 | 24 | import com.androidnetworking.AndroidNetworking; 25 | import com.androidnetworking.common.Priority; 26 | import com.androidnetworking.error.ANError; 27 | import com.androidnetworking.interfaces.StringRequestListener; 28 | import com.dji.wsbridge.lib.BridgeUpdateService; 29 | import com.dji.wsbridge.lib.Utils; 30 | 31 | import org.json.JSONObject; 32 | 33 | import java.io.File; 34 | 35 | public class SettingsActivity extends Activity { 36 | SharedPreferences sharedPreferences; 37 | private Switch switchAutoUpdate; 38 | private Button buttonCheckUpdate; 39 | private Button buttonClose; 40 | private TextView textViewHint; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_settings); 46 | initView(); 47 | } 48 | 49 | private void initView() { 50 | switchAutoUpdate = (Switch) findViewById(R.id.switchAutoUpdate); 51 | buttonCheckUpdate = (Button) findViewById(R.id.btnCheckUpdate); 52 | buttonClose = (Button) findViewById(R.id.btnClose); 53 | 54 | textViewHint = (TextView) findViewById(R.id.tvHint); 55 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(SettingsActivity.this); 56 | if (Utils.isRooted()) { 57 | switchAutoUpdate.setEnabled(true); 58 | 59 | if (sharedPreferences.contains(getResources().getString(R.string.preference_auto_update_key))) { 60 | switchAutoUpdate.setChecked(sharedPreferences.getBoolean(getResources().getString(R.string.preference_auto_update_key), false)); 61 | if (sharedPreferences.getBoolean(getResources().getString(R.string.preference_auto_update_key), false)) { 62 | textViewHint.setVisibility(View.VISIBLE); 63 | } else { 64 | textViewHint.setVisibility(View.GONE); 65 | } 66 | } 67 | } else { 68 | switchAutoUpdate.setEnabled(false); 69 | textViewHint.setText(getResources().getString(R.string.auto_update_msg_root)); 70 | textViewHint.setVisibility(View.VISIBLE); 71 | } 72 | 73 | 74 | switchAutoUpdate.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 75 | @Override 76 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 77 | String msg = isChecked ? "Auto update switced on." : "Auto update switced off."; 78 | if (isChecked) { 79 | textViewHint.setVisibility(View.VISIBLE); 80 | } else { 81 | textViewHint.setVisibility(View.GONE); 82 | } 83 | 84 | Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_SHORT).show(); 85 | sharedPreferences.edit().putBoolean(getResources().getString(R.string.preference_auto_update_key), isChecked).commit(); 86 | } 87 | }); 88 | buttonClose.setOnClickListener(new View.OnClickListener() { 89 | @Override 90 | public void onClick(View v) { 91 | SettingsActivity.this.finish(); 92 | } 93 | }); 94 | buttonCheckUpdate.setOnClickListener(new View.OnClickListener() { 95 | @Override 96 | public void onClick(View v) { 97 | AndroidNetworking.post(BridgeUpdateService.VERSION_CHECK_URL).setPriority(Priority.IMMEDIATE).build().getAsString(new StringRequestListener() { 98 | @Override 99 | public void onResponse(String response) { 100 | try { 101 | JSONObject jsonObject = new JSONObject(response); 102 | if (jsonObject != null) { 103 | String versionName = getApplicationContext().getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), 0).versionName; 104 | if (versionName != null) { 105 | 106 | if (Utils.compareVersionNames(versionName, jsonObject.getString("version")) == -1) { 107 | updateApp(); 108 | } else { 109 | Toast.makeText(getApplicationContext(), "Application is already up to date.", Toast.LENGTH_LONG).show(); 110 | 111 | } 112 | } 113 | } 114 | } catch (Exception e) { 115 | e.printStackTrace(); 116 | } 117 | 118 | } 119 | 120 | @Override 121 | public void onError(ANError anError) { 122 | Log.d("Sid", "error " + anError.getErrorCode() + anError.getErrorDetail()); 123 | } 124 | 125 | }); 126 | 127 | 128 | } 129 | }); 130 | } 131 | 132 | 133 | private void updateApp() { 134 | 135 | String fileName = "bridgeapp.apk"; 136 | final String destination = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/" + fileName; 137 | final Uri uri = Uri.parse("file://" + destination); 138 | 139 | //Delete update file if exists 140 | File file = new File(destination); 141 | if (file.exists()) 142 | file.delete(); 143 | 144 | 145 | //set downloadmanager 146 | DownloadManager.Request request = new DownloadManager.Request(Uri.parse(BridgeUpdateService.UPDATED_APP_URL)); 147 | request.setDescription("Updating BridgeApp"); 148 | request.setTitle("Update"); 149 | 150 | //set destination 151 | request.setDestinationUri(uri); 152 | 153 | // get download service and enqueue file 154 | final DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); 155 | final long downloadId = manager.enqueue(request); 156 | 157 | //set BroadcastReceiver to install app when .apk is downloaded 158 | BroadcastReceiver onComplete = new BroadcastReceiver() { 159 | public void onReceive(Context ctxt, Intent intent) { 160 | if (sharedPreferences.contains(getResources().getString(R.string.preference_auto_update_key)) 161 | && sharedPreferences.getBoolean(getResources().getString(R.string.preference_auto_update_key), false)) { 162 | try { 163 | Intent afterUpdateIntent = new Intent(getApplicationContext(), BridgeActivity.class); 164 | intent.setAction(Intent.ACTION_MAIN); 165 | intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); 166 | PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0, afterUpdateIntent, 0); 167 | 168 | final long DELAY_IN_MILLIS = 1000 * 2 + System.currentTimeMillis(); 169 | AlarmManager alarmManager = (AlarmManager) 170 | getSystemService(Activity.ALARM_SERVICE); 171 | alarmManager.set(AlarmManager.RTC, DELAY_IN_MILLIS, pi); 172 | String command; 173 | command = "pm install -r " + destination; 174 | Process proc = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); 175 | proc.waitFor(); 176 | pi.send(); 177 | 178 | } catch (Exception e) { 179 | e.printStackTrace(); 180 | } 181 | 182 | 183 | } else { 184 | sharedPreferences.edit().putString(getResources().getString(R.string.preference_update_uri), uri.toString()).commit(); 185 | sharedPreferences.edit().putString(getResources().getString(R.string.preference_update_mimetype), manager.getMimeTypeForDownloadedFile(downloadId)).commit(); 186 | Intent updateAvailableIntent = new Intent(getResources().getString(R.string.intent_filter_update_available)); 187 | sendBroadcast(updateAvailableIntent); 188 | SettingsActivity.this.finish(); 189 | 190 | } 191 | 192 | unregisterReceiver(this); 193 | 194 | } 195 | }; 196 | //register receiver for when .apk download is compete 197 | registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/AppBlockCanaryContext.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import android.content.pm.PackageInfo; 4 | import android.content.pm.PackageManager; 5 | import android.util.Log; 6 | import com.github.moduth.blockcanary.BlockCanaryContext; 7 | 8 | /** 9 | * Configuration class for BlockCanary 10 | */ 11 | 12 | public class AppBlockCanaryContext extends BlockCanaryContext { 13 | 14 | private static final String TAG = AppBlockCanaryContext.class.getSimpleName(); 15 | 16 | @Override 17 | public String provideQualifier() { 18 | String qualifier = ""; 19 | try { 20 | PackageInfo info = BridgeApplication.getInstance() 21 | .getPackageManager() 22 | .getPackageInfo(BridgeApplication.getInstance().getPackageName(), 0); 23 | qualifier += info.versionCode + "_" + info.versionName + "_YYB"; 24 | } catch (PackageManager.NameNotFoundException e) { 25 | Log.e(TAG, "provideQualifier exception", e); 26 | } 27 | return qualifier; 28 | } 29 | 30 | /** 31 | * Random number to identify our app 32 | */ 33 | @Override 34 | public String provideUid() { 35 | return "10112017"; 36 | } 37 | 38 | /** 39 | * We mostly run sample app in test devices, 40 | * which only have wifi connection 41 | */ 42 | @Override 43 | public String provideNetworkType() { 44 | return "wifi"; 45 | } 46 | 47 | /** 48 | * As long as possible 49 | */ 50 | @Override 51 | public int provideMonitorDuration() { 52 | return Integer.MAX_VALUE; 53 | } 54 | 55 | /** 56 | * Ideally this should be 16 milliseconds 57 | * to guarantee 60 fps but we are far away 58 | * from there 59 | */ 60 | @Override 61 | public int provideBlockThreshold() { 62 | return 500; 63 | } 64 | 65 | /** 66 | * Turn off notification to not interrupt QA 67 | */ 68 | @Override 69 | public boolean displayNotification() { 70 | return false; 71 | } 72 | 73 | /** 74 | * Path to save log file 75 | */ 76 | @Override 77 | public String providePath() { 78 | return "/DJI/blocks/"; 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/BridgeApplication.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | import android.os.StrictMode; 7 | 8 | import com.dji.wsbridge.BuildConfig; 9 | //import com.google.firebase.FirebaseApp; 10 | import com.squareup.otto.Bus; 11 | import com.squareup.otto.ThreadEnforcer; 12 | 13 | 14 | 15 | public class BridgeApplication extends Application implements Application.ActivityLifecycleCallbacks { 16 | 17 | public static final String TAG = "APPLICATION"; 18 | private static BridgeApplication instance; 19 | private final Bus eventBus = new Bus(ThreadEnforcer.ANY); 20 | 21 | public static BridgeApplication getInstance() { 22 | return instance; 23 | } 24 | 25 | @Override 26 | public void onCreate() { 27 | super.onCreate(); 28 | // if (isInternalVersion()) { 29 | // FirebaseApp.initializeApp(getApplicationContext()); 30 | // } 31 | DJILogger.init(); 32 | instance = this; 33 | registerActivityLifecycleCallbacks(this); 34 | if (BuildConfig.DEBUG) { 35 | // Detect thread violation 36 | StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyDropBox().penaltyLog().build()); 37 | StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll() 38 | .penaltyDropBox() 39 | .penaltyLog() 40 | .build()); 41 | } 42 | } 43 | 44 | public Bus getBus() { 45 | return this.eventBus; 46 | } 47 | 48 | //region -------------------------------------- Activity Callbacks and Helpers --------------------------------------------- 49 | @Override 50 | public void onActivityCreated(Activity activity, Bundle bundle) { 51 | DJILogger.v(TAG, activity.getLocalClassName() + " Created"); 52 | } 53 | 54 | @Override 55 | public void onActivityResumed(Activity activity) { 56 | DJILogger.v(TAG, activity.getLocalClassName() + " Resumed"); 57 | } 58 | 59 | @Override 60 | public void onActivityDestroyed(Activity activity) { 61 | DJILogger.v(TAG, activity.getLocalClassName() + " Destroyed"); 62 | } 63 | 64 | @Override 65 | public void onActivityPaused(Activity activity) { 66 | DJILogger.v(TAG, activity.getLocalClassName() + " Paused"); 67 | } 68 | 69 | @Override 70 | public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { 71 | DJILogger.v(TAG, activity.getLocalClassName() + " SaveInstance"); 72 | } 73 | 74 | @Override 75 | public void onActivityStarted(Activity activity) { 76 | DJILogger.v(TAG, activity.getLocalClassName() + " Started"); 77 | } 78 | 79 | @Override 80 | public void onActivityStopped(Activity activity) { 81 | DJILogger.v(TAG, activity.getLocalClassName() + " Stopped"); 82 | } 83 | //endregion ----------------------------------------------------------------------------------------------------- 84 | } 85 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/BridgeToUSBRunner.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import android.util.Log; 4 | import com.dji.wsbridge.lib.connection.USBConnectionManager; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | public class BridgeToUSBRunner extends Thread { 12 | 13 | private static final String TAG = StreamRunner.class.getSimpleName(); 14 | public long byteCount = 0; 15 | // 从web-socket 读 16 | private InputStream mInputStream; 17 | // 往usb中写 18 | private OutputStream mOutputStream; 19 | private AtomicBoolean mStop = new AtomicBoolean(false); 20 | private DJIPluginRingBufferParser parser; 21 | 22 | public BridgeToUSBRunner(InputStream in, OutputStream out, String name) { 23 | super(name); 24 | mInputStream = in; 25 | mOutputStream = out; 26 | parser = new DJIPluginRingBufferParser(100 * 1024, mOutputStream); 27 | } 28 | 29 | @Override 30 | public void run() { 31 | int ret; 32 | USBConnectionManager.UsbModel usbModel = USBConnectionManager.getInstance().getUSBModel(); 33 | // As explained in: 34 | // https://developer.android.com/guide/topics/connectivity/usb/accessory.html 35 | // "The Android accessory protocol supports packet buffers up to 16384 bytes, 36 | // so you can choose to always declare your buffer to be of this size for simplicity." 37 | byte[] buffer = new byte[16384]; 38 | 39 | while (!mStop.get()) { 40 | try { 41 | if (mOutputStream != null && mInputStream != null) { 42 | ret = mInputStream.read(buffer); 43 | if (ret < 0) { 44 | // Do nothing since it is empty 45 | Log.d(TAG, getName() + ": ret is less than 0"); 46 | break; 47 | } else { 48 | Log.d(TAG, getName() + ": Runner is running"); 49 | if (usbModel != USBConnectionManager.UsbModel.LOGIC_LINK) { 50 | byteCount += ret; 51 | mOutputStream.write(buffer, 0, ret); 52 | } else { 53 | // parser.parse(buffer, 0, ret); 54 | 55 | 56 | int v1RealDataLength = ret + 8; 57 | byte[] box_head = new byte[v1RealDataLength]; 58 | int it = 0; 59 | 60 | box_head[it] = LOGIC_LINK_HEADER_0X55;it++; 61 | box_head[it] = LOGIC_LINK_HEADER_0XCC;it++; 62 | 63 | box_head[it] = (byte) (CHANNEL_CMD & 0xff);it++; 64 | box_head[it] = (byte) ((CHANNEL_CMD & 0xff00) >> 8);it++; 65 | 66 | box_head[it] = (byte) (ret & 0xff);it++; 67 | box_head[it] = (byte) ((ret & 0xff00) >> 8);it++; 68 | box_head[it] = (byte) ((ret & 0xff0000) >> 16);it++; 69 | box_head[it] = (byte) ((ret & 0xff000000) >> 24);it++; 70 | 71 | System.arraycopy(buffer, 0, box_head, 8, ret); 72 | byteCount += ret; 73 | try { 74 | mOutputStream.write(box_head, 0, v1RealDataLength); 75 | // mOutputStream.flush(); 76 | } catch (IOException e) { 77 | e.printStackTrace(); 78 | } 79 | 80 | } 81 | mOutputStream.flush(); 82 | } 83 | } 84 | } catch (Exception e) { 85 | //Crashlytics.logException(e); 86 | //e.printStackTrace(); 87 | //Log.e(TAG, e.getMessage()); 88 | } 89 | } 90 | Log.d(TAG, getName() + ": Runner is stopped"); 91 | } 92 | 93 | public void cleanup() { 94 | mStop.set(true); 95 | } 96 | 97 | public static final int CHANNEL_CMD = 22345; 98 | 99 | static final byte LOGIC_LINK_HEADER_0X55 = Utils.getByte(0x55); 100 | 101 | static final byte LOGIC_LINK_HEADER_0XCC = Utils.getByte(0xCC); 102 | } 103 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/BridgeUpdateService.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import android.app.Activity; 4 | import android.app.AlarmManager; 5 | import android.app.DownloadManager; 6 | import android.app.PendingIntent; 7 | import android.app.Service; 8 | import android.content.BroadcastReceiver; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.content.IntentFilter; 12 | import android.content.SharedPreferences; 13 | import android.net.Uri; 14 | import android.os.Environment; 15 | import android.os.Handler; 16 | import android.os.IBinder; 17 | import android.preference.PreferenceManager; 18 | import android.util.Log; 19 | 20 | import androidx.annotation.Nullable; 21 | 22 | import com.androidnetworking.AndroidNetworking; 23 | import com.androidnetworking.error.ANError; 24 | import com.androidnetworking.interfaces.StringRequestListener; 25 | import com.dji.wsbridge.BridgeActivity; 26 | import com.dji.wsbridge.BuildConfig; 27 | import com.dji.wsbridge.R; 28 | 29 | import org.json.JSONObject; 30 | 31 | import java.io.File; 32 | import java.util.Timer; 33 | import java.util.TimerTask; 34 | 35 | /** 36 | * Created by sidd on 3/20/18. 37 | */ 38 | 39 | public class BridgeUpdateService extends Service { 40 | 41 | public static final String UPDATED_APP_URL = BuildConfig.BASE_URL + "/bridgeapp.apk"; 42 | public static final String VERSION_CHECK_URL = BuildConfig.BASE_URL + "/getBridgeVersion.php"; 43 | 44 | //get url of app on server 45 | private static final String SYSTEM_PACKAGE_NAME = "android"; 46 | private int counter = 0; 47 | private Handler myHandler; 48 | private Timer timer; 49 | private TimerTask timerTask; 50 | 51 | SharedPreferences sharedPreferences; 52 | 53 | public BridgeUpdateService() { 54 | } 55 | 56 | @Nullable 57 | @Override 58 | public IBinder onBind(Intent intent) { 59 | return null; 60 | } 61 | 62 | @Override 63 | public int onStartCommand(Intent intent, int flags, int startId) { 64 | super.onStartCommand(intent, flags, startId); 65 | myHandler = new Handler(); 66 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 67 | startTimer(); 68 | return START_STICKY; 69 | } 70 | 71 | @Override 72 | public void onCreate() { 73 | super.onCreate(); 74 | } 75 | 76 | @Override 77 | public void onDestroy() { 78 | super.onDestroy(); 79 | } 80 | 81 | public void startTimer() { 82 | //set a new Timer 83 | timer = new Timer(); 84 | 85 | //initialize the TimerTask's job 86 | initializeTimerTask(); 87 | 88 | //schedule the timer, to wake up every 1 HOUR 89 | timer.schedule(timerTask, 0, 60 * 60 * 1000); // 90 | } 91 | 92 | /** 93 | * it sets the timer to print the counter every x seconds 94 | */ 95 | public void initializeTimerTask() { 96 | timerTask = new TimerTask() { 97 | public void run() { 98 | myHandler.post(new checkVersionRunnable()); 99 | } 100 | }; 101 | } 102 | 103 | /** 104 | * not needed 105 | */ 106 | public void stoptimertask() { 107 | //stop the timer, if it's not already null 108 | if (timer != null) { 109 | timer.cancel(); 110 | timer = null; 111 | } 112 | } 113 | 114 | private void updateApp() { 115 | 116 | String fileName = "bridgeapp.apk"; 117 | final String destination = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/" + fileName; 118 | final Uri uri = Uri.parse("file://" + destination); 119 | 120 | //Delete update file if exists 121 | File file = new File(destination); 122 | if (file.exists()) 123 | file.delete(); 124 | 125 | 126 | //set downloadmanager 127 | DownloadManager.Request request = new DownloadManager.Request(Uri.parse(UPDATED_APP_URL)); 128 | request.setDescription("Updating BridgeApp"); 129 | request.setTitle("Update"); 130 | 131 | //set destination 132 | request.setDestinationUri(uri); 133 | 134 | // get download service and enqueue file 135 | final DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); 136 | final long downloadId = manager.enqueue(request); 137 | 138 | //set BroadcastReceiver to install app when .apk is downloaded 139 | BroadcastReceiver onComplete = new BroadcastReceiver() { 140 | public void onReceive(Context ctxt, Intent intent) { 141 | if (sharedPreferences.contains(getResources().getString(R.string.preference_auto_update_key)) 142 | && sharedPreferences.getBoolean(getResources().getString(R.string.preference_auto_update_key), false)) { 143 | try { 144 | Intent afterUpdateIntent = new Intent(getApplicationContext(), BridgeActivity.class); 145 | intent.setAction(Intent.ACTION_MAIN); 146 | intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); 147 | PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0, afterUpdateIntent, 0); 148 | 149 | final long DELAY_IN_MILLIS = 1000 * 2 + System.currentTimeMillis(); 150 | AlarmManager alarmManager = (AlarmManager) 151 | getSystemService(Activity.ALARM_SERVICE); 152 | alarmManager.set(AlarmManager.RTC, DELAY_IN_MILLIS, pi); 153 | String command; 154 | command = "pm install -r " + destination; 155 | Process proc = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); 156 | proc.waitFor(); 157 | pi.send(); 158 | 159 | } catch (Exception e) { 160 | e.printStackTrace(); 161 | } 162 | 163 | 164 | } else { 165 | sharedPreferences.edit().putString(getResources().getString(R.string.preference_update_uri), uri.toString()).commit(); 166 | sharedPreferences.edit().putString(getResources().getString(R.string.preference_update_mimetype), manager.getMimeTypeForDownloadedFile(downloadId)).commit(); 167 | Intent updateAvailableIntent = new Intent(getResources().getString(R.string.intent_filter_update_available)); 168 | sendBroadcast(updateAvailableIntent); 169 | 170 | } 171 | 172 | unregisterReceiver(this); 173 | 174 | } 175 | }; 176 | //register receiver for when .apk download is compete 177 | registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); 178 | } 179 | 180 | private class checkVersionRunnable implements Runnable { 181 | 182 | @Override 183 | public void run() { 184 | AndroidNetworking.post(VERSION_CHECK_URL).build().getAsString(new StringRequestListener() { 185 | @Override 186 | public void onResponse(String response) { 187 | 188 | try { 189 | JSONObject jsonObject = new JSONObject(response); 190 | if (jsonObject != null) { 191 | String versionName = getApplicationContext().getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), 0).versionName; 192 | if (versionName != null) { 193 | if (Utils.compareVersionNames(versionName, jsonObject.getString("version")) == -1) { 194 | Runtime.getRuntime().exec("dpm set-device-owner com.dji.wsbridge/.DeviceAdminRcvr"); 195 | updateApp(); 196 | } 197 | } 198 | } 199 | } catch (Exception e) { 200 | e.printStackTrace(); 201 | } 202 | 203 | } 204 | 205 | @Override 206 | public void onError(ANError anError) { 207 | Log.d("BridgeUpdateService", "error " + anError.getErrorCode() + anError.getErrorDetail()); 208 | 209 | } 210 | 211 | }); 212 | 213 | } 214 | } 215 | 216 | } 217 | 218 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/DJILogger.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.IntDef; 7 | import androidx.annotation.StringDef; 8 | 9 | import com.androidnetworking.AndroidNetworking; 10 | import com.androidnetworking.error.ANError; 11 | import com.androidnetworking.interfaces.OkHttpResponseListener; 12 | import com.dji.wsbridge.BuildConfig; 13 | 14 | import org.json.JSONException; 15 | import org.json.JSONObject; 16 | 17 | import java.lang.annotation.Retention; 18 | import java.lang.annotation.RetentionPolicy; 19 | import java.security.MessageDigest; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.util.ArrayList; 22 | import java.util.Collections; 23 | import java.util.List; 24 | 25 | import okhttp3.Response; 26 | 27 | //import static com.dji.wsbridge.lib.Utils.recordExceptionToFirebase; 28 | 29 | public class DJILogger extends Thread { 30 | 31 | public static final String CONNECTION_GOOD = "good"; 32 | public static final String CONNECTION_BAD = "bad"; 33 | public static final String CONNTECTION_WARNING = "warning"; 34 | public static final int CONNECTION_RC = 1; 35 | public static final int CONNECTION_BRIDGE = 2; 36 | private static final String TAG = "RemoteLogger"; 37 | // Add your remote server IP 38 | private static final String REMOTE_LOGGER_URL = BuildConfig.BASE_URL + "/updateBridgeStatus.php"; 39 | private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); 40 | private static DJILogger instance = new DJILogger(); 41 | private String serverURL; 42 | private String deviceID; 43 | private boolean isEnabled = false; 44 | private boolean isNetworkEnabled = true; 45 | private List messageQueue; 46 | 47 | private DJILogger() { 48 | messageQueue = Collections.synchronizedList(new ArrayList()); 49 | this.start(); 50 | } 51 | 52 | 53 | //region Public APIs 54 | public static void v(String tag, String message) { 55 | if (instance.isEnabled) { 56 | if (instance.isNetworkEnabled) { 57 | instance.sendMessage("verbose", tag + ": " + message); 58 | } 59 | Log.e(tag, message); 60 | } 61 | } 62 | 63 | public static void i(String tag, String message) { 64 | if (instance.isEnabled) { 65 | if (instance.isNetworkEnabled) { 66 | instance.sendMessage("info", tag + ": " + message); 67 | } 68 | Log.e(tag, message); 69 | } 70 | } 71 | 72 | public static void d(String tag, String message) { 73 | if (instance.isEnabled) { 74 | if (instance.isNetworkEnabled) { 75 | instance.sendMessage("debug", tag + ": " + message); 76 | } 77 | Log.e(tag, message); 78 | } 79 | } 80 | 81 | public static void w(String tag, String message) { 82 | if (instance.isEnabled) { 83 | if (instance.isNetworkEnabled) { 84 | instance.sendMessage("warn", tag + ": " + message); 85 | } 86 | Log.e(tag, message); 87 | } 88 | } 89 | 90 | public static void e(String tag, String message) { 91 | if (instance.isEnabled) { 92 | if (instance.isNetworkEnabled) { 93 | instance.sendMessage("error", tag + ": " + message); 94 | } 95 | Log.e(tag, message); 96 | } 97 | } 98 | 99 | public static void logConnectionChange(@ConnetionTypeParam int connectionType, @ConnetionValueParam String value) { 100 | if (instance.isEnabled) { 101 | if (instance.isNetworkEnabled) { 102 | instance.sendMessage(String.valueOf(connectionType), value); 103 | } 104 | Log.d(TAG, "Connection change for type: " + connectionType + " new value: " + value); 105 | } 106 | } 107 | 108 | 109 | public static void init() { 110 | DJILogger.setServerURL(REMOTE_LOGGER_URL); 111 | final String ip = Utils.getIPAddress(true); 112 | // Set the last group of IP address to be DeviceID 113 | String deviceID = ""; 114 | if (!TextUtils.isEmpty(ip)) { 115 | final String[] split = ip.split("\\."); 116 | if (split.length > 0) { 117 | deviceID = split[split.length - 1]; 118 | } 119 | DJILogger.setDeviceID(deviceID, false); 120 | DJILogger.setEnabled(true); 121 | } 122 | } 123 | //endregion 124 | 125 | //region Helper Methods 126 | private static void setServerURL(String serverURL) { 127 | instance.serverURL = serverURL; 128 | } 129 | 130 | private static void setDeviceID(String deviceID, boolean forceUpdate) { 131 | if (TextUtils.isEmpty(instance.deviceID) || forceUpdate) { 132 | instance.deviceID = deviceID; 133 | } 134 | } 135 | 136 | private static void setEnabled(boolean isEnabled) { 137 | instance.isEnabled = isEnabled; 138 | } 139 | 140 | private static String bytesToHex(byte[] bytes) { 141 | char[] hexChars = new char[bytes.length * 2]; 142 | for (int j = 0; j < bytes.length; j++) { 143 | int v = bytes[j] & 0xFF; 144 | hexChars[j * 2] = hexArray[v >>> 4]; 145 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; 146 | } 147 | return new String(hexChars); 148 | } 149 | 150 | public static String sha1Hash(byte[] bytes) { 151 | String hash = null; 152 | try { 153 | MessageDigest digest = MessageDigest.getInstance("SHA-1"); 154 | digest.update(bytes, 0, bytes.length); 155 | bytes = digest.digest(); 156 | hash = bytesToHex(bytes); 157 | } catch (NoSuchAlgorithmException e) { 158 | e.printStackTrace(); 159 | } 160 | return hash; 161 | } 162 | 163 | private void sendMessage(String LogLevel, String message) { 164 | 165 | JSONObject object = new JSONObject(); 166 | try { 167 | object.put("device_id", deviceID); 168 | object.put("log_level", LogLevel); 169 | object.put("message", message); 170 | object.put("time_stamp", System.currentTimeMillis()); 171 | } catch (JSONException e) { 172 | //recordExceptionToFirebase(e); 173 | e.printStackTrace(); 174 | } 175 | messageQueue.add(object); 176 | } 177 | //endregion 178 | 179 | @Override 180 | public void run() { 181 | 182 | while (isNetworkEnabled) { 183 | if (messageQueue.size() > 0) { 184 | final JSONObject object = messageQueue.remove(0); 185 | AndroidNetworking.post(serverURL).addBodyParameter("data", object.toString()).build() 186 | .getAsOkHttpResponse(new OkHttpResponseListener() { 187 | @Override 188 | public void onResponse(Response response) { 189 | //DO Nothing 190 | } 191 | 192 | @Override 193 | public void onError(ANError anError) { 194 | Log.e(TAG, "Error: " + anError); 195 | } 196 | }); 197 | } 198 | } 199 | } 200 | 201 | @Retention(RetentionPolicy.CLASS) 202 | @StringDef({ 203 | CONNECTION_BAD, CONNECTION_GOOD, CONNTECTION_WARNING 204 | }) 205 | @interface ConnetionValueParam { 206 | } 207 | 208 | @Retention(RetentionPolicy.CLASS) 209 | @IntDef({ 210 | CONNECTION_RC, CONNECTION_BRIDGE 211 | }) 212 | @interface ConnetionTypeParam { 213 | } 214 | 215 | 216 | } 217 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/DJIPluginRingBufferParser.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import android.util.Log; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | 7 | public class DJIPluginRingBufferParser { 8 | private String TAG = getClass().getSimpleName(); 9 | 10 | /** 命令通道 */ 11 | public static final int CHANNEL_CMD = 22345; 12 | 13 | static final byte LOGIC_LINK_HEADER_0X55 = Utils.getByte(0x55); 14 | 15 | static final byte LOGIC_LINK_HEADER_0XCC = Utils.getByte(0xCC); 16 | 17 | /** 18 | * mavice air 2新增的逻辑链路的帧头长度 19 | */ 20 | private static final int LOGIC_LINK_HEAD_LENGTH = 8; 21 | 22 | private final float factor = 1f / 8f; // 扩容因子 23 | 24 | private boolean isGettedHeader = false; 25 | private final boolean isDebug = true; 26 | 27 | protected byte[] buffer; 28 | private int byteOffset = 0; 29 | 30 | private String name = "default"; 31 | 32 | private OutputStream mOutputStream; 33 | 34 | public DJIPluginRingBufferParser(int bufferSize, OutputStream out) { 35 | this.buffer = new byte[bufferSize]; 36 | mOutputStream = out; 37 | } 38 | 39 | public void setName(String name) { 40 | this.name = name; 41 | } 42 | 43 | public String getName() { 44 | return this.name; 45 | } 46 | 47 | public void parse(byte[] buffer, int offset, int count) { 48 | if (count > this.buffer.length - byteOffset) { 49 | tryToExpandCapacity(count); 50 | } 51 | System.arraycopy(buffer, offset, this.buffer, byteOffset, count); 52 | byteOffset += count; 53 | findPack(); 54 | } 55 | 56 | /** 57 | * @Description : 当容量不足时,尝试扩容 58 | * @param length 需要扩容的容量值 59 | */ 60 | private void tryToExpandCapacity(final int length) { 61 | final int newCapacity = newCapacity(this.buffer.length, length); 62 | if (isDebug) { 63 | Log.d(TAG, "Try to expand capacity:" + (newCapacity - this.buffer.length)); 64 | } 65 | final byte[] newArray = new byte[newCapacity]; 66 | System.arraycopy(this.buffer, 0, newArray, 0, this.buffer.length); 67 | this.buffer = newArray; 68 | } 69 | 70 | /** 71 | * @Description : 计算新的容量值 72 | * @param originLength 当前的容量值 73 | * @param length 需要增加的容量值 74 | * @return 75 | */ 76 | private int newCapacity(final int originLength, final int length) { 77 | int size = 0; 78 | do { 79 | size += (int) (factor * originLength); 80 | } while (size <= length); 81 | return (size + originLength); 82 | } 83 | 84 | private void resetBuffer() { 85 | if (isDebug) { 86 | Log.d(TAG, "_" + name + "byteOffset=" + byteOffset + " expendSize=" + expendSize); 87 | } 88 | if (expendSize > 0) { 89 | byteOffset -= expendSize; 90 | if (byteOffset > 0) { 91 | System.arraycopy(buffer, expendSize, buffer, 0, byteOffset); 92 | } else { 93 | if (isDebug && byteOffset < 0) { 94 | Log.d(TAG, "_" + name + "byteOffset < 0"); 95 | } 96 | byteOffset = 0; 97 | } 98 | expendSize = 0; 99 | } 100 | } 101 | 102 | private int myHeaderIndex = 0; 103 | private int expendSize = 0; 104 | 105 | private void findPack() { 106 | 107 | while (true) { 108 | resetBuffer(); 109 | 110 | if (!isGettedHeader) { 111 | for (int i = 0; i < byteOffset; i++) { 112 | 113 | if (buffer[i]==LOGIC_LINK_HEADER_0X55) { 114 | 115 | myHeaderIndex = i; 116 | if (myHeaderIndex >= byteOffset) {//不足以找到完整包头 跳出循环继续接收 117 | expendSize = myHeaderIndex; 118 | break; 119 | } 120 | 121 | isGettedHeader = true; 122 | 123 | if (isGettedHeader) { 124 | expendSize = myHeaderIndex+1; 125 | break; 126 | } 127 | } 128 | expendSize++; 129 | }//for 130 | } 131 | 132 | //没有包头 清空当前数据 133 | if (!isGettedHeader) { 134 | if (isDebug) { 135 | Log.d(TAG, "_" + name + "parseRcvBuffer 当前buffer没有包头"); 136 | } 137 | break; 138 | } 139 | 140 | if (byteOffset < expendSize + 2) { 141 | break; 142 | } 143 | 144 | int v1Length = Utils.getInt(buffer, expendSize, 2) & 0x03FF; 145 | int v1Version = Utils.getInt(buffer, expendSize, 2) >> 10; 146 | if (isDebug) { 147 | byte[] data11 = new byte[2]; 148 | System.arraycopy(buffer, expendSize, data11, 0, 2); 149 | Log.d(TAG, "_" + name + "parseRcvBuffer V1 data length:"+v1Length + " v1 version:" + v1Version + " "+ Utils.bytesToHex(data11)); 150 | } 151 | 152 | if (v1Length < 0) { 153 | isGettedHeader = false; 154 | continue; 155 | } 156 | 157 | if (byteOffset < myHeaderIndex + v1Length) { 158 | break; 159 | } 160 | 161 | int v1RealDataLength = v1Length + LOGIC_LINK_HEAD_LENGTH; 162 | 163 | PackBufferObject bufferObject = PackBufferObject.getPackBufferObject(v1RealDataLength); 164 | byte[] box_head = bufferObject.getBuffer(); 165 | int it = 0; 166 | 167 | //逻辑链路固定的header 0x55, 0xcc 168 | box_head[it] = LOGIC_LINK_HEADER_0X55;it++; 169 | box_head[it] = LOGIC_LINK_HEADER_0XCC;it++; 170 | 171 | //通道号 172 | box_head[it] = (byte) (CHANNEL_CMD & 0xff);it++; 173 | box_head[it] = (byte) ((CHANNEL_CMD & 0xff00) >> 8);it++; 174 | 175 | //v1数据长度 176 | box_head[it] = (byte) (v1Length & 0xff);it++; 177 | box_head[it] = (byte) ((v1Length & 0xff00) >> 8);it++; 178 | box_head[it] = (byte) ((v1Length & 0xff0000) >> 16);it++; 179 | box_head[it] = (byte) ((v1Length & 0xff000000) >> 24);it++; 180 | 181 | System.arraycopy(buffer, myHeaderIndex, box_head, it, v1Length); 182 | try { 183 | mOutputStream.write(box_head, 0, v1RealDataLength); 184 | mOutputStream.flush(); 185 | } catch (IOException e) { 186 | e.printStackTrace(); 187 | } 188 | expendSize += v1Length; 189 | isGettedHeader = false; 190 | } 191 | 192 | resetBuffer(); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/NetworkServerInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, DJI. All rights reserved. 3 | */ 4 | package com.dji.wsbridge.lib; 5 | 6 | import com.dji.wsbridge.lib.connection.WSConnectionManager; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.nio.ByteBuffer; 11 | 12 | public class NetworkServerInputStream extends InputStream { 13 | private WSConnectionManager mServer; 14 | 15 | public NetworkServerInputStream(WSConnectionManager server) { 16 | mServer = server; 17 | } 18 | 19 | @Override 20 | public int read() throws IOException { 21 | ByteBuffer buffer = mServer.read(); 22 | return buffer.get(); 23 | } 24 | 25 | @Override 26 | public int read(byte[] b) throws IOException { 27 | ByteBuffer buffer = mServer.read(); 28 | int len = Math.min(buffer.remaining(), b.length); 29 | buffer.get(b, 0, len); 30 | return len; 31 | } 32 | 33 | @Override 34 | public int read(byte[] b, int off, int len) throws IOException { 35 | ByteBuffer buffer = mServer.read(); 36 | len = Math.min(buffer.remaining(), b.length); 37 | buffer.get(b, off, len); 38 | return len; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/NetworkServerOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, DJI. All rights reserved. 3 | */ 4 | package com.dji.wsbridge.lib; 5 | 6 | import com.dji.wsbridge.lib.connection.WSConnectionManager; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | import java.util.concurrent.Callable; 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | 14 | import io.reactivex.Observable; 15 | import io.reactivex.schedulers.Schedulers; 16 | 17 | public class NetworkServerOutputStream extends OutputStream { 18 | 19 | private static final String TAG = "BridgeStream"; 20 | 21 | private static final int TRANSFER_SIZE_WITHOUT_VIDEO = 5; 22 | private static final int TRANSFER_SIZE_WITH_VIDEO = 2 * 1024; 23 | private static final int BUFFER_SIZE = 10 * 1024 * 1024; 24 | 25 | private static final byte[] VIDEO_HEADER = {0x55, (byte) 0xcc, 0x4a, 0x57}; 26 | private static final byte[] VIDEO_EXT_HEADER = {0x55, (byte) 0xcc, 0x4b, 0x57}; 27 | 28 | private WSConnectionManager mServer; 29 | private ByteArrayOutputStream mBuffer; 30 | private AtomicBoolean isSending = new AtomicBoolean(false); 31 | //private Observable timer 32 | Observable observable = Observable.fromCallable(new Callable() { 33 | @Override 34 | public Boolean call() throws Exception { 35 | isSending.set(true); 36 | // remove limit since memory issue has been fixed, this is no longer needed 37 | int limit = (mServer.streamFilter == WSConnectionManager.StreamFilter.FILTER_NONE) 38 | ? TRANSFER_SIZE_WITH_VIDEO 39 | : TRANSFER_SIZE_WITHOUT_VIDEO; 40 | if (mBuffer.size() > limit) { 41 | byte[] byteArray = mBuffer.toByteArray(); 42 | mServer.send(byteArray); 43 | mBuffer.reset(); 44 | } 45 | isSending.set(false); 46 | return true; 47 | } 48 | }); 49 | 50 | public NetworkServerOutputStream(WSConnectionManager server) { 51 | mServer = server; 52 | mBuffer = new ByteArrayOutputStream(); 53 | } 54 | 55 | @Override 56 | public void write(int b) throws IOException { 57 | if (b != -1) { 58 | writeAfterFilter(new byte[]{(byte) b}, 0, 1); 59 | } 60 | } 61 | 62 | @Override 63 | public void write(byte[] b) throws IOException { 64 | writeAfterFilter(b, 0, b.length); 65 | } 66 | 67 | @Override 68 | public void write(byte[] b, int off, int len) throws IOException { 69 | if (len != -1) { 70 | writeAfterFilter(b, off, len); 71 | } 72 | } 73 | 74 | private void writeAfterFilter(byte[] b, int off, int len) { 75 | if (shouldFilter(b, off, len)) { 76 | mBuffer.write(b, off, len); 77 | } 78 | sendIfReady(); 79 | } 80 | 81 | private boolean shouldFilter(byte[] b, int off, int len) { 82 | //off & len not checked. 83 | //assumes only one frame at a time. Need to check if there are multiple frames per call. 84 | return ((mServer.streamFilter == WSConnectionManager.StreamFilter.FILTER_VIDEO) && (indexOf(b, VIDEO_HEADER) 85 | >= 0 || indexOf(b, VIDEO_EXT_HEADER) >= 0)) ? false : true; 86 | } 87 | 88 | private void sendIfReady() { 89 | if (!isSending.get()) { 90 | observable.subscribeOn(Schedulers.trampoline()).subscribe(); 91 | } 92 | } 93 | 94 | private int indexOf(byte[] data, byte[] pattern) { 95 | int[] failure = computeFailure(pattern); 96 | 97 | int j = 0; 98 | if (data.length == 0) return -1; 99 | 100 | for (int i = 0; i < data.length; i++) { 101 | while (j > 0 && pattern[j] != data[i]) { 102 | j = failure[j - 1]; 103 | } 104 | if (pattern[j] == data[i]) { 105 | j++; 106 | } 107 | if (j == pattern.length) { 108 | return i - pattern.length + 1; 109 | } 110 | } 111 | return -1; 112 | } 113 | 114 | /** 115 | * Computes the failure function using a boot-strapping process, 116 | * where the pattern is matched against itself. 117 | */ 118 | private int[] computeFailure(byte[] pattern) { 119 | int[] failure = new int[pattern.length]; 120 | 121 | int j = 0; 122 | for (int i = 1; i < pattern.length; i++) { 123 | while (j > 0 && pattern[j] != pattern[i]) { 124 | j = failure[j - 1]; 125 | } 126 | if (pattern[j] == pattern[i]) { 127 | j++; 128 | } 129 | failure[i] = j; 130 | } 131 | 132 | return failure; 133 | } 134 | } 135 | 136 | 137 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/PackBufferObject.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class PackBufferObject { 6 | 7 | private byte[] buffer; 8 | private volatile boolean isUsing = true; 9 | private boolean isRepeat; 10 | 11 | private PackBufferObject(int length) { 12 | int minLength = 100; 13 | int mLength = length< minLength ? minLength : length; 14 | buffer = new byte[mLength];//最长上行长度 15 | } 16 | 17 | public byte[] getBuffer() { 18 | return buffer; 19 | } 20 | 21 | public void willRepeat(boolean isRepeat) { 22 | this.isRepeat = isRepeat; 23 | } 24 | public void noUsed() { 25 | if (!isRepeat)isUsing = false; 26 | } 27 | 28 | private static ArrayList list = new ArrayList<>(); 29 | static synchronized PackBufferObject getPackBufferObject(int length) { 30 | for (PackBufferObject object : list) { 31 | if (!object.isUsing && object.getBuffer().length>=length) { 32 | object.isUsing = true; 33 | return object; 34 | } 35 | } 36 | PackBufferObject object = new PackBufferObject(length); 37 | list.add(object); 38 | return object; 39 | } 40 | } -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/StreamRunner.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | 9 | //import static com.dji.wsbridge.lib.Utils.recordExceptionToFirebase; 10 | 11 | public class StreamRunner extends Thread { 12 | 13 | private static final String TAG = StreamRunner.class.getSimpleName(); 14 | public long byteCount = 0; 15 | private InputStream mInputStream; 16 | private OutputStream mOutputStream; 17 | private AtomicBoolean mStop = new AtomicBoolean(false); 18 | 19 | public StreamRunner(InputStream in, OutputStream out, String name) { 20 | super(name); 21 | mInputStream = in; 22 | mOutputStream = out; 23 | } 24 | 25 | @Override 26 | public void run() { 27 | int ret; 28 | // As explained in: 29 | // https://developer.android.com/guide/topics/connectivity/usb/accessory.html 30 | // "The Android accessory protocol supports packet buffers up to 16384 bytes, 31 | // so you can choose to always declare your buffer to be of this size for simplicity." 32 | byte[] buffer = new byte[16384]; 33 | 34 | while (!mStop.get()) { 35 | try { 36 | if (mOutputStream != null && mInputStream != null) { 37 | ret = mInputStream.read(buffer); 38 | if (ret < 0) { 39 | // Do nothing since it is empty 40 | Log.d(TAG, getName() + ": ret is less than 0"); 41 | break; 42 | } else { 43 | Log.d(TAG, getName() + ": Runner is running"); 44 | byteCount += ret; 45 | mOutputStream.write(buffer, 0, ret); 46 | mOutputStream.flush(); 47 | } 48 | } 49 | } catch (Exception e) { 50 | //recordExceptionToFirebase(e); 51 | //e.printStackTrace(); 52 | //Log.e(TAG, e.getMessage()); 53 | } 54 | } 55 | Log.d(TAG, getName() + ": Runner is stopped"); 56 | } 57 | 58 | public void cleanup() { 59 | mStop.set(true); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/Utils.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib; 2 | 3 | import com.dji.wsbridge.BuildConfig; 4 | //import com.google.firebase.crashlytics.FirebaseCrashlytics; 5 | 6 | import java.io.BufferedInputStream; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.File; 9 | import java.io.FileInputStream; 10 | import java.net.InetAddress; 11 | import java.net.NetworkInterface; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | public class Utils { 16 | 17 | 18 | /** 19 | * Convert byte array to hex string 20 | */ 21 | public static String bytesToHex(byte[] bytes) { 22 | StringBuilder sbuf = new StringBuilder(); 23 | for (int idx = 0; idx < bytes.length; idx++) { 24 | int intVal = bytes[idx] & 0xff; 25 | if (intVal < 0x10) sbuf.append("0"); 26 | sbuf.append(Integer.toHexString(intVal).toUpperCase()); 27 | } 28 | return sbuf.toString(); 29 | } 30 | 31 | /** 32 | * Get utf8 byte array. 33 | * 34 | * @return array of NULL if error was found 35 | */ 36 | public static byte[] getUTF8Bytes(String str) { 37 | try { 38 | return str.getBytes("UTF-8"); 39 | } catch (Exception ex) { 40 | return null; 41 | } 42 | } 43 | 44 | /** 45 | * Load UTF8withBOM or any ansi text file. 46 | * 47 | * @throws java.io.IOException 48 | */ 49 | public static String loadFileAsString(String filename) throws java.io.IOException { 50 | final int BUFLEN = 1024; 51 | BufferedInputStream is = new BufferedInputStream(new FileInputStream(filename), BUFLEN); 52 | try { 53 | ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFLEN); 54 | byte[] bytes = new byte[BUFLEN]; 55 | boolean isUTF8 = false; 56 | int read, count = 0; 57 | while ((read = is.read(bytes)) != -1) { 58 | if (count == 0 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { 59 | isUTF8 = true; 60 | baos.write(bytes, 3, read - 3); // drop UTF8 bom marker 61 | } else { 62 | baos.write(bytes, 0, read); 63 | } 64 | count += read; 65 | } 66 | return isUTF8 ? new String(baos.toByteArray(), "UTF-8") : new String(baos.toByteArray()); 67 | } finally { 68 | try { 69 | is.close(); 70 | } catch (Exception ex) { 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Returns MAC address of the given interface name. 77 | * 78 | * @param interfaceName eth0, wlan0 or NULL=use first interface 79 | * @return mac address or empty string 80 | */ 81 | public static String getMACAddress(String interfaceName) { 82 | try { 83 | List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); 84 | for (NetworkInterface intf : interfaces) { 85 | if (interfaceName != null) { 86 | if (!intf.getName().equalsIgnoreCase(interfaceName)) continue; 87 | } 88 | byte[] mac = intf.getHardwareAddress(); 89 | if (mac == null) return ""; 90 | StringBuilder buf = new StringBuilder(); 91 | for (int idx = 0; idx < mac.length; idx++) 92 | buf.append(String.format("%02X:", mac[idx])); 93 | if (buf.length() > 0) buf.deleteCharAt(buf.length() - 1); 94 | return buf.toString(); 95 | } 96 | } catch (Exception ex) { 97 | } // for now eat exceptions 98 | return ""; 99 | /*try { 100 | // this is so Linux hack 101 | return loadFileAsString("/sys/class/net/" +interfaceName + "/address").toUpperCase().trim(); 102 | } catch (IOException ex) { 103 | return null; 104 | }*/ 105 | } 106 | 107 | /** 108 | * Get IP address from first non-localhost interface 109 | * 110 | * @param ipv4 true=return ipv4, false=return ipv6 111 | * @return address or empty string 112 | */ 113 | public static String getIPAddress(boolean useIPv4) { 114 | try { 115 | List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); 116 | for (NetworkInterface intf : interfaces) { 117 | List addrs = Collections.list(intf.getInetAddresses()); 118 | for (InetAddress addr : addrs) { 119 | if (!addr.isLoopbackAddress()) { 120 | String sAddr = addr.getHostAddress(); 121 | //boolean isIPv4 = InetAddressUtils.isIPv4Address(sAddr); 122 | boolean isIPv4 = sAddr.indexOf(':') < 0; 123 | 124 | if (useIPv4) { 125 | if (isIPv4) return sAddr; 126 | } else { 127 | if (!isIPv4) { 128 | int delim = sAddr.indexOf('%'); // drop ip6 zone suffix 129 | return delim < 0 ? sAddr.toUpperCase() : sAddr.substring(0, delim).toUpperCase(); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } catch (Exception ex) { 136 | } // for now eat exceptions 137 | return ""; 138 | } 139 | 140 | public static int compareVersionNames(String oldVersionName, String newVersionName) { 141 | int res = 0; 142 | 143 | String[] oldNumbers = oldVersionName.split("\\."); 144 | String[] newNumbers = newVersionName.split("\\."); 145 | 146 | // To avoid IndexOutOfBounds 147 | int maxIndex = Math.min(oldNumbers.length, newNumbers.length); 148 | 149 | for (int i = 0; i < maxIndex; i++) { 150 | int oldVersionPart = Integer.valueOf(oldNumbers[i]); 151 | int newVersionPart = Integer.valueOf(newNumbers[i]); 152 | 153 | if (oldVersionPart < newVersionPart) { 154 | res = -1; 155 | break; 156 | } else if (oldVersionPart > newVersionPart) { 157 | res = 1; 158 | break; 159 | } 160 | } 161 | 162 | // If versions are the same so far, but they have different length... 163 | if (res == 0 && oldNumbers.length != newNumbers.length) { 164 | res = (oldNumbers.length > newNumbers.length) ? 1 : -1; 165 | } 166 | 167 | return res; 168 | } 169 | 170 | public static boolean isRooted() { 171 | return findBinary("su"); 172 | } 173 | 174 | public static boolean findBinary(String binaryName) { 175 | boolean found = false; 176 | if (!found) { 177 | String[] places = {"/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/", "/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/"}; 178 | for (String where : places) { 179 | if (new File(where + binaryName).exists()) { 180 | found = true; 181 | break; 182 | } 183 | } 184 | } 185 | return found; 186 | } 187 | 188 | public static boolean isInternalVersion() { 189 | return BuildConfig.BUILD_TYPE == "internal"; 190 | } 191 | 192 | // public static void logToFirebase(String message) { 193 | // if (isInternalVersion() && FirebaseCrashlytics.getInstance() != null) { 194 | // FirebaseCrashlytics.getInstance().log(message); 195 | // } 196 | // } 197 | // 198 | // public static void recordExceptionToFirebase(Exception exception) { 199 | // if (isInternalVersion() && FirebaseCrashlytics.getInstance() != null) { 200 | // FirebaseCrashlytics.getInstance().recordException(exception); 201 | // } 202 | // } 203 | 204 | public static byte getByte(int data) { 205 | return (byte) (data & 0xff); 206 | } 207 | 208 | public static int getInt(byte[] bytes, final int offset, int length) { 209 | if (null == bytes) { 210 | return 0; 211 | } 212 | final int bytesLen = bytes.length; 213 | if (bytesLen == 0 || offset < 0 || bytesLen <= offset) { 214 | return 0; 215 | } 216 | if (length > bytesLen - offset) { 217 | length = bytesLen - offset; 218 | } 219 | 220 | int value = 0; 221 | for (int i = length + offset - 1; i >= offset; i--) { 222 | value = (value << 8 | (bytes[i] & 0xff)); 223 | } 224 | return value; 225 | } 226 | 227 | public static byte[] readBytes(byte[] source, int from, int length) { 228 | byte[] result = new byte[length]; 229 | System.arraycopy(source, from, result, 0, length); 230 | /** 231 | for (int i = 0; i < length; i++) { 232 | result[i] = source[from + i]; 233 | } 234 | */ 235 | return result; 236 | } 237 | 238 | public static byte[] getBytes(short data) { 239 | byte[] bytes = new byte[2]; 240 | bytes[0] = (byte) (data & 0xff); 241 | bytes[1] = (byte) ((data & 0xff00) >> 8); 242 | return bytes; 243 | } 244 | } -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/connection/ConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib.connection; 2 | 3 | import java.util.ArrayList; 4 | 5 | public interface ConnectionManager { 6 | 7 | interface ConnectionCallback { 8 | void onConnect(String message); 9 | void onDisconnect(String message); 10 | } 11 | 12 | ArrayList getStreams(); // returns [InputStream, OutputStream] 13 | void closeStreams(); 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/connection/USBConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib.connection; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.hardware.usb.UsbAccessory; 8 | import android.hardware.usb.UsbManager; 9 | import android.os.ParcelFileDescriptor; 10 | import android.text.TextUtils; 11 | import android.util.Log; 12 | 13 | import com.dji.wsbridge.BridgeActivity; 14 | import com.dji.wsbridge.lib.BridgeApplication; 15 | import com.dji.wsbridge.lib.DJILogger; 16 | 17 | import java.io.FileDescriptor; 18 | import java.io.FileInputStream; 19 | import java.io.FileOutputStream; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | import java.util.ArrayList; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | import io.reactivex.Observable; 27 | import io.reactivex.Observer; 28 | import io.reactivex.annotations.NonNull; 29 | import io.reactivex.disposables.Disposable; 30 | import io.reactivex.schedulers.Schedulers; 31 | 32 | //import static com.dji.wsbridge.lib.Utils.recordExceptionToFirebase; 33 | 34 | public class USBConnectionManager implements ConnectionManager { 35 | 36 | private static USBConnectionManager sInstance = new USBConnectionManager(); 37 | public static USBConnectionManager getInstance() { 38 | return sInstance; 39 | } 40 | 41 | private static final String TAG = "USB"; 42 | 43 | private UsbManager mUsbManager; 44 | private UsbAccessory mAccessory; 45 | private ParcelFileDescriptor mFileDescriptor; 46 | 47 | private InputStream mInStream; 48 | private OutputStream mOutStream; 49 | 50 | private UsbModel currentModel = UsbModel.UNKNOWN; 51 | 52 | public enum UsbModel { 53 | /** 54 | * 231之前的整机 55 | */ 56 | AG("AG410"), 57 | 58 | WM160("WM160"), 59 | 60 | /** 61 | * 新增的逻辑链路 62 | */ 63 | LOGIC_LINK("com.dji.logiclink"), 64 | 65 | UNKNOWN("Unknown"); 66 | 67 | private String value; 68 | 69 | UsbModel(String value) { 70 | this.value = value; 71 | } 72 | 73 | public static UsbModel find(String modelName) { 74 | UsbModel result = UNKNOWN; 75 | if (TextUtils.isEmpty(modelName)) { 76 | return result; 77 | } 78 | 79 | for (int i = 0; i < values().length; i++) { 80 | if (values()[i].value.equals(modelName)) { 81 | result = values()[i]; 82 | break; 83 | } 84 | } 85 | return result; 86 | } 87 | 88 | /** 89 | * Retrieves the display name of an enum constant. 90 | * 91 | * @return string The display name of an enum 92 | */ 93 | public String getModel() { 94 | return this.value; 95 | } 96 | 97 | } 98 | 99 | 100 | private BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { 101 | public void onReceive(Context context, Intent intent) { 102 | String action = intent.getAction(); 103 | if (action.equalsIgnoreCase(UsbManager.ACTION_USB_ACCESSORY_DETACHED)) { //Check if change in USB state 104 | BridgeApplication.getInstance().getBus().post(new USBConnectionEvent(false)); 105 | Log.d(TAG, "ACTION_USB_ACCESSORY_DETACHED"); 106 | return; 107 | } 108 | if (action.equalsIgnoreCase("android.hardware.usb.action.USB_STATE")) { //Check if change in USB state 109 | if (intent.getExtras().getBoolean("connected")) { 110 | Log.d(TAG, "USB_STATE CONNECTED"); 111 | BridgeApplication.getInstance().getBus().post(new USBConnectionEvent(true)); 112 | } else { 113 | Log.d(TAG, "USB_STATE DISCONNECTED"); 114 | BridgeApplication.getInstance().getBus().post(new USBConnectionEvent(false)); 115 | } 116 | } 117 | } 118 | }; 119 | 120 | /** 121 | * ACTION_USB_ACCESSORY_DETACHED is a Service broadcast so we can receive it here. 122 | * // It doesn't have to be in an Activity like {@link BridgeActivity#onNewIntent} 123 | */ 124 | public void init() { 125 | // Broadcast receiver 126 | IntentFilter filter = new IntentFilter(); 127 | filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); 128 | filter.addAction("android.hardware.usb.action.USB_STATE"); 129 | BridgeApplication.getInstance().registerReceiver(mUsbReceiver, filter); 130 | 131 | Observable.timer(2, TimeUnit.SECONDS) 132 | .observeOn(Schedulers.computation()) 133 | .repeat() 134 | .subscribe(new Observer() { 135 | @Override 136 | public void onSubscribe(@NonNull Disposable d) { 137 | 138 | } 139 | @Override 140 | public void onNext(@NonNull Long aLong) { 141 | checkForDJIAccessory(); 142 | } 143 | @Override 144 | public void onError(@NonNull Throwable e) { 145 | 146 | } 147 | @Override 148 | public void onComplete() { 149 | 150 | } 151 | }); 152 | } 153 | 154 | public void destroy() { 155 | BridgeApplication.getInstance().unregisterReceiver(mUsbReceiver); 156 | } 157 | 158 | public void checkForDJIAccessory() { 159 | mUsbManager = (UsbManager) BridgeApplication.getInstance().getSystemService(Context.USB_SERVICE); 160 | UsbAccessory[] accessoryList = mUsbManager.getAccessoryList(); 161 | if (accessoryList != null 162 | && accessoryList.length > 0 163 | && !TextUtils.isEmpty(accessoryList[0].getManufacturer()) 164 | && accessoryList[0].getManufacturer().equals("DJI")) { 165 | mAccessory = accessoryList[0]; 166 | String model = mAccessory.getModel(); 167 | currentModel = UsbModel.find(model); 168 | BridgeApplication.getInstance().getBus().post(new RCConnectionEvent(true)); 169 | //Check permission 170 | if (mUsbManager.hasPermission(mAccessory)) { 171 | Log.d(TAG, "RC CONNECTED"); 172 | } else { 173 | Log.d(TAG, "NO Permission to USB Accessory"); 174 | DJILogger.e(TAG, "NO Permission to USB Accessory"); 175 | //mUsbManager.requestPermission(mAccessory, null); 176 | } 177 | } else { 178 | BridgeApplication.getInstance().getBus().post(new RCConnectionEvent(false)); 179 | Log.d(TAG, "RC DISCONNECTED"); 180 | } 181 | } 182 | 183 | @Override 184 | public ArrayList getStreams() { 185 | try { 186 | if (mAccessory != null && mUsbManager.hasPermission(mAccessory)) { 187 | mFileDescriptor = mUsbManager.openAccessory(mAccessory); 188 | if (mFileDescriptor != null) { 189 | FileDescriptor fd = mFileDescriptor.getFileDescriptor(); 190 | if (fd.valid()) { 191 | mInStream = new FileInputStream(fd); 192 | mOutStream = new FileOutputStream(fd); 193 | } else { 194 | DJILogger.e(TAG, "Invalid File Descriptor"); 195 | } 196 | } else { 197 | DJILogger.e(TAG, "Cannot Open Accessory"); 198 | } 199 | } else { 200 | DJILogger.e(TAG, "Accessory NOT available"); 201 | } 202 | } catch (Exception e) { 203 | 204 | } 205 | 206 | ArrayList array = new ArrayList<>(); 207 | if (mInStream != null && mOutStream != null) { 208 | array.add(mInStream); 209 | array.add(mOutStream); 210 | } 211 | return array; 212 | } 213 | 214 | @Override 215 | public void closeStreams() { 216 | try { 217 | if (mOutStream != null) { 218 | mOutStream.close(); 219 | mOutStream = null; 220 | } 221 | if (mInStream != null) { 222 | mInStream.close(); 223 | mInStream = null; 224 | } 225 | if (mFileDescriptor != null) { 226 | mFileDescriptor.close(); 227 | mFileDescriptor = null; 228 | } 229 | } catch (IOException e) { 230 | //recordExceptionToFirebase(e); 231 | e.printStackTrace(); 232 | } 233 | } 234 | 235 | public UsbModel getUSBModel() { 236 | return currentModel; 237 | } 238 | 239 | /** 240 | * Event to send changes in USB Connection 241 | */ 242 | public static final class USBConnectionEvent { 243 | final boolean isConnected; 244 | public USBConnectionEvent(boolean isConnected) { 245 | this.isConnected = isConnected; 246 | } 247 | public boolean isConnected() { 248 | return isConnected; 249 | } 250 | } 251 | 252 | /** 253 | * Event to send changes in DJI RC Connection 254 | */ 255 | public static final class RCConnectionEvent { 256 | final boolean isConnected; 257 | public RCConnectionEvent(boolean isConnected) { 258 | this.isConnected = isConnected; 259 | } 260 | public boolean isConnected() { 261 | return isConnected; 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/connection/WSConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.dji.wsbridge.lib.connection; 2 | 3 | import com.dji.wsbridge.lib.BridgeApplication; 4 | import com.dji.wsbridge.lib.DJILogger; 5 | import com.dji.wsbridge.lib.NetworkServerInputStream; 6 | import com.dji.wsbridge.lib.NetworkServerOutputStream; 7 | 8 | import org.java_websocket.WebSocket; 9 | import org.java_websocket.WebSocketImpl; 10 | import org.java_websocket.framing.Framedata; 11 | import org.java_websocket.framing.FramedataImpl1; 12 | import org.java_websocket.handshake.ClientHandshake; 13 | import org.java_websocket.server.WebSocketServer; 14 | import org.json.JSONObject; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | import java.net.InetSocketAddress; 20 | import java.net.UnknownHostException; 21 | import java.nio.ByteBuffer; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.ArrayList; 24 | import java.util.Collection; 25 | import java.util.Iterator; 26 | import java.util.Locale; 27 | import java.util.Timer; 28 | import java.util.TimerTask; 29 | import java.util.concurrent.LinkedBlockingDeque; 30 | import java.util.concurrent.atomic.AtomicBoolean; 31 | 32 | //import static com.dji.wsbridge.lib.Utils.recordExceptionToFirebase; 33 | 34 | 35 | public class WSConnectionManager extends WebSocketServer implements ConnectionManager { 36 | 37 | private static final String TAG = "NW"; 38 | private static final String KEEP_ALIVE = "KeepAlive"; 39 | private static final int DISCONNECT_TIMEOUT = 30000; //Milliseconds 40 | private static final int KEEP_ALIVE_TIMER_INTERVAL = 1000; //Milliseconds 41 | private static final int MAX_BUFFER_SIZE = 100;// To avoid OOM crash, ideally we should be more dynamic with this value 42 | private static WSConnectionManager instance; 43 | public StreamFilter streamFilter = StreamFilter.FILTER_NONE; 44 | private final LinkedBlockingDeque mQueue; 45 | private ByteBuffer mLast; 46 | private Timer keepAlivePollTimer; 47 | private TimerTask keepAlivePollTimerTask; 48 | private long lastPingTime = 0; 49 | private InputStream mInStream; 50 | private OutputStream mOutStream; 51 | private ByteStatCounter rxStatTracker; 52 | private ByteStatCounter txStatTracker; 53 | private final AtomicBoolean isSettingUpTimer = new AtomicBoolean(false); 54 | 55 | public WSConnectionManager(int port) throws UnknownHostException { 56 | super(new InetSocketAddress(port)); 57 | mQueue = new LinkedBlockingDeque<>(MAX_BUFFER_SIZE); 58 | } 59 | 60 | public static WSConnectionManager getInstance() { 61 | return instance; 62 | } 63 | 64 | public static void setupInstance(int port) throws UnknownHostException { 65 | DJILogger.d(TAG, "setupInstance "); 66 | instance = new WSConnectionManager(port); 67 | instance.start(); 68 | } 69 | 70 | @Override 71 | public void onOpen(WebSocket conn, ClientHandshake handshake) { 72 | DJILogger.d(TAG, "onOpen "); 73 | sendConnectivityStatus(true, conn); 74 | lastPingTime = System.currentTimeMillis(); 75 | setupKeepAlivePolling(); 76 | } 77 | 78 | @Override 79 | public void onClose(WebSocket conn, int code, String reason, boolean remote) { 80 | DJILogger.d(TAG, "onClose "); 81 | sendConnectivityStatus(false, conn); 82 | if (activeConnectionCount() <= 0) { 83 | stopKeepAlivePolling(); 84 | } 85 | } 86 | 87 | @Override 88 | public void onError(WebSocket conn, Exception ex) { 89 | DJILogger.d(TAG, "onError " + ex.getMessage()); 90 | sendConnectivityStatus(false, conn); 91 | if (activeConnectionCount() <= 0) { 92 | stopKeepAlivePolling(); 93 | } 94 | } 95 | 96 | @Override 97 | public void onStart() { 98 | 99 | } 100 | 101 | @Override 102 | public void onWebsocketPing(WebSocket webSocket, Framedata framedata) { 103 | ByteBuffer buffer = framedata.getPayloadData(); 104 | String message = new String(buffer.array(), StandardCharsets.UTF_8); 105 | try { 106 | //Expected JSON Format: {"Type": "KeepAlive", "Platform" : "iOS" or "Android", "Filter": 0 - None, 1 - filter video} 107 | JSONObject receivedData = new JSONObject(message); 108 | 109 | if (receivedData.get("Type").equals(KEEP_ALIVE)) { 110 | lastPingTime = System.currentTimeMillis(); 111 | } 112 | streamFilter = (receivedData.getInt("Filter") == 1) ? StreamFilter.FILTER_VIDEO : StreamFilter.FILTER_NONE; 113 | } catch (Exception e) { 114 | //recordExceptionToFirebase(e); 115 | //e.printStackTrace(); 116 | //Log.e(TAG, e.getMessage()); 117 | } 118 | try { 119 | //Sent JSON Format: {"rx_kbps" : 0.0, "rx_bytecount" : 0, "tx_kbps" : 0.0, "tx_bytecount" : 0} 120 | JSONObject sentJSON = new JSONObject(); 121 | sentJSON.put("rx_kbps", rxStatTracker.recentKBps); 122 | sentJSON.put("rx_bytecount", rxStatTracker.recentByteCount); 123 | sentJSON.put("tx_kbps", txStatTracker.recentKBps); 124 | sentJSON.put("tx_bytecount", txStatTracker.recentByteCount); 125 | ((FramedataImpl1) framedata).setPayload(ByteBuffer.wrap(sentJSON.toString() 126 | .getBytes(StandardCharsets.UTF_8))); 127 | } catch (Exception e) { 128 | //recordExceptionToFirebase(e); 129 | //e.printStackTrace(); 130 | //Log.e(TAG,e.getMessage()); 131 | } 132 | 133 | super.onWebsocketPing(webSocket, framedata); 134 | } 135 | 136 | private void sendConnectivityStatus(boolean isConnected, WebSocket conn) { 137 | String hostName = "Unknown"; 138 | if (conn != null && conn.getRemoteSocketAddress() != null) { 139 | hostName = conn.getRemoteSocketAddress().getHostName(); 140 | } 141 | BridgeApplication.getInstance() 142 | .getBus() 143 | .post(new WSConnectionEvent(isConnected, hostName, activeConnectionCount())); 144 | if (isConnected) { 145 | txStatTracker = new ByteStatCounter(); 146 | rxStatTracker = new ByteStatCounter(); 147 | } else { 148 | txStatTracker = null; 149 | rxStatTracker = null; 150 | } 151 | } 152 | 153 | private void setupKeepAlivePolling() { 154 | if (isSettingUpTimer.compareAndSet(false, true)) { 155 | stopKeepAlivePolling(); 156 | keepAlivePollTimerTask = new TimerTask() { 157 | @Override 158 | public void run() { 159 | long currentTime = System.currentTimeMillis(); 160 | long delta = currentTime - lastPingTime; 161 | if (lastPingTime > 0 && delta > DISCONNECT_TIMEOUT) { 162 | for (Iterator iterator = connections().iterator(); iterator.hasNext(); ) { 163 | final WebSocket eachConnection = iterator.next(); 164 | sendConnectivityStatus(false, eachConnection); 165 | eachConnection.close(); 166 | DJILogger.e(TAG, 167 | "Disconnecting Network. No Pings for " 168 | + String.format(Locale.US, 169 | "%.2f", 170 | delta / 1000.0) 171 | + "secs."); 172 | } 173 | stopKeepAlivePolling(); 174 | } 175 | } 176 | }; 177 | 178 | keepAlivePollTimer = new Timer(); 179 | keepAlivePollTimer.scheduleAtFixedRate(keepAlivePollTimerTask, 180 | KEEP_ALIVE_TIMER_INTERVAL, 181 | KEEP_ALIVE_TIMER_INTERVAL); 182 | isSettingUpTimer.set(false); 183 | } 184 | } 185 | 186 | private void stopKeepAlivePolling() { 187 | lastPingTime = 0; 188 | if (keepAlivePollTimer != null) { 189 | keepAlivePollTimer.cancel(); 190 | keepAlivePollTimer = null; 191 | } 192 | } 193 | 194 | @Override 195 | public ArrayList getStreams() { 196 | mInStream = new NetworkServerInputStream(this); 197 | mOutStream = new NetworkServerOutputStream(this); 198 | ArrayList array = new ArrayList<>(); 199 | array.add(mInStream); 200 | array.add(mOutStream); 201 | return array; 202 | } 203 | 204 | @Override 205 | public void closeStreams() { 206 | try { 207 | if (mOutStream != null) { 208 | mOutStream.close(); 209 | mOutStream = null; 210 | } 211 | if (mInStream != null) { 212 | mInStream.close(); 213 | mInStream = null; 214 | } 215 | } catch (IOException e) { 216 | //recordExceptionToFirebase(e); 217 | //e.printStackTrace(); 218 | //Log.e(TAG,e.getMessage()); 219 | } 220 | } 221 | 222 | //region -------------------------------------------- Read Write ---------------------------------------------- 223 | @Override 224 | public void onMessage(WebSocket conn, ByteBuffer buffer) { 225 | // Drop the first package in the queue if it is full 226 | if (mQueue.remainingCapacity() <= 0) { 227 | mQueue.removeFirst(); 228 | } 229 | mQueue.add(buffer); 230 | //DJILogger.d(TAG, "onMessage" + buffer.array().length); 231 | if (rxStatTracker != null) { 232 | rxStatTracker.increaseByteCount(buffer.limit()); 233 | } 234 | } 235 | 236 | @Override 237 | public void onMessage(WebSocket conn, String message) { 238 | //DJILogger.d(TAG, "onMessage "+message); 239 | System.out.println(conn + ": " + message); 240 | } 241 | 242 | public ByteBuffer read() { 243 | if (mLast == null || mLast.remaining() <= 0) { 244 | try { 245 | mLast = mQueue.take(); 246 | } catch (InterruptedException e) { 247 | //recordExceptionToFirebase(e); 248 | //Log.e(TAG,e.getMessage()); 249 | //e.printStackTrace(); 250 | } 251 | } 252 | return mLast; 253 | } 254 | 255 | public void send(byte[] b) { 256 | Collection con = connections(); 257 | if (txStatTracker != null) { 258 | txStatTracker.increaseByteCount(b.length); 259 | } 260 | if (activeConnectionCount() > 0) { 261 | final int maxBufferSizePerConnection = MAX_BUFFER_SIZE / con.size(); 262 | synchronized (con) { 263 | for (WebSocket conn : con) { 264 | if (conn.isOpen()) { 265 | final boolean isSlowTraffic; 266 | if (conn instanceof WebSocketImpl && ((WebSocketImpl) conn).outQueue.size() > maxBufferSizePerConnection) { 267 | // Do nothing because we don't want to over flow the internal buffer of WebSocket 268 | // This could happen when usb is fast but the connection is slow ( producer/consumer problem) 269 | isSlowTraffic = true; 270 | } else { 271 | conn.send(b); 272 | isSlowTraffic = false; 273 | } 274 | if (isSlowTraffic) { 275 | String hostName = ""; 276 | if (conn != null && conn.getRemoteSocketAddress() != null) { 277 | hostName = conn.getRemoteSocketAddress().getHostName(); 278 | } 279 | BridgeApplication.getInstance() 280 | .getBus() 281 | .post(new WSTrafficEvent(isSlowTraffic, hostName)); 282 | } 283 | //DJILogger.d("SOURCE", DJILogger.sha1Hash(b) + " -- " + DJILogger.bytesToHex(b)); 284 | } 285 | } 286 | } 287 | } 288 | } 289 | 290 | private int activeConnectionCount() { 291 | return connections().size(); 292 | } 293 | 294 | public enum StreamFilter { 295 | FILTER_NONE, FILTER_VIDEO 296 | } 297 | //endregion -------------------------------------------------------------------------------------------------- 298 | 299 | /** 300 | * Event to send changes in WebSocket Connection 301 | */ 302 | public static final class WSConnectionEvent { 303 | final boolean isConnected; 304 | final String message; 305 | final int activeConnectionCount; 306 | 307 | public WSConnectionEvent(boolean isConnected, String message, int activeConnectionCount) { 308 | this.isConnected = isConnected; 309 | this.message = message; 310 | this.activeConnectionCount = activeConnectionCount; 311 | } 312 | 313 | public boolean isConnected() { 314 | return isConnected; 315 | } 316 | 317 | public String getMessage() { 318 | return message; 319 | } 320 | 321 | public int getActiveConnectionCount() { 322 | return activeConnectionCount; 323 | } 324 | } 325 | 326 | /** 327 | * Event to notify changes in WebSocket Traffic 328 | */ 329 | public static final class WSTrafficEvent { 330 | final boolean isSlowConnection; 331 | final String message; 332 | 333 | public WSTrafficEvent(boolean isSlowConnection, String message) { 334 | this.isSlowConnection = isSlowConnection; 335 | this.message = message; 336 | } 337 | 338 | public boolean isSlowConnection() { 339 | return isSlowConnection; 340 | } 341 | 342 | public String getMessage() { 343 | return message; 344 | } 345 | } 346 | 347 | private class ByteStatCounter { 348 | 349 | public long recentByteCount = 0; 350 | public double recentKBps = 0.0; 351 | 352 | private long interval = 0; 353 | private long currByteCount = 0; 354 | 355 | void increaseByteCount(long byteCount) { 356 | currByteCount += byteCount; 357 | 358 | long currentInterval = System.currentTimeMillis(); 359 | long delta = currentInterval - interval; 360 | 361 | if (interval == 0) { 362 | interval = currentInterval; 363 | } else if (delta > 10000) { 364 | interval = currentInterval; 365 | recentByteCount = currByteCount; 366 | recentKBps = recentByteCount / delta; 367 | currByteCount = 0; 368 | } 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_dji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_dji.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_navgation_bar_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_navgation_bar_2x.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_rc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_rc.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_rect.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_tab_bkgnd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_tab_bkgnd.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_tab_left_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_tab_left_1.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_tab_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_tab_right.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-hdpi/icon_wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-hdpi/icon_wifi.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_dji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_dji.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_navgation_bar_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_navgation_bar_2x.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_rc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_rc.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_rect.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_tab_bkgnd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_tab_bkgnd.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_tab_left_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_tab_left_1.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_tab_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_tab_right.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-ldpi/icon_wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-ldpi/icon_wifi.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_dji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_dji.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_navgation_bar_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_navgation_bar_2x.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_rc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_rc.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_rect.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_tab_bkgnd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_tab_bkgnd.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_tab_left_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_tab_left_1.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_tab_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_tab_right.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/drawable-mdpi/icon_wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dji-sdk/Android-Bridge-App/d2b841e35801ad766bd074f3454d0723ba1c92bc/android-wsbridge/app/src/main/res/drawable-mdpi/icon_wifi.png -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/layout/activity_bridge.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 12 | 22 | 23 | 25 | 30 | 31 | 42 | 43 | 52 | 53 | 54 | 55 | 64 | 65 | 66 | 81 | 82 | 95 | 96 | 104 | 105 | 106 | 107 | 115 | 116 | 125 | 126 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /android-wsbridge/app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 22 | 23 | 29 | 30 | 38 | 39 | 40 | 44 | 45 |