├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── README.md ├── SupportedDevices.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher_alpha-playstore.png │ ├── java │ ├── com │ │ └── fpvout │ │ │ └── digiview │ │ │ ├── DataCollectionAgreementPopupActivity.java │ │ │ ├── H264Extractor.java │ │ │ ├── InputStreamBufferedDataSource.java │ │ │ ├── InputStreamDataSource.java │ │ │ ├── MainActivity.java │ │ │ ├── OverlayStatus.java │ │ │ ├── OverlayView.java │ │ │ ├── PerformancePreset.java │ │ │ ├── SettingsActivity.java │ │ │ ├── UsbDeviceBroadcastReceiver.java │ │ │ ├── UsbDeviceListener.java │ │ │ ├── UsbMaskConnection.java │ │ │ ├── VersionPreference.java │ │ │ └── VideoReaderExoplayer.java │ └── usb │ │ ├── AndroidUSBInputStream.java │ │ ├── AndroidUSBOutputStream.java │ │ └── CircularByteBuffer.java │ └── res │ ├── drawable-hdpi │ ├── ic_goggles.png │ ├── ic_goggles_disconnected_red.png │ ├── ic_goggles_disconnected_white.png │ ├── ic_goggles_white.png │ └── ic_splash.png │ ├── drawable-mdpi │ ├── ic_goggles.png │ ├── ic_goggles_disconnected_red.png │ ├── ic_goggles_disconnected_white.png │ ├── ic_goggles_white.png │ └── ic_splash.png │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable-xhdpi │ ├── ic_goggles.png │ ├── ic_goggles_disconnected_red.png │ ├── ic_goggles_disconnected_white.png │ ├── ic_goggles_white.png │ └── ic_splash.png │ ├── drawable-xxhdpi │ ├── ic_goggles.png │ ├── ic_goggles_disconnected_red.png │ ├── ic_goggles_disconnected_white.png │ ├── ic_goggles_white.png │ ├── ic_gogles_disconnected_white.png │ └── ic_splash.png │ ├── drawable-xxxhdpi │ ├── ic_goggles.png │ ├── ic_goggles_disconnected_red.png │ ├── ic_goggles_disconnected_white.png │ ├── ic_goggles_white.png │ └── ic_splash.png │ ├── drawable │ ├── ic_launcher_background.xml │ └── splash.xml │ ├── font │ └── gidolinya.otf │ ├── layout │ ├── activity_main.xml │ ├── backdrop_view.xml │ ├── datacollection_agreement_popup.xml │ └── settings_activity.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-night │ └── themes.xml │ ├── values-pt │ └── strings.xml │ ├── values-zh │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── strings.xml │ ├── styles.xml │ └── themes.xml │ └── xml │ ├── device_filter.xml │ └── root_preferences.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master, dev ] 6 | pull_request: 7 | branches: [ master, dev ] 8 | workflow_dispatch: 9 | 10 | 11 | 12 | jobs: 13 | apk: 14 | name: Generate APK 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Build debug APK 24 | run: bash ./gradlew assembleDebug --stacktrace 25 | - name: Upload APK 26 | uses: actions/upload-artifact@v1 27 | with: 28 | name: app 29 | path: app/build/outputs/apk/debug/app-debug.apk 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .idea 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 fpvout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![digiview-banner](https://user-images.githubusercontent.com/956646/118431082-def7c080-b6d5-11eb-913e-40b6fc58a861.png)](https://play.google.com/store/apps/details?id=com.fpvout.digiview) 2 | 3 | DigiView is an Android app that allows you to get a live preview from your DJI FPV Goggles (V1 & V2). 4 | 5 | Working with Android 7+ and devices supporting USB Host mode. 6 | 7 | [Is my device Compatible?](SupportedDevices.md) (non-exhaustive) 8 | 9 | [DigiView Wiki](https://github.com/fpvout/fpvout.com/wiki) 10 | 11 | ## Download 12 | You can download the **Beta** Release [from Google Play Store](https://play.google.com/store/apps/details?id=com.fpvout.digiview). If you need an apk, you can get one [here](https://github.com/fpvout/DigiView-Android/releases/download/v1.0.0-beta/DigiView_1.0.0_Beta.apk). 13 | 14 | **Note:** App is still in development so you might run into some bugs or even a crash. 15 | 16 | ## Instructions 17 | - Power on your goggles. 18 | - Power on your drone. 19 | - Plug the USB cable from your goggles into your phone (if using an OTG adapter, that goes into your phone). 20 | - Automatic launch is enabled so app should launch. If not, just launch app manually. 21 | - Wait a bit and video should start streaming. 22 | - If there is no video after a few seconds, it's frozen or slowed down, try to unplug the USB cable and plug it back in. 23 | 24 | If there is any other issues, please check our [Discord server](https://discord.gg/uGYMNByeTH), some people might help you there. 25 | 26 | ## Known Issues 27 | - **You might need to set Auto temp control off on your Vista/Air unit (under Settings > Device in goggle menu) to get it to work**. 28 | - Some people reported it's working best in 50mbps mode 29 | - Concerning hardware, USB-C to USB-C cables sometimes don't work depending on the phone you're using. in that case, an USB OTG adapter is the best option. 30 | - You *may* need to activate USB OTG in your Android settings to get the fpv goggles detected. 31 | - See [open issues](https://github.com/fpvout/DigiView-Android/issues) for more or to report a new issue. 32 | 33 | ## Development 34 | - We will put more info here in the near future. 35 | 36 | ## Contributing 37 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Most PRs should be based on and target `dev` branch. 38 | 39 | ## They deserve some credits! 40 | - [jlucidar](https://github.com/jlucidar) - Lead developer - [Donate](https://paypal.me/jlucidar) - [Youtube](https://www.youtube.com/channel/UCBbyqtxntnlF6Cn_8ezkTLQ) 41 | - [omouren](https://github.com/omouren) - Some really appreciated stability fixes! - [Donate](https://paypal.me/omouren) - [Youtube](https://www.youtube.com/channel/UCJi-wllO8GY5f9k8gB_VGTg) 42 | - [vinayselvaraj](https://github.com/vinayselvaraj) - stopped the screen from sleeping! 43 | - [Joonas](https://fpv.wtf/) - Found the secret packet - [Donate](https://www.buymeacoffee.com/fpv.wtf) 44 | - [D3VL](https://d3vl.com) - Who is orchestrating all that stuff - [Donate](https://www.buymeacoffee.com/d3vl) 45 | 46 | Please make sure to check these guys out! And feel free to donate ;) 47 | 48 | ## Made with love using: 49 | - [Exoplayer](https://exoplayer.dev/) 50 | - [Gidolinya font](https://github.com/larsenwork/Gidole) 51 | 52 | ## License 53 | [MIT](https://choosealicense.com/licenses/mit/) 54 | -------------------------------------------------------------------------------- /SupportedDevices.md: -------------------------------------------------------------------------------- 1 | | Brand | Model | Android Version | v1 Goggles | v2 Goggles | 25mbit | 50mbit | Latency | Notes | 2 | |:---------:|:--------------|:-----------------:|:-------------:|:-------------:|:---------:|:---------:|:---------:|:-----:| 3 | | Essential | PH-1 | 10 | 🟢 | ❔ | 🔴 | 🟢 | 200-2000ms|| 4 | | Fairphone | 3 | 10 | ❔ | 🟢 | 🟢 | ❔ | 5+ seconds|| 5 | | Google | Pixel 4XL | 11 | 🟢 | ❔ | ❔ | 🟢 | ~150ms || 6 | | Google | Nexus 7 (2012)| 7.1.2 | ❔ | 🟢 | 🟢 | 🟢 | ~1000+ms | Very high latency but still good enough to show spectators. | 7 | | Huawei | P30 Pro | 10.1 | 🟢 | ❔ | 🔴 | 🔴 | ❔ || 8 | | LG | Nexus 5x | 8.1 | ❔ | 🟢 | ❔ | 🟢 | 600+ms || 9 | | Nokia | 5.1 Plus | 10 | 🟢 | ❔ | 🟢 | 🟢 | ~200ms || 10 | | OnePlus | Nord N10 5G | 10 | 🟢 | ❔ | 🟢 | 🟢 | 100-200ms || 11 | | OnePlus | 8 Pro | 11 | ❔ | 🟢 | 🔴 | 🟢 | ~180ms || 12 | | OnePlus | 7T | 10 | 🟢 | ❔ | ❔ | 🟢 | ~150ms | Sometimes it works very well. Most of the times it shows a few frames at high speed and then freezes or shows frames at a very low rate. Changing cables didn't seem to help it.| 13 | | Samsung | Galaxy S20 | Latest | 🟢 | ❔ | 🔴 | 🟢 | Fast || 14 | | Samsung | S7 Edge | 8.0.0 | ❔ | 🟢 | ❔ | 🟢 | ~220ms || 15 | | Samsung | S20 5G | 11 | 🟢 | ❔ | 🔴 | 🟢 | 200-300ms || 16 | | Samsung | Galaxy S8 | ❔ | 🟢 | ❔ | ❔ | 🟢 | ❔ || 17 | | Samsung | Galaxy S9 | 10 | 🟢 | ❔ | ❔ | 🟢 | High, then low | Occasionally freezes 18 | | Samsung | Note 9 | 10 | 🟢 | ❔ | ❔ | 🟢 | ~150ms|| 19 | | Samsung | S10+ 5G | 11 | 🟢 | ❔ | 🟢 | 🟢 | ~500ms || 20 | | Samsung | Galaxy S7 (not edge)| 8.0.0 | ❔ | 🟢 | ❔ | 🟢 | ~200ms || 21 | | Samsung | S7 | 8.0 | 🟢 | ❔ | 🟢 | 🟢 | <1s|| 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | signingConfigs { 7 | digiview { 8 | try{ 9 | storeFile file(digiviewStoreFile) 10 | storePassword digiviewStorePassword 11 | keyPassword digiviewKeyPassword 12 | keyAlias digiviewKeyAlias 13 | } 14 | catch (ex) { 15 | println("You should define mStoreFile, mStorePassword, mKeyPassword and mKeyAlias in ~/.gradle/gradle.properties.") 16 | } 17 | } 18 | } 19 | 20 | compileSdkVersion 30 21 | buildToolsVersion "30.0.3" 22 | 23 | defaultConfig { 24 | applicationId "com.fpvout.digiview" 25 | minSdkVersion 21 26 | targetSdkVersion 30 27 | versionCode 3 28 | versionName '1.0.0' 29 | resConfigs "en", "de", "fr", "es", "zh", "pt" 30 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | 33 | buildTypes { 34 | debug { 35 | applicationIdSuffix '.debug' 36 | versionNameSuffix '-debug' 37 | signingConfig debug.signingConfig 38 | debuggable true 39 | } 40 | alpha { 41 | //applicationIdSuffix '.alpha' 42 | versionNameSuffix '-alpha' 43 | signingConfig signingConfigs.digiview 44 | } 45 | beta { 46 | //applicationIdSuffix '.beta' 47 | minifyEnabled false 48 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 49 | versionNameSuffix '-beta' 50 | signingConfig signingConfigs.digiview 51 | } 52 | release { 53 | minifyEnabled false 54 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 55 | signingConfig signingConfigs.digiview 56 | } 57 | 58 | } 59 | 60 | compileOptions { 61 | sourceCompatibility JavaVersion.VERSION_1_8 62 | targetCompatibility JavaVersion.VERSION_1_8 63 | } 64 | } 65 | 66 | dependencies { 67 | 68 | implementation 'androidx.appcompat:appcompat:1.2.0' 69 | implementation 'com.google.android.material:material:1.3.0' 70 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 71 | implementation 'com.google.android.exoplayer:exoplayer:2.13.3' 72 | implementation 'io.sentry:sentry-android:4.3.0' 73 | implementation 'androidx.preference:preference:1.1.1' 74 | 75 | testImplementation 'junit:junit:4.+' 76 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 77 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 78 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 17 | 21 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher_alpha-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/ic_launcher_alpha-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/DataCollectionAgreementPopupActivity.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.os.Bundle; 7 | 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.preference.PreferenceManager; 10 | 11 | public class DataCollectionAgreementPopupActivity extends AppCompatActivity { 12 | private SharedPreferences preferences; 13 | private AlertDialog.Builder builder; 14 | 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | preferences = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext()); 18 | builder = new AlertDialog.Builder(this); 19 | initializeUI(); 20 | } 21 | 22 | private void initializeUI() { 23 | builder.setTitle(R.string.data_collection_header) 24 | .setMessage(R.string.data_collection_text) 25 | .setPositiveButton(R.string.data_collection_agree_button, (dialog, which) -> confirmDataCollection()) 26 | .setNegativeButton(R.string.data_collection_deny_button, (dialog, which) -> cancelDataCollection()); 27 | AlertDialog dialog = builder.create(); 28 | dialog.show(); 29 | } 30 | 31 | private void cancelDataCollection() { 32 | preferences.edit() 33 | .putBoolean("dataCollectionAccepted", false) 34 | .putBoolean("dataCollectionReplied", true).apply(); 35 | 36 | Intent intent = new Intent(); 37 | setResult(RESULT_OK, intent); 38 | 39 | finish(); 40 | } 41 | 42 | private void confirmDataCollection() { 43 | preferences.edit() 44 | .putBoolean("dataCollectionAccepted", true) 45 | .putBoolean("dataCollectionReplied", true).apply(); 46 | Intent intent = new Intent(); 47 | setResult(RESULT_OK, intent); 48 | finish(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/H264Extractor.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import com.google.android.exoplayer2.C; 4 | import com.google.android.exoplayer2.Format; 5 | import com.google.android.exoplayer2.extractor.Extractor; 6 | import com.google.android.exoplayer2.extractor.ExtractorInput; 7 | import com.google.android.exoplayer2.extractor.ExtractorOutput; 8 | import com.google.android.exoplayer2.extractor.ExtractorsFactory; 9 | import com.google.android.exoplayer2.extractor.PositionHolder; 10 | import com.google.android.exoplayer2.extractor.SeekMap; 11 | import com.google.android.exoplayer2.extractor.ts.H264Reader; 12 | import com.google.android.exoplayer2.extractor.ts.TsPayloadReader; 13 | import com.google.android.exoplayer2.util.ParsableByteArray; 14 | import com.google.android.exoplayer2.extractor.ts.SeiReader; 15 | 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | 19 | import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; 20 | /** 21 | * Extracts data from H264 bitstreams. 22 | */ 23 | public final class H264Extractor implements Extractor { 24 | /** Factory for {@link H264Extractor} instances. */ 25 | public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new H264Extractor()}; 26 | 27 | private static int MAX_SYNC_FRAME_SIZE = 131072; 28 | 29 | private long firstSampleTimestampUs; 30 | private static long sampleTime = 10000; // todo: try to lower this. it directly infer on speed and latency. this should be equal to 16666 to reach 60fps but works better with lower value 31 | private final H264Reader reader; 32 | private final ParsableByteArray sampleData; 33 | 34 | private boolean startedPacket; 35 | 36 | public H264Extractor() { 37 | this(0); 38 | } 39 | 40 | public H264Extractor(int mMaxSyncFrameSize, int mSampleTime) { 41 | this(0, mMaxSyncFrameSize, mSampleTime); 42 | } 43 | 44 | public H264Extractor(long firstSampleTimestampUs) { 45 | this(firstSampleTimestampUs, MAX_SYNC_FRAME_SIZE, (int) sampleTime); 46 | } 47 | 48 | public H264Extractor(long firstSampleTimestampUs, int mMaxSyncFrameSize, int mSampleTime) { 49 | MAX_SYNC_FRAME_SIZE = mMaxSyncFrameSize; 50 | sampleTime = mSampleTime; 51 | this.firstSampleTimestampUs = firstSampleTimestampUs; 52 | reader = new H264Reader(new SeiReader(new ArrayList()),false,true); 53 | sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); 54 | } 55 | 56 | // Extractor implementation. 57 | @Override 58 | public boolean sniff(ExtractorInput input) throws IOException { 59 | return true; 60 | } 61 | 62 | @Override 63 | public void init(ExtractorOutput output) { 64 | reader.createTracks(output, new TsPayloadReader.TrackIdGenerator(0, 1)); 65 | output.endTracks(); 66 | output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); 67 | } 68 | 69 | @Override 70 | public void seek(long position, long timeUs) { 71 | startedPacket = false; 72 | reader.seek(); 73 | } 74 | 75 | @Override 76 | public void release() { 77 | // Do nothing. 78 | } 79 | 80 | @Override 81 | public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { 82 | int bytesRead = input.read(sampleData.getData(), 0, MAX_SYNC_FRAME_SIZE); 83 | if (bytesRead == C.RESULT_END_OF_INPUT) { 84 | return RESULT_END_OF_INPUT; 85 | } 86 | 87 | // Feed whatever data we have to the reader, regardless of whether the read finished or not. 88 | sampleData.setPosition(0); 89 | sampleData.setLimit(bytesRead); 90 | if (!startedPacket) { 91 | // Pass data to the reader as though it's contained within a single infinitely long packet. 92 | reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); 93 | startedPacket = true; 94 | } 95 | firstSampleTimestampUs+=sampleTime; 96 | reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); 97 | reader.consume(sampleData); 98 | return RESULT_CONTINUE; 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/InputStreamBufferedDataSource.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | 6 | import com.google.android.exoplayer2.C; 7 | import com.google.android.exoplayer2.upstream.DataSource; 8 | import com.google.android.exoplayer2.upstream.DataSpec; 9 | import com.google.android.exoplayer2.upstream.TransferListener; 10 | 11 | import java.io.EOFException; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | 15 | import usb.CircularByteBuffer; 16 | 17 | public class InputStreamBufferedDataSource implements DataSource { 18 | private static final int READ_BUFFER_SIZE = 50 * 1024 * 1024; 19 | private static final String ERROR_THREAD_NOT_INITIALIZED = "Read thread not initialized, call first 'startReadThread()'"; 20 | private static final long READ_TIMEOUT = 200; 21 | 22 | private Context context; 23 | private DataSpec dataSpec; 24 | private InputStream inputStream; 25 | private long bytesRemaining; 26 | private boolean opened; 27 | 28 | private CircularByteBuffer readBuffer; 29 | private Thread receiveThread; 30 | private boolean working; 31 | 32 | 33 | public InputStreamBufferedDataSource(Context context, DataSpec dataSpec, InputStream inputStream) { 34 | this.context = context; 35 | this.dataSpec = dataSpec; 36 | this.inputStream = inputStream; 37 | startReadThread(); 38 | } 39 | 40 | @Override 41 | public void addTransferListener(TransferListener transferListener) { 42 | 43 | } 44 | 45 | @Override 46 | public long open(DataSpec dataSpec) throws IOException { 47 | try { 48 | long skipped = inputStream.skip(dataSpec.position); 49 | if (skipped < dataSpec.position) 50 | throw new EOFException(); 51 | 52 | if (dataSpec.length != C.LENGTH_UNSET) { 53 | bytesRemaining = dataSpec.length; 54 | } else { 55 | bytesRemaining = C.LENGTH_UNSET; 56 | } 57 | } catch (IOException e) { 58 | throw new IOException(e); 59 | } 60 | 61 | opened = true; 62 | return bytesRemaining; 63 | } 64 | 65 | @Override 66 | public int read(byte[] buffer, int offset, int readLength) throws IOException { 67 | if (readBuffer == null) 68 | throw new IOException(ERROR_THREAD_NOT_INITIALIZED); 69 | 70 | long deadLine = System.currentTimeMillis() + READ_TIMEOUT; 71 | int readBytes = 0; 72 | while (System.currentTimeMillis() < deadLine && readBytes <= 0) 73 | readBytes = readBuffer.read(buffer, offset, readLength); 74 | if (readBytes <= 0) 75 | return readBytes; 76 | return readBytes; 77 | } 78 | 79 | public void startReadThread(){ 80 | if (!working) { 81 | working = true; 82 | readBuffer = new CircularByteBuffer(READ_BUFFER_SIZE); 83 | receiveThread = new Thread() { 84 | @Override 85 | public void run() { 86 | while (working) { 87 | byte[] buffer = new byte[1024]; 88 | int receivedBytes = 0; 89 | try { 90 | receivedBytes = inputStream.read(buffer, 0, buffer.length); 91 | } catch (IOException e) { 92 | e.printStackTrace(); 93 | } 94 | if (receivedBytes > 0) { 95 | readBuffer.write(buffer, 0, receivedBytes); 96 | } 97 | } 98 | } 99 | }; 100 | receiveThread.start(); 101 | } 102 | } 103 | 104 | @Override 105 | public Uri getUri() { 106 | return dataSpec.uri; 107 | } 108 | 109 | @Override 110 | public void close() throws IOException { 111 | working = false; 112 | if (receiveThread != null){ 113 | receiveThread.interrupt(); 114 | } 115 | try { 116 | if (inputStream != null) { 117 | inputStream.close(); 118 | } 119 | } catch (IOException e) { 120 | throw new IOException(e); 121 | } finally { 122 | inputStream = null; 123 | if (opened) { 124 | opened = false; 125 | } 126 | } 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/InputStreamDataSource.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | 6 | import com.google.android.exoplayer2.C; 7 | import com.google.android.exoplayer2.upstream.DataSource; 8 | import com.google.android.exoplayer2.upstream.DataSpec; 9 | import com.google.android.exoplayer2.upstream.TransferListener; 10 | 11 | import java.io.EOFException; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | 15 | public class InputStreamDataSource implements DataSource { 16 | private Context context; 17 | private DataSpec dataSpec; 18 | private InputStream inputStream; 19 | private long bytesRemaining; 20 | private boolean opened; 21 | 22 | public InputStreamDataSource(Context context, DataSpec dataSpec, InputStream inputStream) { 23 | this.context = context; 24 | this.dataSpec = dataSpec; 25 | this.inputStream = inputStream; 26 | } 27 | 28 | @Override 29 | public void addTransferListener(TransferListener transferListener) { 30 | 31 | } 32 | 33 | @Override 34 | public long open(DataSpec dataSpec) throws IOException { 35 | try { 36 | long skipped = inputStream.skip(dataSpec.position); 37 | if (skipped < dataSpec.position) 38 | throw new EOFException(); 39 | 40 | if (dataSpec.length != C.LENGTH_UNSET) { 41 | bytesRemaining = dataSpec.length; 42 | } else { 43 | bytesRemaining = C.LENGTH_UNSET; 44 | } 45 | } catch (IOException e) { 46 | throw new IOException(e); 47 | } 48 | 49 | opened = true; 50 | return bytesRemaining; 51 | } 52 | 53 | @Override 54 | public int read(byte[] buffer, int offset, int readLength) throws IOException { 55 | return inputStream.read(buffer, offset, readLength); 56 | } 57 | 58 | @Override 59 | public Uri getUri() { 60 | return dataSpec.uri; 61 | } 62 | 63 | @Override 64 | public void close() throws IOException { 65 | try { 66 | if (inputStream != null) { 67 | inputStream.close(); 68 | } 69 | } catch (IOException e) { 70 | throw new IOException(e); 71 | } finally { 72 | inputStream = null; 73 | if (opened) { 74 | opened = false; 75 | } 76 | } 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.LayoutTransition; 6 | import android.app.PendingIntent; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.IntentFilter; 10 | import android.content.SharedPreferences; 11 | import android.hardware.usb.UsbDevice; 12 | import android.hardware.usb.UsbManager; 13 | import android.os.Bundle; 14 | import android.os.Handler; 15 | import android.util.Log; 16 | import android.view.GestureDetector; 17 | import android.view.MotionEvent; 18 | import android.view.ScaleGestureDetector; 19 | import android.view.SurfaceView; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.view.WindowManager; 23 | 24 | import androidx.appcompat.app.ActionBar; 25 | import androidx.appcompat.app.AppCompatActivity; 26 | import androidx.preference.PreferenceManager; 27 | 28 | import java.util.HashMap; 29 | 30 | import io.sentry.SentryLevel; 31 | import io.sentry.android.core.SentryAndroid; 32 | 33 | import static com.fpvout.digiview.VideoReaderExoplayer.VideoZoomedIn; 34 | 35 | public class MainActivity extends AppCompatActivity implements UsbDeviceListener { 36 | private static final String ACTION_USB_PERMISSION = "com.fpvout.digiview.USB_PERMISSION"; 37 | private static final String TAG = "DIGIVIEW"; 38 | private static final int VENDOR_ID = 11427; 39 | private static final int PRODUCT_ID = 31; 40 | private int shortAnimationDuration; 41 | private float buttonAlpha = 1; 42 | private View settingsButton; 43 | private View watermarkView; 44 | private OverlayView overlayView; 45 | PendingIntent permissionIntent; 46 | UsbDeviceBroadcastReceiver usbDeviceBroadcastReceiver; 47 | UsbManager usbManager; 48 | UsbDevice usbDevice; 49 | UsbMaskConnection mUsbMaskConnection; 50 | VideoReaderExoplayer mVideoReader; 51 | boolean usbConnected = false; 52 | SurfaceView fpvView; 53 | private GestureDetector gestureDetector; 54 | private ScaleGestureDetector scaleGestureDetector; 55 | private SharedPreferences sharedPreferences; 56 | private static final String ShowWatermark = "ShowWatermark"; 57 | 58 | @Override 59 | protected void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | Log.d(TAG, "APP - On Create"); 62 | setContentView(R.layout.activity_main); 63 | 64 | // check Data Collection agreement 65 | checkDataCollectionAgreement(); 66 | 67 | // Hide top bar and status bar 68 | View decorView = getWindow().getDecorView(); 69 | decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 70 | | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 71 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 72 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 73 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 74 | | View.SYSTEM_UI_FLAG_FULLSCREEN); 75 | ActionBar actionBar = getSupportActionBar(); 76 | if (actionBar != null) { 77 | actionBar.hide(); 78 | } 79 | 80 | // Prevent screen from sleeping 81 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 82 | 83 | usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); 84 | permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); 85 | usbDeviceBroadcastReceiver = new UsbDeviceBroadcastReceiver(this); 86 | 87 | IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); 88 | registerReceiver(usbDeviceBroadcastReceiver, filter); 89 | IntentFilter filterDetached = new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED); 90 | registerReceiver(usbDeviceBroadcastReceiver, filterDetached); 91 | 92 | shortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); 93 | watermarkView = findViewById(R.id.watermarkView); 94 | overlayView = findViewById(R.id.overlayView); 95 | fpvView = findViewById(R.id.fpvView); 96 | 97 | settingsButton = findViewById(R.id.settingsButton); 98 | settingsButton.setOnClickListener(new View.OnClickListener() { 99 | @Override 100 | public void onClick(View v) { 101 | Intent intent = new Intent(v.getContext(), SettingsActivity.class); 102 | v.getContext().startActivity(intent); 103 | } 104 | }); 105 | 106 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 107 | 108 | // Enable resizing animations 109 | ((ViewGroup) findViewById(R.id.mainLayout)).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); 110 | 111 | setupGestureDetectors(); 112 | 113 | mUsbMaskConnection = new UsbMaskConnection(); 114 | Handler videoReaderEventListener = new Handler(this.getMainLooper(), msg -> onVideoReaderEvent((VideoReaderExoplayer.VideoReaderEventMessageCode) msg.obj)); 115 | 116 | mVideoReader = new VideoReaderExoplayer(fpvView, this, videoReaderEventListener); 117 | 118 | if (!usbConnected) { 119 | if (searchDevice()) { 120 | connect(); 121 | } else { 122 | showOverlay(R.string.waiting_for_usb_device, OverlayStatus.Disconnected); 123 | } 124 | } 125 | } 126 | 127 | private void setupGestureDetectors() { 128 | gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { 129 | @Override 130 | public boolean onSingleTapConfirmed(MotionEvent e) { 131 | toggleSettingsButton(); 132 | return super.onSingleTapConfirmed(e); 133 | } 134 | 135 | @Override 136 | public boolean onDoubleTap(MotionEvent e) { 137 | mVideoReader.toggleZoom(); 138 | return super.onDoubleTap(e); 139 | } 140 | }); 141 | 142 | scaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener() { 143 | @Override 144 | public void onScaleEnd(ScaleGestureDetector detector) { 145 | if (detector.getScaleFactor() < 1) { 146 | mVideoReader.zoomOut(); 147 | } else { 148 | mVideoReader.zoomIn(); 149 | } 150 | } 151 | }); 152 | } 153 | 154 | @Override 155 | public boolean onTouchEvent(MotionEvent event) { 156 | gestureDetector.onTouchEvent(event); 157 | scaleGestureDetector.onTouchEvent(event); 158 | 159 | return super.onTouchEvent(event); 160 | } 161 | 162 | private void updateWatermark() { 163 | if (overlayView.getVisibility() == View.VISIBLE) { 164 | watermarkView.setAlpha(0); 165 | return; 166 | } 167 | 168 | if (sharedPreferences.getBoolean(ShowWatermark, true)) { 169 | watermarkView.setAlpha(0.3F); 170 | } else { 171 | watermarkView.setAlpha(0F); 172 | } 173 | } 174 | 175 | private void updateVideoZoom() { 176 | if (sharedPreferences.getBoolean(VideoZoomedIn, true)) { 177 | mVideoReader.zoomIn(); 178 | } else { 179 | mVideoReader.zoomOut(); 180 | } 181 | } 182 | 183 | private void cancelButtonAnimation() { 184 | Handler handler = settingsButton.getHandler(); 185 | if (handler != null) { 186 | settingsButton.getHandler().removeCallbacksAndMessages(null); 187 | } 188 | } 189 | 190 | private void showSettingsButton() { 191 | cancelButtonAnimation(); 192 | 193 | if (overlayView.getVisibility() == View.VISIBLE) { 194 | buttonAlpha = 1; 195 | settingsButton.setAlpha(1); 196 | } 197 | } 198 | 199 | private void toggleSettingsButton() { 200 | if (buttonAlpha == 1 && overlayView.getVisibility() == View.VISIBLE) return; 201 | 202 | // cancel any pending delayed animations first 203 | cancelButtonAnimation(); 204 | 205 | if (buttonAlpha == 1) { 206 | buttonAlpha = 0; 207 | } else { 208 | buttonAlpha = 1; 209 | } 210 | 211 | settingsButton.animate() 212 | .alpha(buttonAlpha) 213 | .setDuration(shortAnimationDuration) 214 | .setListener(new AnimatorListenerAdapter() { 215 | @Override 216 | public void onAnimationEnd(Animator animation) { 217 | autoHideSettingsButton(); 218 | } 219 | }); 220 | } 221 | 222 | private void autoHideSettingsButton() { 223 | if (overlayView.getVisibility() == View.VISIBLE) return; 224 | if (buttonAlpha == 0) return; 225 | 226 | settingsButton.postDelayed(new Runnable() { 227 | @Override 228 | public void run() { 229 | buttonAlpha = 0; 230 | settingsButton.animate() 231 | .alpha(0) 232 | .setDuration(shortAnimationDuration); 233 | } 234 | }, 3000); 235 | } 236 | 237 | @Override 238 | public void usbDeviceApproved(UsbDevice device) { 239 | Log.i(TAG, "USB - usbDevice approved"); 240 | usbDevice = device; 241 | showOverlay(R.string.usb_device_approved, OverlayStatus.Connected); 242 | connect(); 243 | } 244 | 245 | @Override 246 | public void usbDeviceDetached() { 247 | Log.i(TAG, "USB - usbDevice detached"); 248 | showOverlay(R.string.usb_device_detached_waiting, OverlayStatus.Disconnected); 249 | this.onStop(); 250 | } 251 | 252 | private boolean searchDevice() { 253 | HashMap deviceList = usbManager.getDeviceList(); 254 | if (deviceList.size() <= 0) { 255 | usbDevice = null; 256 | return false; 257 | } 258 | 259 | for (UsbDevice device : deviceList.values()) { 260 | if (device.getVendorId() == VENDOR_ID && device.getProductId() == PRODUCT_ID) { 261 | if (usbManager.hasPermission(device)) { 262 | Log.i(TAG, "USB - usbDevice attached"); 263 | showOverlay(R.string.usb_device_found, OverlayStatus.Connected); 264 | usbDevice = device; 265 | return true; 266 | } 267 | 268 | usbManager.requestPermission(device, permissionIntent); 269 | } 270 | } 271 | 272 | return false; 273 | } 274 | 275 | private void connect() { 276 | usbConnected = true; 277 | mUsbMaskConnection.setUsbDevice(usbManager.openDevice(usbDevice), usbDevice); 278 | mVideoReader.setUsbMaskConnection(mUsbMaskConnection); 279 | overlayView.hide(); 280 | mVideoReader.start(); 281 | updateWatermark(); 282 | autoHideSettingsButton(); 283 | showOverlay(R.string.waiting_for_video, OverlayStatus.Connected); 284 | } 285 | 286 | @Override 287 | public void onResume() { 288 | super.onResume(); 289 | Log.d(TAG, "APP - On Resume"); 290 | 291 | View decorView = getWindow().getDecorView(); 292 | decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 293 | | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 294 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 295 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 296 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 297 | | View.SYSTEM_UI_FLAG_FULLSCREEN); 298 | ActionBar actionBar = getSupportActionBar(); 299 | if (actionBar != null) { 300 | actionBar.hide(); 301 | } 302 | 303 | if (!usbConnected) { 304 | if (searchDevice()) { 305 | Log.d(TAG, "APP - On Resume usbDevice device found"); 306 | connect(); 307 | } else { 308 | showOverlay(R.string.waiting_for_usb_device, OverlayStatus.Connected); 309 | } 310 | } 311 | 312 | settingsButton.setAlpha(1); 313 | autoHideSettingsButton(); 314 | updateWatermark(); 315 | updateVideoZoom(); 316 | } 317 | 318 | private boolean onVideoReaderEvent(VideoReaderExoplayer.VideoReaderEventMessageCode m) { 319 | if (VideoReaderExoplayer.VideoReaderEventMessageCode.WAITING_FOR_VIDEO.equals(m)) { 320 | Log.d(TAG, "event: WAITING_FOR_VIDEO"); 321 | showOverlay(R.string.waiting_for_video, OverlayStatus.Connected); 322 | } else if (VideoReaderExoplayer.VideoReaderEventMessageCode.VIDEO_PLAYING.equals(m)) { 323 | Log.d(TAG, "event: VIDEO_PLAYING"); 324 | hideOverlay(); 325 | } 326 | return false; // false to continue listening 327 | } 328 | 329 | private void showOverlay(int textId, OverlayStatus connected) { 330 | overlayView.show(textId, connected); 331 | updateWatermark(); 332 | showSettingsButton(); 333 | } 334 | 335 | private void hideOverlay() { 336 | overlayView.hide(); 337 | updateWatermark(); 338 | showSettingsButton(); 339 | autoHideSettingsButton(); 340 | } 341 | 342 | @Override 343 | protected void onStop() { 344 | super.onStop(); 345 | Log.d(TAG, "APP - On Stop"); 346 | 347 | mUsbMaskConnection.stop(); 348 | mVideoReader.stop(); 349 | usbConnected = false; 350 | } 351 | 352 | @Override 353 | protected void onPause() { 354 | super.onPause(); 355 | Log.d(TAG, "APP - On Pause"); 356 | 357 | mUsbMaskConnection.stop(); 358 | mVideoReader.stop(); 359 | usbConnected = false; 360 | } 361 | 362 | @Override 363 | protected void onDestroy() { 364 | super.onDestroy(); 365 | Log.d(TAG, "APP - On Destroy"); 366 | 367 | mUsbMaskConnection.stop(); 368 | mVideoReader.stop(); 369 | usbConnected = false; 370 | } 371 | 372 | @Override 373 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 374 | super.onActivityResult(requestCode, resultCode, data); 375 | 376 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 377 | boolean dataCollectionAccepted = preferences.getBoolean("dataCollectionAccepted", false); 378 | 379 | if (requestCode == 1) { // Data Collection agreement Activity 380 | if (resultCode == RESULT_OK && dataCollectionAccepted) { 381 | SentryAndroid.init(this, options -> options.setBeforeSend((event, hint) -> { 382 | if (SentryLevel.DEBUG.equals(event.getLevel())) 383 | return null; 384 | else 385 | return event; 386 | })); 387 | } 388 | 389 | } 390 | } //onActivityResult 391 | 392 | private void checkDataCollectionAgreement() { 393 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 394 | boolean dataCollectionAccepted = preferences.getBoolean("dataCollectionAccepted", false); 395 | boolean dataCollectionReplied = preferences.getBoolean("dataCollectionReplied", false); 396 | if (!dataCollectionReplied) { 397 | Intent intent = new Intent(this, DataCollectionAgreementPopupActivity.class); 398 | startActivityForResult(intent, 1); 399 | } else if (dataCollectionAccepted) { 400 | SentryAndroid.init(this, options -> options.setBeforeSend((event, hint) -> { 401 | if (SentryLevel.DEBUG.equals(event.getLevel())) 402 | return null; 403 | else 404 | return event; 405 | })); 406 | } 407 | 408 | } 409 | 410 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/OverlayStatus.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | public enum OverlayStatus { 4 | Connected, 5 | Disconnected, 6 | Error 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/OverlayView.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import androidx.constraintlayout.widget.ConstraintLayout; 10 | 11 | public class OverlayView extends ConstraintLayout { 12 | private final TextView textView; 13 | private final ImageView imageView; 14 | 15 | public OverlayView(Context context, AttributeSet attrs) { 16 | super(context, attrs); 17 | 18 | inflate(getContext(),R.layout.backdrop_view,this); 19 | 20 | textView = findViewById(R.id.backdrop_text); 21 | 22 | imageView = findViewById(R.id.backdrop_image); 23 | } 24 | 25 | public void hide(){ 26 | setVisibility(View.GONE); 27 | } 28 | 29 | public void show(int textResourceId, OverlayStatus status){ 30 | showInfo(getContext().getString(textResourceId), status); 31 | } 32 | 33 | private void showInfo(String text, OverlayStatus status){ 34 | 35 | setVisibility(View.VISIBLE); 36 | 37 | textView.setText(text); 38 | 39 | int image = R.drawable.ic_goggles_white; 40 | switch(status){ 41 | case Disconnected: 42 | image = R.drawable.ic_goggles_disconnected_white; 43 | break; 44 | case Error: 45 | image = R.drawable.ic_goggles_disconnected_red; 46 | break; 47 | } 48 | 49 | imageView.setImageResource(image); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/PerformancePreset.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | public class PerformancePreset { 4 | int h264ReaderMaxSyncFrameSize = 131072; 5 | int h264ReaderSampleTime = 10000; 6 | int exoPlayerMinBufferMs = 500; 7 | int exoPlayerMaxBufferMs = 2000; 8 | int exoPlayerBufferForPlaybackMs = 17; 9 | int exoPlayerBufferForPlaybackAfterRebufferMs = 17; 10 | DataSourceType dataSourceType = DataSourceType.INPUT_STREAM; 11 | 12 | private PerformancePreset(){ 13 | 14 | } 15 | 16 | private PerformancePreset(int mH264ReaderMaxSyncFrameSize, int mH264ReaderSampleTime, int mExoPlayerMinBufferMs, int mExoPlayerMaxBufferMs, int mExoPlayerBufferForPlaybackMs, int mExoPlayerBufferForPlaybackAfterRebufferMs, DataSourceType mDataSourceType){ 17 | h264ReaderMaxSyncFrameSize = mH264ReaderMaxSyncFrameSize; 18 | h264ReaderSampleTime = mH264ReaderSampleTime; 19 | exoPlayerMinBufferMs = mExoPlayerMinBufferMs; 20 | exoPlayerMaxBufferMs = mExoPlayerMaxBufferMs; 21 | exoPlayerBufferForPlaybackMs = mExoPlayerBufferForPlaybackMs; 22 | exoPlayerBufferForPlaybackAfterRebufferMs = mExoPlayerBufferForPlaybackAfterRebufferMs; 23 | dataSourceType = mDataSourceType; 24 | } 25 | 26 | static PerformancePreset getPreset(PresetType p) { 27 | switch (p) { 28 | case CONSERVATIVE: 29 | return new PerformancePreset(131072, 14000, 500, 2000, 34, 34, DataSourceType.INPUT_STREAM); 30 | case AGGRESSIVE: 31 | return new PerformancePreset(131072, 7000, 50, 2000, 17, 17, DataSourceType.INPUT_STREAM); 32 | case LEGACY: 33 | return new PerformancePreset(30720, 200, 32768, 65536, 0, 0, DataSourceType.BUFFERED_INPUT_STREAM); 34 | case LEGACY_BUFFERED: 35 | return new PerformancePreset(30720, 300, 32768, 65536, 34, 34, DataSourceType.BUFFERED_INPUT_STREAM); 36 | case DEFAULT: 37 | default: 38 | return new PerformancePreset(131072, 10000, 500, 2000, 17, 17, DataSourceType.INPUT_STREAM); 39 | } 40 | } 41 | 42 | public enum DataSourceType { 43 | INPUT_STREAM, 44 | BUFFERED_INPUT_STREAM 45 | } 46 | 47 | static PerformancePreset getPreset(String p) { 48 | switch (p) { 49 | case "conservative": 50 | return getPreset(PresetType.CONSERVATIVE); 51 | case "aggressive": 52 | return getPreset(PresetType.AGGRESSIVE); 53 | case "legacy": 54 | return getPreset(PresetType.LEGACY); 55 | case "new_legacy": 56 | return getPreset(PresetType.LEGACY_BUFFERED); 57 | case "default": 58 | default: 59 | return getPreset(PresetType.DEFAULT); 60 | } 61 | } 62 | 63 | public enum PresetType { 64 | DEFAULT, 65 | CONSERVATIVE, 66 | AGGRESSIVE, 67 | LEGACY, 68 | LEGACY_BUFFERED 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return "PerformancePreset{" + 74 | "h264ReaderMaxSyncFrameSize=" + h264ReaderMaxSyncFrameSize + 75 | ", h264ReaderSampleTime=" + h264ReaderSampleTime + 76 | ", exoPlayerMinBufferMs=" + exoPlayerMinBufferMs + 77 | ", exoPlayerMaxBufferMs=" + exoPlayerMaxBufferMs + 78 | ", exoPlayerBufferForPlaybackMs=" + exoPlayerBufferForPlaybackMs + 79 | ", exoPlayerBufferForPlaybackAfterRebufferMs=" + exoPlayerBufferForPlaybackAfterRebufferMs + 80 | ", dataSourceType=" + dataSourceType + 81 | '}'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.os.Bundle; 4 | import android.view.MenuItem; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.appcompat.app.ActionBar; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.preference.PreferenceFragmentCompat; 10 | 11 | public class SettingsActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.settings_activity); 17 | if (savedInstanceState == null) { 18 | getSupportFragmentManager() 19 | .beginTransaction() 20 | .replace(R.id.settings, new SettingsFragment()) 21 | .commit(); 22 | } 23 | ActionBar actionBar = getSupportActionBar(); 24 | if (actionBar != null) { 25 | actionBar.setDisplayHomeAsUpEnabled(true); 26 | } 27 | } 28 | 29 | @Override 30 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 31 | 32 | if (item.getItemId() == android.R.id.home) { 33 | this.finish(); 34 | return true; 35 | } 36 | 37 | return super.onOptionsItemSelected(item); 38 | } 39 | 40 | public static class SettingsFragment extends PreferenceFragmentCompat { 41 | @Override 42 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 43 | setPreferencesFromResource(R.xml.root_preferences, rootKey); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/UsbDeviceBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.hardware.usb.UsbDevice; 7 | import android.hardware.usb.UsbManager; 8 | 9 | public class UsbDeviceBroadcastReceiver extends BroadcastReceiver { 10 | private static final String ACTION_USB_PERMISSION = "com.fpvout.digiview.USB_PERMISSION"; 11 | private final UsbDeviceListener listener; 12 | 13 | public UsbDeviceBroadcastReceiver(UsbDeviceListener listener ){ 14 | this.listener = listener; 15 | } 16 | 17 | @Override 18 | public void onReceive(Context context, Intent intent) { 19 | String action = intent.getAction(); 20 | if (ACTION_USB_PERMISSION.equals(action)) { 21 | UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); 22 | 23 | if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { 24 | if(device != null){ 25 | listener.usbDeviceApproved(device); 26 | } 27 | } 28 | } 29 | 30 | if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { 31 | listener.usbDeviceDetached(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/UsbDeviceListener.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.hardware.usb.UsbDevice; 4 | 5 | public interface UsbDeviceListener { 6 | void usbDeviceApproved(UsbDevice device); 7 | void usbDeviceDetached(); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.hardware.usb.UsbDevice; 4 | import android.hardware.usb.UsbDeviceConnection; 5 | import android.hardware.usb.UsbInterface; 6 | 7 | import java.io.IOException; 8 | 9 | import usb.AndroidUSBInputStream; 10 | import usb.AndroidUSBOutputStream; 11 | 12 | public class UsbMaskConnection { 13 | 14 | private final byte[] magicPacket = "RMVT".getBytes(); 15 | private UsbDeviceConnection usbConnection; 16 | private UsbDevice device; 17 | private UsbInterface usbInterface; 18 | AndroidUSBInputStream mInputStream; 19 | AndroidUSBOutputStream mOutputStream; 20 | private boolean ready = false; 21 | 22 | public UsbMaskConnection() { 23 | } 24 | 25 | public void setUsbDevice(UsbDeviceConnection c, UsbDevice d) { 26 | usbConnection = c; 27 | device = d; 28 | usbInterface = device.getInterface(3); 29 | 30 | usbConnection.claimInterface(usbInterface,true); 31 | 32 | mOutputStream = new AndroidUSBOutputStream(usbInterface.getEndpoint(0), usbConnection); 33 | mInputStream = new AndroidUSBInputStream(usbInterface.getEndpoint(1), usbInterface.getEndpoint(0), usbConnection); 34 | ready = true; 35 | } 36 | 37 | public void start(){ 38 | mOutputStream.write(magicPacket); 39 | } 40 | 41 | public void stop() { 42 | ready = false; 43 | try { 44 | if (mInputStream != null) 45 | mInputStream.close(); 46 | 47 | if (mOutputStream != null) 48 | mOutputStream.close(); 49 | } catch (IOException e) { 50 | e.printStackTrace(); 51 | } 52 | 53 | if (usbConnection != null) { 54 | usbConnection.releaseInterface(usbInterface); 55 | usbConnection.close(); 56 | } 57 | } 58 | 59 | public boolean isReady() { 60 | return ready; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/VersionPreference.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.util.AttributeSet; 7 | 8 | import androidx.preference.Preference; 9 | 10 | // From https://stackoverflow.com/a/20157577/877465 11 | public class VersionPreference extends Preference { 12 | public VersionPreference(Context context, AttributeSet attrs) { 13 | super(context, attrs); 14 | String versionName; 15 | final PackageManager packageManager = context.getPackageManager(); 16 | if (packageManager != null) { 17 | try { 18 | PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); 19 | versionName = packageInfo.versionName; 20 | } catch (PackageManager.NameNotFoundException e) { 21 | versionName = null; 22 | } 23 | setSummary(versionName); 24 | } 25 | 26 | setSelectable(false); 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fpvout/digiview/VideoReaderExoplayer.java: -------------------------------------------------------------------------------- 1 | package com.fpvout.digiview; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.net.Uri; 6 | import android.os.Handler; 7 | import android.os.Looper; 8 | import android.os.Message; 9 | import android.util.Log; 10 | import android.view.SurfaceView; 11 | 12 | import androidx.constraintlayout.widget.ConstraintLayout; 13 | import androidx.preference.PreferenceManager; 14 | 15 | import com.google.android.exoplayer2.C; 16 | import com.google.android.exoplayer2.DefaultLoadControl; 17 | import com.google.android.exoplayer2.ExoPlaybackException; 18 | import com.google.android.exoplayer2.ExoPlayer; 19 | import com.google.android.exoplayer2.Format; 20 | import com.google.android.exoplayer2.MediaItem; 21 | import com.google.android.exoplayer2.Player; 22 | import com.google.android.exoplayer2.SimpleExoPlayer; 23 | import com.google.android.exoplayer2.extractor.Extractor; 24 | import com.google.android.exoplayer2.extractor.ExtractorsFactory; 25 | import com.google.android.exoplayer2.source.MediaSource; 26 | import com.google.android.exoplayer2.source.ProgressiveMediaSource; 27 | import com.google.android.exoplayer2.upstream.DataSource; 28 | import com.google.android.exoplayer2.upstream.DataSpec; 29 | import com.google.android.exoplayer2.util.NonNullApi; 30 | import com.google.android.exoplayer2.video.VideoListener; 31 | 32 | import usb.AndroidUSBInputStream; 33 | 34 | public class VideoReaderExoplayer { 35 | private static final String TAG = "DIGIVIEW"; 36 | private Handler videoReaderEventListener; 37 | private SimpleExoPlayer mPlayer; 38 | static final String VideoPreset = "VideoPreset"; 39 | private final SurfaceView surfaceView; 40 | private AndroidUSBInputStream inputStream; 41 | private UsbMaskConnection mUsbMaskConnection; 42 | private boolean zoomedIn; 43 | private final Context context; 44 | private PerformancePreset performancePreset = PerformancePreset.getPreset(PerformancePreset.PresetType.DEFAULT); 45 | static final String VideoZoomedIn = "VideoZoomedIn"; 46 | private final SharedPreferences sharedPreferences; 47 | 48 | VideoReaderExoplayer(SurfaceView videoSurface, Context c) { 49 | surfaceView = videoSurface; 50 | context = c; 51 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(c); 52 | } 53 | 54 | VideoReaderExoplayer(SurfaceView videoSurface, Context c, Handler v) { 55 | this(videoSurface, c); 56 | videoReaderEventListener = v; 57 | } 58 | 59 | public void setUsbMaskConnection(UsbMaskConnection connection) { 60 | mUsbMaskConnection = connection; 61 | inputStream = mUsbMaskConnection.mInputStream; 62 | } 63 | 64 | public void start() { 65 | zoomedIn = sharedPreferences.getBoolean(VideoZoomedIn, true); 66 | performancePreset = PerformancePreset.getPreset(sharedPreferences.getString(VideoPreset, "default")); 67 | 68 | DefaultLoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(performancePreset.exoPlayerMinBufferMs, performancePreset.exoPlayerMaxBufferMs, performancePreset.exoPlayerBufferForPlaybackMs, performancePreset.exoPlayerBufferForPlaybackAfterRebufferMs).build(); 69 | mPlayer = new SimpleExoPlayer.Builder(context).setLoadControl(loadControl).build(); 70 | mPlayer.setVideoSurfaceView(surfaceView); 71 | mPlayer.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING); 72 | mPlayer.setWakeMode(C.WAKE_MODE_LOCAL); 73 | 74 | DataSpec dataSpec = new DataSpec(Uri.EMPTY, 0, C.LENGTH_UNSET); 75 | 76 | Log.d(TAG, "preset: " + performancePreset); 77 | 78 | DataSource.Factory dataSourceFactory = () -> { 79 | switch (performancePreset.dataSourceType){ 80 | case INPUT_STREAM: 81 | return (DataSource) new InputStreamDataSource(context, dataSpec, inputStream); 82 | case BUFFERED_INPUT_STREAM: 83 | default: 84 | return (DataSource) new InputStreamBufferedDataSource(context, dataSpec, inputStream); 85 | } 86 | }; 87 | 88 | ExtractorsFactory extractorsFactory = () ->new Extractor[] {new H264Extractor(performancePreset.h264ReaderMaxSyncFrameSize, performancePreset.h264ReaderSampleTime)}; 89 | MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory).createMediaSource(MediaItem.fromUri(Uri.EMPTY)); 90 | mPlayer.setMediaSource(mediaSource); 91 | 92 | mPlayer.prepare(); 93 | mPlayer.play(); 94 | mPlayer.addListener(new ExoPlayer.EventListener() { 95 | @Override 96 | @NonNullApi 97 | public void onPlayerError(ExoPlaybackException error) { 98 | switch (error.type) { 99 | case ExoPlaybackException.TYPE_SOURCE: 100 | Log.e(TAG, "PLAYER_SOURCE - TYPE_SOURCE: " + error.getSourceException().getMessage()); 101 | (new Handler(Looper.getMainLooper())).postDelayed(() -> restart(), 1000); 102 | break; 103 | case ExoPlaybackException.TYPE_REMOTE: 104 | Log.e(TAG, "PLAYER_SOURCE - TYPE_REMOTE: " + error.getSourceException().getMessage()); 105 | break; 106 | case ExoPlaybackException.TYPE_RENDERER: 107 | Log.e(TAG, "PLAYER_SOURCE - TYPE_RENDERER: " + error.getSourceException().getMessage()); 108 | break; 109 | case ExoPlaybackException.TYPE_UNEXPECTED: 110 | Log.e(TAG, "PLAYER_SOURCE - TYPE_UNEXPECTED: " + error.getSourceException().getMessage()); 111 | break; 112 | } 113 | } 114 | 115 | @Override 116 | public void onPlaybackStateChanged(@NonNullApi int state) { 117 | switch (state) { 118 | case Player.STATE_IDLE: 119 | case Player.STATE_READY: 120 | case Player.STATE_BUFFERING: 121 | break; 122 | case Player.STATE_ENDED: 123 | Log.d(TAG, "PLAYER_STATE - ENDED"); 124 | sendEvent(VideoReaderEventMessageCode.WAITING_FOR_VIDEO); // let MainActivity know so it can hide watermark/show settings button 125 | (new Handler(Looper.getMainLooper())).postDelayed(() -> restart(), 1000); 126 | break; 127 | } 128 | } 129 | }); 130 | 131 | mPlayer.addVideoListener(new VideoListener() { 132 | @Override 133 | public void onRenderedFirstFrame() { 134 | Log.d(TAG, "PLAYER_RENDER - FIRST FRAME"); 135 | sendEvent(VideoReaderEventMessageCode.VIDEO_PLAYING); // let MainActivity know so it can hide watermark/show settings button 136 | } 137 | 138 | @Override 139 | public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { 140 | if (!zoomedIn) { 141 | ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) surfaceView.getLayoutParams(); 142 | params.dimensionRatio = width + ":" + height; 143 | surfaceView.setLayoutParams(params); 144 | } 145 | } 146 | }); 147 | } 148 | 149 | private void sendEvent(VideoReaderEventMessageCode eventCode) { 150 | if (videoReaderEventListener != null) { // let MainActivity know so it can hide watermark/show settings button 151 | Message videoReaderEventMessage = new Message(); 152 | videoReaderEventMessage.obj = eventCode; 153 | videoReaderEventListener.sendMessage(videoReaderEventMessage); 154 | } 155 | } 156 | 157 | public void toggleZoom() { 158 | zoomedIn = !zoomedIn; 159 | 160 | SharedPreferences.Editor preferencesEditor = sharedPreferences.edit(); 161 | preferencesEditor.putBoolean(VideoZoomedIn, zoomedIn); 162 | preferencesEditor.apply(); 163 | 164 | ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) surfaceView.getLayoutParams(); 165 | 166 | if (zoomedIn) { 167 | params.dimensionRatio = ""; 168 | } else { 169 | if (mPlayer == null) return; 170 | Format videoFormat = mPlayer.getVideoFormat(); 171 | if (videoFormat == null) return; 172 | 173 | params.dimensionRatio = videoFormat.width + ":" + videoFormat.height; 174 | } 175 | 176 | surfaceView.setLayoutParams(params); 177 | } 178 | 179 | public void zoomIn() { 180 | if (!zoomedIn) { 181 | toggleZoom(); 182 | } 183 | } 184 | 185 | public void zoomOut() { 186 | if (zoomedIn) { 187 | toggleZoom(); 188 | } 189 | } 190 | 191 | public void restart() { 192 | mPlayer.release(); 193 | 194 | if (mUsbMaskConnection.isReady()) { 195 | mUsbMaskConnection.start(); 196 | start(); 197 | } 198 | } 199 | 200 | public void stop() { 201 | if (mPlayer != null) 202 | mPlayer.release(); 203 | } 204 | 205 | public enum VideoReaderEventMessageCode {WAITING_FOR_VIDEO, VIDEO_PLAYING} 206 | } 207 | -------------------------------------------------------------------------------- /app/src/main/java/usb/AndroidUSBInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019, Digi International Inc. 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, you can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | package usb; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | 21 | import android.hardware.usb.UsbDeviceConnection; 22 | import android.hardware.usb.UsbEndpoint; 23 | import android.util.Log; 24 | 25 | /** 26 | * This class acts as a wrapper to read data from the USB Interface in Android 27 | * behaving like an {@code InputputStream} class. 28 | */ 29 | public class AndroidUSBInputStream extends InputStream { 30 | 31 | private final String TAG = "USBInputStream"; 32 | // Constants. 33 | private static final int OFFSET = 0; 34 | private static final int READ_TIMEOUT = 100; 35 | 36 | // Variables. 37 | private UsbDeviceConnection usbConnection; 38 | 39 | private UsbEndpoint receiveEndPoint; 40 | private final UsbEndpoint sendEndPoint; 41 | 42 | private boolean working = false; 43 | 44 | 45 | /** 46 | * Class constructor. Instantiates a new {@code AndroidUSBInputStream} 47 | * object with the given parameters. 48 | * 49 | * @param readEndpoint The USB end point to use to read data from. 50 | * @param connection The USB connection to use to read data from. 51 | * 52 | * @see UsbDeviceConnection 53 | * @see UsbEndpoint 54 | */ 55 | public AndroidUSBInputStream( UsbEndpoint readEndpoint, UsbEndpoint sendEndpoint, UsbDeviceConnection connection) { 56 | this.usbConnection = connection; 57 | this.receiveEndPoint = readEndpoint; 58 | this.sendEndPoint = sendEndpoint; 59 | } 60 | 61 | @Override 62 | public int read() throws IOException { 63 | byte[] buffer = new byte[131072]; 64 | return read(buffer, 0, buffer.length); 65 | } 66 | 67 | @Override 68 | public int read(byte[] buffer, int offset, int length) throws IOException { 69 | int receivedBytes = usbConnection.bulkTransfer(receiveEndPoint, buffer, buffer.length, READ_TIMEOUT); 70 | if (receivedBytes <= 0) { 71 | // send magic packet again; Would be great to handle this in UsbMaskConnection directly... 72 | Log.d(TAG, "received buffer empty, sending magic packet again..."); 73 | usbConnection.bulkTransfer(sendEndPoint, "RMVT".getBytes(), "RMVT".getBytes().length, 2000); 74 | receivedBytes = usbConnection.bulkTransfer(receiveEndPoint, buffer, buffer.length, READ_TIMEOUT); 75 | } 76 | return receivedBytes; 77 | } 78 | 79 | 80 | @Override 81 | public void close() throws IOException {} 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/usb/AndroidUSBOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019, Digi International Inc. 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, you can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | package usb; 17 | 18 | import android.hardware.usb.UsbDeviceConnection; 19 | import android.hardware.usb.UsbEndpoint; 20 | 21 | import java.io.IOException; 22 | import java.io.OutputStream; 23 | import java.util.concurrent.LinkedBlockingQueue; 24 | 25 | /** 26 | * This class acts as a wrapper to write data to the USB Interface in Android 27 | * behaving like an {@code OutputStream} class. 28 | */ 29 | public class AndroidUSBOutputStream extends OutputStream { 30 | 31 | // Constants. 32 | private static final int WRITE_TIMEOUT = 2000; 33 | 34 | // Variables. 35 | private final UsbDeviceConnection usbConnection; 36 | 37 | private final UsbEndpoint sendEndPoint; 38 | 39 | private LinkedBlockingQueue writeQueue; 40 | 41 | private final boolean streamOpen = true; 42 | 43 | /** 44 | * Class constructor. Instantiates a new {@code AndroidUSBOutputStream} 45 | * object with the given parameters. 46 | * 47 | * @param writeEndpoint The USB end point to use to write data to. 48 | * @param connection The USB connection to use to write data to. 49 | * 50 | * @see UsbDeviceConnection 51 | * @see UsbEndpoint 52 | */ 53 | public AndroidUSBOutputStream(UsbEndpoint writeEndpoint, UsbDeviceConnection connection) { 54 | this.usbConnection = connection; 55 | this.sendEndPoint = writeEndpoint; 56 | } 57 | 58 | /* 59 | * (non-Javadoc) 60 | * @see java.io.OutputStream#write(int) 61 | */ 62 | @Override 63 | public void write(int oneByte) { 64 | write(new byte[] {(byte)oneByte}); 65 | } 66 | 67 | /* 68 | * (non-Javadoc) 69 | * @see java.io.OutputStream#write(byte[]) 70 | */ 71 | @Override 72 | public void write(byte[] buffer) { 73 | write(buffer, 0, buffer.length); 74 | } 75 | 76 | /* 77 | * (non-Javadoc) 78 | * @see java.io.OutputStream#write(byte[], int, int) 79 | */ 80 | @Override 81 | public void write(byte[] buffer, int offset, int count) { 82 | usbConnection.bulkTransfer(sendEndPoint, buffer, count, WRITE_TIMEOUT); 83 | 84 | } 85 | 86 | 87 | @Override 88 | public void close() throws IOException { 89 | super.close(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/usb/CircularByteBuffer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019, Digi International Inc. 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, you can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | package usb; 17 | 18 | /** 19 | * Helper class used to store data bytes as a circular buffer. 20 | */ 21 | public class CircularByteBuffer { 22 | 23 | // Variables. 24 | private byte[] buffer; 25 | 26 | private int readIndex; 27 | private int writeIndex; 28 | 29 | private boolean empty = true; 30 | 31 | /** 32 | * Instantiates a new {@code CircularByteBuffer} with the given capacity 33 | * in bytes. 34 | * 35 | * @param size Circular byte buffer size in bytes. 36 | * 37 | * @throws IllegalArgumentException if {@code size < 1}. 38 | */ 39 | public CircularByteBuffer(int size) { 40 | if (size < 1) 41 | throw new IllegalArgumentException("Buffer size must be greater than 0."); 42 | 43 | buffer = new byte[size]; 44 | readIndex = 0; 45 | writeIndex = 0; 46 | } 47 | 48 | /** 49 | * Writes the given amount of bytes to the circular byte buffer. 50 | * 51 | * @param data Bytes to write. 52 | * @param offset Offset inside data where bytes to write start. 53 | * @param numBytes Number of bytes to write. 54 | * @return The number of bytes actually written. 55 | * 56 | * @throws IllegalArgumentException if {@code offset < 0} or 57 | * if {@code numBytes < 1}. 58 | * @throws NullPointerException if {@code data == null}. 59 | * 60 | * @see #read(byte[], int, int) 61 | * @see #skip(int) 62 | */ 63 | public synchronized int write(byte[] data, int offset, int numBytes) { 64 | if (data == null) 65 | throw new NullPointerException("Data cannot be null."); 66 | if (offset < 0) 67 | throw new IllegalArgumentException("Offset cannot be negative."); 68 | if (numBytes < 1) 69 | throw new IllegalArgumentException("Number of bytes to write must be greater than 0."); 70 | 71 | // Check if there are enough bytes to write. 72 | int availableBytes = data.length - offset; 73 | if (numBytes > availableBytes) 74 | numBytes = availableBytes; 75 | 76 | // Check where we should start writing. 77 | if (numBytes < buffer.length - getWriteIndex()) { 78 | System.arraycopy(data, offset, buffer, getWriteIndex(), numBytes); 79 | writeIndex = getWriteIndex() + numBytes; 80 | } else { 81 | System.arraycopy(data, offset, buffer, getWriteIndex(), buffer.length - getWriteIndex()); 82 | System.arraycopy(data, offset + buffer.length-getWriteIndex(), buffer, 0, numBytes - (buffer.length - getWriteIndex())); 83 | writeIndex = numBytes - (buffer.length-getWriteIndex()); 84 | if (getReadIndex() < getWriteIndex()) 85 | readIndex = getWriteIndex(); 86 | } 87 | 88 | // Check if we were able to write all the bytes. 89 | if (numBytes > getCapacity()) 90 | numBytes = getCapacity(); 91 | 92 | empty = false; 93 | return numBytes; 94 | } 95 | 96 | /** 97 | * Reads the given amount of bytes to the given array from the circular byte 98 | * buffer. 99 | * 100 | * @param data Byte buffer to place read bytes in. 101 | * @param offset Offset inside data to start placing read bytes in. 102 | * @param numBytes Number of bytes to read. 103 | * @return The number of bytes actually read. 104 | * 105 | * @throws IllegalArgumentException if {@code offset < 0} or 106 | * if {@code numBytes < 1}. 107 | * @throws NullPointerException if {@code data == null}. 108 | * 109 | * @see #skip(int) 110 | * @see #write(byte[], int, int) 111 | */ 112 | public synchronized int read(byte[] data, int offset, int numBytes) { 113 | if (data == null) 114 | throw new NullPointerException("Data cannot be null."); 115 | if (offset < 0) 116 | throw new IllegalArgumentException("Offset cannot be negative."); 117 | if (numBytes < 1) 118 | throw new IllegalArgumentException("Number of bytes to read must be greater than 0."); 119 | 120 | // If we are empty, return 0. 121 | if (empty) 122 | return 0; 123 | 124 | // If we try to place bytes in an index bigger than buffer index, return 0 read bytes. 125 | if (offset >= data.length) 126 | return 0; 127 | 128 | if (data.length - offset < numBytes) 129 | return read(data, offset, data.length - offset); 130 | if (availableToRead() < numBytes) 131 | return read(data, offset, availableToRead()); 132 | if (numBytes < buffer.length - getReadIndex()){ 133 | System.arraycopy(buffer, getReadIndex(), data, offset, numBytes); 134 | readIndex = getReadIndex() + numBytes; 135 | } else { 136 | System.arraycopy(buffer, getReadIndex(), data, offset, buffer.length - getReadIndex()); 137 | System.arraycopy(buffer, 0, data, offset + buffer.length - getReadIndex(), numBytes - (buffer.length - getReadIndex())); 138 | readIndex = numBytes-(buffer.length - getReadIndex()); 139 | } 140 | 141 | // If we have read all bytes, set the buffer as empty. 142 | if (readIndex == writeIndex) 143 | empty = true; 144 | 145 | return numBytes; 146 | } 147 | 148 | /** 149 | * Skips the given number of bytes from the circular byte buffer. 150 | * 151 | * @param numBytes Number of bytes to skip. 152 | * @return The number of bytes actually skipped. 153 | * 154 | * @throws IllegalArgumentException if {@code numBytes < 1}. 155 | * 156 | * @see #read(byte[], int, int) 157 | * @see #write(byte[], int, int) 158 | */ 159 | public synchronized int skip(int numBytes) { 160 | if (numBytes < 1) 161 | throw new IllegalArgumentException("Number of bytes to skip must be greater than 0."); 162 | 163 | // If we are empty, return 0. 164 | if (empty) 165 | return 0; 166 | 167 | if (availableToRead() < numBytes) 168 | return skip(availableToRead()); 169 | if (numBytes < buffer.length - getReadIndex()) 170 | readIndex = getReadIndex() + numBytes; 171 | else 172 | readIndex = numBytes - (buffer.length - getReadIndex()); 173 | 174 | // If we have skipped all bytes, set the buffer as empty. 175 | if (readIndex == writeIndex) 176 | empty = true; 177 | 178 | return numBytes; 179 | } 180 | 181 | /** 182 | * Returns the available number of bytes to read from the byte buffer. 183 | * 184 | * @return The number of bytes in the buffer available for reading. 185 | * 186 | * @see #getCapacity() 187 | * @see #read(byte[], int, int) 188 | */ 189 | public int availableToRead() { 190 | if (empty) 191 | return 0; 192 | if (getReadIndex() < getWriteIndex()) 193 | return (getWriteIndex() - getReadIndex()); 194 | else 195 | return (buffer.length - getReadIndex() + getWriteIndex()); 196 | } 197 | 198 | /** 199 | * Returns the current read index. 200 | * 201 | * @return readIndex The current read index. 202 | */ 203 | private int getReadIndex() { 204 | return readIndex; 205 | } 206 | 207 | /** 208 | * Returns the current write index. 209 | * 210 | * @return writeIndex The current write index. 211 | */ 212 | private int getWriteIndex() { 213 | return writeIndex; 214 | } 215 | 216 | /** 217 | * Returns the circular byte buffer capacity. 218 | * 219 | * @return The circular byte buffer capacity. 220 | */ 221 | public int getCapacity() { 222 | return buffer.length; 223 | } 224 | 225 | /** 226 | * Clears the circular buffer. 227 | */ 228 | public void clearBuffer() { 229 | empty = true; 230 | readIndex = 0; 231 | writeIndex = 0; 232 | } 233 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_goggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-hdpi/ic_goggles.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_goggles_disconnected_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-hdpi/ic_goggles_disconnected_red.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_goggles_disconnected_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-hdpi/ic_goggles_disconnected_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_goggles_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-hdpi/ic_goggles_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-hdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_goggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-mdpi/ic_goggles.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_goggles_disconnected_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-mdpi/ic_goggles_disconnected_red.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_goggles_disconnected_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-mdpi/ic_goggles_disconnected_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_goggles_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-mdpi/ic_goggles_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-mdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_goggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xhdpi/ic_goggles.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_goggles_disconnected_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xhdpi/ic_goggles_disconnected_red.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_goggles_disconnected_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xhdpi/ic_goggles_disconnected_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_goggles_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xhdpi/ic_goggles_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xhdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_goggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxhdpi/ic_goggles.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_goggles_disconnected_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxhdpi/ic_goggles_disconnected_red.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_goggles_disconnected_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxhdpi/ic_goggles_disconnected_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_goggles_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxhdpi/ic_goggles_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_gogles_disconnected_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxhdpi/ic_gogles_disconnected_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxhdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_goggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxxhdpi/ic_goggles.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_goggles_disconnected_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxxhdpi/ic_goggles_disconnected_red.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_goggles_disconnected_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxxhdpi/ic_goggles_disconnected_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_goggles_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxxhdpi/ic_goggles_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/drawable-xxxhdpi/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 11 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/font/gidolinya.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpvout/DigiView-Android/42ae75d2eab0fffe85d7016a8f79091f2ebcde36/app/src/main/res/font/gidolinya.otf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 20 | 21 | 22 | 35 | 36 | 44 | 45 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/backdrop_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 17 | 18 | 29 | 30 | 40 | 41 | 42 | 43 | 49 | 50 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/datacollection_agreement_popup.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 |