Tinfoil network transfer support
Change progress bar behavior. Add settings. Add NCZ/XCI/XCZ 'support' (Fix bug #1). Make IntentService foreground service.
This commit is contained in:
parent
32a5d36caf
commit
0061551e6b
31 changed files with 1885 additions and 805 deletions
116
.idea/codeStyles/Project.xml
Normal file
116
.idea/codeStyles/Project.xml
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
24
README.md
24
README.md
|
@ -12,7 +12,7 @@ Sometimes I add new posts about this project [on my home page](https://developer
|
||||||
|
|
||||||
#### License
|
#### License
|
||||||
|
|
||||||
Source code license [GNU General Public License v3](https://github.com/developersu/ns-usbloader-mobile/blob/master/LICENSE)
|
Source code license [GNU General Public License v3](https://github.com/developersu/ns-usbloader-mobile/blob/master/LICENSE) or any later version.
|
||||||
|
|
||||||
Logo font: [Play](https://fonts.google.com/specimen/Play) by Jonas Hecksher. Open Font License distribution.
|
Logo font: [Play](https://fonts.google.com/specimen/Play) by Jonas Hecksher. Open Font License distribution.
|
||||||
|
|
||||||
|
@ -23,11 +23,13 @@ Logo font: [Play](https://fonts.google.com/specimen/Play) by Jonas Hecksher. Ope
|
||||||
|
|
||||||
* Maximum: Android 10.0 (Q)
|
* Maximum: Android 10.0 (Q)
|
||||||
|
|
||||||
* USB-OTG support
|
* USB-OTG support / WiFi
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
1. Open TinFoil/GoldLeaf
|
#### USB
|
||||||
|
|
||||||
|
1. Open Awoo Installer/Tinfoil/GoldLeaf v0.5
|
||||||
2. Connect NS to Android device using OTG cable.
|
2. Connect NS to Android device using OTG cable.
|
||||||
3. Allow interaction request. Application opens.
|
3. Allow interaction request. Application opens.
|
||||||
4. Click 'hamburger' menu-button and select application you'd like to use.
|
4. Click 'hamburger' menu-button and select application you'd like to use.
|
||||||
|
@ -36,6 +38,14 @@ Logo font: [Play](https://fonts.google.com/specimen/Play) by Jonas Hecksher. Ope
|
||||||
|
|
||||||
Note: use short usb cable.
|
Note: use short usb cable.
|
||||||
|
|
||||||
|
#### WiFi
|
||||||
|
|
||||||
|
For installation over the Net (Tinfoil):
|
||||||
|
1. Connect to WiFi
|
||||||
|
2. Setup 90DNS or whatever you use
|
||||||
|
3. Open Awoo Installer, select installation over the net
|
||||||
|
4. Open settings (click 'hamburger' menu-button), enter NS IP you see on the screen
|
||||||
|
|
||||||
#### Bugs
|
#### Bugs
|
||||||
|
|
||||||
If you're Samsung owner, it would be better to not rotating your phone during transfer. Or minimizing. Well, on my elder device it causes application failure. But you can try if you're curious, and report/update a bug. Please mention your device model.
|
If you're Samsung owner, it would be better to not rotating your phone during transfer. Or minimizing. Well, on my elder device it causes application failure. But you can try if you're curious, and report/update a bug. Please mention your device model.
|
||||||
|
@ -43,15 +53,15 @@ If you're Samsung owner, it would be better to not rotating your phone during tr
|
||||||
### Other notes
|
### Other notes
|
||||||
|
|
||||||
'Status' = 'Uploaded' that appears in the table does not mean that file has been installed. It means that it has been sent to NS without any issues! That's what this app about.
|
'Status' = 'Uploaded' that appears in the table does not mean that file has been installed. It means that it has been sent to NS without any issues! That's what this app about.
|
||||||
Handling successful/failed installation is a purpose of the other side application: TinFoil/GoldLeaf. And they don't provide any feedback interfaces so I can't detect success/failure.
|
Handling successful/failed installation is a purpose of the other side application: TinFoil/GoldLeaf v0.5. And they don't provide any feedback interfaces so I can't detect success/failure.
|
||||||
|
|
||||||
#### Translators!
|
#### Translators
|
||||||
|
|
||||||
Wait a bit.
|
Are welcome.
|
||||||
|
|
||||||
#### TODO:
|
#### TODO:
|
||||||
|
|
||||||
- [ ] Tinfoil NET transfer support
|
- [x] Tinfoil NET transfer support
|
||||||
- [ ] Better UI
|
- [ ] Better UI
|
||||||
- [ ] Multi-select files (if possible)
|
- [ ] Multi-select files (if possible)
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ android {
|
||||||
applicationId "com.blogspot.developersu.ns_usbloader"
|
applicationId "com.blogspot.developersu.ns_usbloader"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 1
|
versionCode 2
|
||||||
versionName "0.1"
|
versionName "0.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
android.defaultConfig.vectorDrawables.useSupportLibrary = true
|
android.defaultConfig.vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,12 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.blogspot.developersu.ns_usbloader">
|
package="com.blogspot.developersu.ns_usbloader">
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.usb.host" />
|
<uses-feature android:name="android.hardware.usb.host" />
|
||||||
|
|
||||||
<uses-permission android:name="com.blogspot.developersu.ns_usbloader.ACTION_USB_PERMISSION" /> <!-- TODO: REMOVE -->
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For Android 9 -->
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -13,11 +17,16 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
tools:ignore="GoogleAppIndexingWarning"> <!-- <- IDK WTF, RTFM MB -->
|
||||||
|
<activity
|
||||||
|
android:name=".SettingsActivity"
|
||||||
|
android:label="@string/title_activity_settings"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".AboutActivity"
|
android:name=".AboutActivity"
|
||||||
android:label="@string/title_activity_about"
|
android:label="@string/title_activity_about"
|
||||||
android:theme="@style/AppTheme.NoActionBar"></activity>
|
android:theme="@style/AppTheme.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
@ -39,7 +48,7 @@
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.hardware.usb.action.USB_DEVICE_DETACHED"
|
android:name="android.hardware.usb.action.USB_DEVICE_DETACHED"
|
||||||
android:resource="@xml/device_filter" />
|
android:resource="@xml/device_filter" />
|
||||||
<!--
|
<!-- TODO: fix, implement, etc.
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.intent.action.EDIT" />
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
@ -74,7 +83,8 @@
|
||||||
android:resource="@xml/device_filter" />
|
android:resource="@xml/device_filter" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".Service.CommunicationsService" />
|
<service android:name=".service.CommunicationsService"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -23,12 +23,13 @@ public class AboutActivity extends AppCompatActivity {
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
TextView t1 = (TextView) findViewById(R.id.textView1);
|
final TextView t1 = findViewById(R.id.textView1);
|
||||||
t1.append(" v."+BuildConfig.VERSION_NAME);
|
t1.append(" v."+BuildConfig.VERSION_NAME);
|
||||||
TextView t2 = (TextView) findViewById(R.id.textView2);
|
final TextView t2 = findViewById(R.id.textView2);
|
||||||
t2.setMovementMethod(LinkMovementMethod.getInstance());
|
t2.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
TextView t4 = (TextView) findViewById(R.id.textView4);
|
final TextView t4 = findViewById(R.id.textView4);
|
||||||
t4.setMovementMethod(LinkMovementMethod.getInstance());
|
t4.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
final TextView t6 = findViewById(R.id.textView6);
|
||||||
|
t6.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,11 @@ import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.blogspot.developersu.ns_usbloader.Model.NsResultReciever;
|
import com.blogspot.developersu.ns_usbloader.model.NsResultReciever;
|
||||||
import com.blogspot.developersu.ns_usbloader.Service.CommunicationsService;
|
import com.blogspot.developersu.ns_usbloader.service.CommunicationsService;
|
||||||
import com.blogspot.developersu.ns_usbloader.View.NSPElement;
|
import com.blogspot.developersu.ns_usbloader.view.NSPElement;
|
||||||
import com.blogspot.developersu.ns_usbloader.View.NspItemsAdapter;
|
import com.blogspot.developersu.ns_usbloader.view.NspItemsAdapter;
|
||||||
import com.google.android.material.navigation.NavigationView;
|
import com.google.android.material.navigation.NavigationView;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
|
@ -64,7 +63,7 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
private NsResultReciever nsResultReciever;
|
private NsResultReciever nsResultReciever;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putParcelableArrayList("DATASET_LIST", mDataset);
|
outState.putParcelableArrayList("DATASET_LIST", mDataset);
|
||||||
outState.putParcelable("USB_DEVICE", usbDevice);
|
outState.putParcelable("USB_DEVICE", usbDevice);
|
||||||
|
@ -89,6 +88,7 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
registerReceiver(innerBroadcastReceiver, intentFilter);
|
registerReceiver(innerBroadcastReceiver, intentFilter);
|
||||||
nsResultReciever.setReceiver(this);
|
nsResultReciever.setReceiver(this);
|
||||||
blockUI(CommunicationsService.isServiceActive());
|
blockUI(CommunicationsService.isServiceActive());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -140,36 +140,31 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
// Handle back button push when drawer opened
|
// Handle back button push when drawer opened
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
||||||
if (drawer.isDrawerOpen(GravityCompat.START)) {
|
if (drawer.isDrawerOpen(GravityCompat.START))
|
||||||
drawer.closeDrawer(GravityCompat.START);
|
drawer.closeDrawer(GravityCompat.START);
|
||||||
} else {
|
else
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Drawer actions
|
// Drawer actions
|
||||||
@Override
|
@Override
|
||||||
public boolean onNavigationItemSelected(MenuItem item) {
|
public boolean onNavigationItemSelected(MenuItem item) {
|
||||||
// Handle navigation view item clicks here.
|
// Handle navigation view item clicks here.
|
||||||
int id = item.getItemId();
|
switch (item.getItemId()){
|
||||||
|
case R.id.nav_gl:
|
||||||
if (id == R.id.nav_tf_usb) {
|
|
||||||
// TODO: make something useful
|
|
||||||
}
|
|
||||||
else if (id == R.id.nav_tf_net) {
|
|
||||||
// TODO: implement
|
|
||||||
}
|
|
||||||
else if (id == R.id.nav_gl) { // TODO: SET LISTENER
|
|
||||||
if (! mDataset.isEmpty()){
|
if (! mDataset.isEmpty()){
|
||||||
for (NSPElement element: mDataset){
|
for (NSPElement element: mDataset)
|
||||||
element.setSelected(false);
|
element.setSelected(false);
|
||||||
}
|
|
||||||
mAdapter.notifyDataSetChanged();
|
mAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
else if (id == R.id.nav_about) {
|
case R.id.nav_settings:
|
||||||
//Log.i("LPR", "ABOUT");
|
startActivity(new Intent(this, SettingsActivity.class));
|
||||||
|
break;
|
||||||
|
case R.id.nav_about:
|
||||||
startActivity(new Intent(this, AboutActivity.class));
|
startActivity(new Intent(this, AboutActivity.class));
|
||||||
|
//case R.id.nav_tf_usb:
|
||||||
|
//case R.id.nav_tf_net:
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
||||||
|
@ -192,12 +187,10 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
drawer.addDrawerListener(toggle);
|
drawer.addDrawerListener(toggle);
|
||||||
toggle.syncState();
|
toggle.syncState();
|
||||||
drawerNavView.setNavigationItemSelectedListener(this);
|
drawerNavView.setNavigationItemSelectedListener(this);
|
||||||
|
|
||||||
// Initialize Progress Bar
|
// Initialize Progress Bar
|
||||||
progressBarMain = findViewById(R.id.mainProgressBar);
|
progressBarMain = findViewById(R.id.mainProgressBar);
|
||||||
// Configure data set in case it's restored from screen rotation or something
|
// Configure data set in case it's restored from screen rotation or something
|
||||||
if (savedInstanceState != null){
|
if (savedInstanceState != null){
|
||||||
//Log.i("LPR", "NOT EMPTY INSTANCE-"+getIntent().getAction());
|
|
||||||
mDataset = savedInstanceState.getParcelableArrayList("DATASET_LIST");
|
mDataset = savedInstanceState.getParcelableArrayList("DATASET_LIST");
|
||||||
// Restore USB device information
|
// Restore USB device information
|
||||||
usbDevice = savedInstanceState.getParcelable("USB_DEVICE");
|
usbDevice = savedInstanceState.getParcelable("USB_DEVICE");
|
||||||
|
@ -239,6 +232,29 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
mAdapter = new NspItemsAdapter(mDataset);
|
mAdapter = new NspItemsAdapter(mDataset);
|
||||||
recyclerView.setAdapter(mAdapter);
|
recyclerView.setAdapter(mAdapter);
|
||||||
|
this.setSwipeFunctionsToView();
|
||||||
|
// Select files button
|
||||||
|
selectBtn = findViewById(R.id.buttonSelect);
|
||||||
|
selectBtn.setOnClickListener(e->{
|
||||||
|
Intent fileChooser = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
fileChooser.setType("application/octet-stream"); //fileChooser.setType("*/*"); ???
|
||||||
|
|
||||||
|
if (fileChooser.resolveActivity(getPackageManager()) != null)
|
||||||
|
startActivityForResult(Intent.createChooser(fileChooser, getString(R.string.select_file_btn)), ADD_NSP_INTENT_CODE);
|
||||||
|
else
|
||||||
|
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), getResources().getString(R.string.install_file_explorer));
|
||||||
|
});
|
||||||
|
// Upload to NS button
|
||||||
|
uploadToNsBtn = findViewById(R.id.buttonUpload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUploadBtnState(){ // TODO: this function is bad. It multiplies entropy and sorrow.
|
||||||
|
uploadToNsBtn.setEnabled(mAdapter.getItemCount() > 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @see MainActivity#onCreate
|
||||||
|
* */
|
||||||
|
private void setSwipeFunctionsToView(){
|
||||||
ItemTouchHelper.Callback ithCallBack = new ItemTouchHelper.Callback() {
|
ItemTouchHelper.Callback ithCallBack = new ItemTouchHelper.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
@ -261,136 +277,100 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
};
|
};
|
||||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(ithCallBack);
|
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(ithCallBack);
|
||||||
itemTouchHelper.attachToRecyclerView(recyclerView);
|
itemTouchHelper.attachToRecyclerView(recyclerView);
|
||||||
// Select files button
|
|
||||||
selectBtn = findViewById(R.id.buttonSelect);
|
|
||||||
selectBtn.setOnClickListener(e->{
|
|
||||||
Intent fileChooser = new Intent(Intent.ACTION_GET_CONTENT);
|
|
||||||
//fileChooser.setType("*/*");
|
|
||||||
fileChooser.setType("application/octet-stream");
|
|
||||||
|
|
||||||
if (fileChooser.resolveActivity(getPackageManager()) != null)
|
|
||||||
startActivityForResult(
|
|
||||||
Intent.createChooser(fileChooser, getString(R.string.select_file_btn))
|
|
||||||
, ADD_NSP_INTENT_CODE);
|
|
||||||
else
|
|
||||||
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), getResources().getString(R.string.install_file_explorer));
|
|
||||||
});
|
|
||||||
// Upload to NS button
|
|
||||||
uploadToNsBtn = findViewById(R.id.buttonUpload);
|
|
||||||
|
|
||||||
updateUploadBtnState();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUploadBtnState(){
|
|
||||||
if (mAdapter.getItemCount() > 0)
|
|
||||||
uploadToNsBtn.setEnabled(true);
|
|
||||||
else
|
|
||||||
uploadToNsBtn.setEnabled(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
//Snackbar.make(findViewById(android.R.id.content), requestCode+" "+resultCode, Snackbar.LENGTH_SHORT).show();
|
//Snackbar.make(findViewById(android.R.id.content), requestCode+" "+resultCode, Snackbar.LENGTH_SHORT).show();
|
||||||
if (requestCode != ADD_NSP_INTENT_CODE || data == null)
|
if (requestCode != ADD_NSP_INTENT_CODE || data == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Uri uri = data.getData();
|
Uri uri = data.getData();
|
||||||
|
|
||||||
if (uri == null)
|
if (uri == null || uri.getScheme() == null || ! uri.getScheme().equals("content"))
|
||||||
return;
|
|
||||||
if (uri.getScheme() == null || ! uri.getScheme().equals("content"))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String fileName = LoperHelpers.getFileNameFromUri(uri, this);
|
String fileName = LoperHelpers.getFileNameFromUri(uri, this);
|
||||||
long fileSize = LoperHelpers.getFileSizeFromUri(uri, this);
|
long fileSize = LoperHelpers.getFileSizeFromUri(uri, this);
|
||||||
|
|
||||||
if (fileName == null || fileSize < 0) {
|
if (fileName == null || fileSize < 0) {
|
||||||
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), getResources().getString(R.string.popup_not_supported));
|
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), getResources().getString(R.string.popup_incorrect_file));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! fileName.endsWith(".nsp")){
|
String fileExtension = fileName.replaceAll("^.*\\.", "").toLowerCase();
|
||||||
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), getResources().getString(R.string.popup_wrong_file));
|
switch (fileExtension){
|
||||||
|
case "nsp":
|
||||||
|
case "nsz":
|
||||||
|
case "xci":
|
||||||
|
case "xcz":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), getResources().getString(R.string.popup_non_supported_format));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean shouldBeAdded = true;
|
boolean isAlreadyAddedElement = false;
|
||||||
|
|
||||||
for (NSPElement element: mDataset){
|
for (NSPElement element: mDataset){
|
||||||
if (element.getFilename().equals(fileName)) {
|
if (element.getFilename().equals(fileName)) {
|
||||||
shouldBeAdded = false;
|
isAlreadyAddedElement = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NSPElement element;
|
if (isAlreadyAddedElement)
|
||||||
if (shouldBeAdded){
|
return;
|
||||||
element = new NSPElement(uri, fileName, fileSize);
|
|
||||||
if (drawerNavView.getCheckedItem() != null && drawerNavView.getCheckedItem().getItemId() != R.id.nav_gl)
|
NSPElement element = new NSPElement(uri, fileName, fileSize);
|
||||||
|
if (drawerNavView.getCheckedItem() != null) // && drawerNavView.getCheckedItem().getItemId() != R.id.nav_gl
|
||||||
element.setSelected(true);
|
element.setSelected(true);
|
||||||
mDataset.add(element);
|
mDataset.add(element);
|
||||||
}
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
mAdapter.notifyDataSetChanged();
|
mAdapter.notifyDataSetChanged();
|
||||||
// Enable upload button
|
// Enable upload button
|
||||||
updateUploadBtnState();
|
updateUploadBtnState();
|
||||||
}
|
}
|
||||||
/*
|
private void uploadFiles(){
|
||||||
private void uploadFilesTemp(){
|
ArrayList<NSPElement> NSPElementsToSend = new ArrayList<>();
|
||||||
ArrayList<NSPElement> toSentArray = new ArrayList<>();
|
|
||||||
for (NSPElement element: mDataset){
|
for (NSPElement element: mDataset){
|
||||||
if (element.isSelected())
|
if (element.isSelected())
|
||||||
toSentArray.add(element);
|
NSPElementsToSend.add(element);
|
||||||
}
|
}
|
||||||
if (toSentArray.isEmpty()) {
|
// Do we have files to send?
|
||||||
|
if (NSPElementsToSend.isEmpty()){
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.nothing_selected_message), Snackbar.LENGTH_LONG).show();
|
Snackbar.make(findViewById(android.R.id.content), getString(R.string.nothing_selected_message), Snackbar.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Intent serviceStartIntent = new Intent(this, CommunicationsService.class);
|
// Do we have selected protocol?
|
||||||
serviceStartIntent.putExtra(NsConstants.NS_RESULT_RECEIVER, nsResultReciever);
|
|
||||||
serviceStartIntent.putParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST, toSentArray);
|
|
||||||
if (drawerNavView.getCheckedItem() == null) {
|
if (drawerNavView.getCheckedItem() == null) {
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.no_protocol_selected_message), Snackbar.LENGTH_LONG).show();
|
Snackbar.make(findViewById(android.R.id.content), getString(R.string.no_protocol_selected_message), Snackbar.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (drawerNavView.getCheckedItem().getItemId()){
|
Intent serviceStartIntent = new Intent(this, CommunicationsService.class);
|
||||||
case R.id.nav_tf_usb:
|
serviceStartIntent.putExtra(NsConstants.NS_RESULT_RECEIVER, nsResultReciever);
|
||||||
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_USB);
|
serviceStartIntent.putParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST, NSPElementsToSend);
|
||||||
break;
|
// Is it TF Net transfer?
|
||||||
case R.id.nav_tf_net:
|
if (drawerNavView.getCheckedItem().getItemId() == R.id.nav_tf_net){
|
||||||
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_NET);
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_NET);
|
||||||
break;
|
SharedPreferences sp = getSharedPreferences("NSUSBloader", MODE_PRIVATE);
|
||||||
case R.id.nav_gl:
|
|
||||||
if (toSentArray.size() > 1){
|
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.one_item_for_gl_notification), Snackbar.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_GL_USB);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_NS_DEVICE_IP, sp.getString("SNsIP", "192.168.1.42"));
|
||||||
|
if (sp.getBoolean("SAutoIP", true))
|
||||||
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PHONE_IP, "");
|
||||||
|
else
|
||||||
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PHONE_IP, sp.getString("SServerIP", "192.168.1.142"));
|
||||||
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PHONE_PORT, sp.getInt("SServerPort", 6042));
|
||||||
startService(serviceStartIntent);
|
startService(serviceStartIntent);
|
||||||
blockUI(true);
|
blockUI(true);
|
||||||
}
|
|
||||||
*/
|
|
||||||
private void uploadFiles(){
|
|
||||||
//**************************************************************************************************************************************
|
|
||||||
try{
|
|
||||||
int id = drawerNavView.getCheckedItem().getItemId();
|
|
||||||
if (id == R.id.nav_tf_net) {
|
|
||||||
Toast.makeText(getApplicationContext(), "Not supported yet", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
// Ok, so it's something USB related
|
||||||
catch (NullPointerException npe){
|
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.no_protocol_selected_message), Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
//**************************************************************************************************************************************
|
|
||||||
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||||
// If somehow we can't get system service
|
// Do we have manager?
|
||||||
if (usbManager == null) {
|
if (usbManager == null) {
|
||||||
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), "Internal issue: getSystemService(Context.USB_SERVICE) returned null");
|
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), "Internal issue: getSystemService(Context.USB_SERVICE) returned null");
|
||||||
return; // ??? HOW ???
|
return;
|
||||||
}
|
}
|
||||||
// If device not connected
|
// If device not connected
|
||||||
if (usbDevice == null){
|
if (usbDevice == null){
|
||||||
|
@ -419,70 +399,47 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent serviceStartIntent;
|
|
||||||
ArrayList<NSPElement> toSentArray = new ArrayList<>();
|
|
||||||
for (NSPElement element: mDataset){
|
|
||||||
if (element.isSelected())
|
|
||||||
toSentArray.add(element);
|
|
||||||
}
|
|
||||||
if (toSentArray.isEmpty()) {
|
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.nothing_selected_message), Snackbar.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
serviceStartIntent = new Intent(this, CommunicationsService.class);
|
|
||||||
serviceStartIntent.putExtra(NsConstants.NS_RESULT_RECEIVER, nsResultReciever);
|
|
||||||
serviceStartIntent.putParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST, toSentArray);
|
|
||||||
if (drawerNavView.getCheckedItem() == null) {
|
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.no_protocol_selected_message), Snackbar.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (drawerNavView.getCheckedItem().getItemId()){
|
switch (drawerNavView.getCheckedItem().getItemId()){
|
||||||
case R.id.nav_tf_usb:
|
case R.id.nav_tf_usb:
|
||||||
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_USB);
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_USB);
|
||||||
break;
|
break;
|
||||||
case R.id.nav_tf_net:
|
|
||||||
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_NET);
|
|
||||||
break;
|
|
||||||
case R.id.nav_gl:
|
case R.id.nav_gl:
|
||||||
if (toSentArray.size() > 1){
|
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.one_item_for_gl_notification), Snackbar.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_GL_USB);
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_GL_USB);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), getString(R.string.unknown_protocol_error), Snackbar.LENGTH_LONG).show(); // ?_?
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_NS_DEVICE, usbDevice);
|
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_NS_DEVICE, usbDevice);
|
||||||
startService(serviceStartIntent);
|
startService(serviceStartIntent);
|
||||||
blockUI(true);
|
blockUI(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void blockUI(boolean shouldBeBlocked){
|
private void blockUI(boolean shouldBlock){
|
||||||
if (shouldBeBlocked){
|
if (shouldBlock) {
|
||||||
selectBtn.setEnabled(false);
|
selectBtn.setEnabled(false);
|
||||||
recyclerView.setLayoutFrozen(true);
|
recyclerView.suppressLayout(true);
|
||||||
uploadToNsBtn.setCompoundDrawablesWithIntrinsicBounds(null, getResources().getDrawable(R.drawable.ic_cancel), null, null);
|
uploadToNsBtn.setCompoundDrawablesWithIntrinsicBounds(null, getResources().getDrawable(R.drawable.ic_cancel), null, null);
|
||||||
uploadToNsBtn.setText(R.string.interrupt_btn);
|
uploadToNsBtn.setText(R.string.interrupt_btn);
|
||||||
uploadToNsBtn.setOnClickListener(e -> {
|
uploadToNsBtn.setOnClickListener(e -> stopService(new Intent(this, CommunicationsService.class)));
|
||||||
CommunicationsService.cancel();
|
|
||||||
});
|
|
||||||
progressBarMain.setVisibility(ProgressBar.VISIBLE);
|
progressBarMain.setVisibility(ProgressBar.VISIBLE);
|
||||||
progressBarMain.setIndeterminate(true);//TODO
|
progressBarMain.setIndeterminate(true);//TODO
|
||||||
|
uploadToNsBtn.setEnabled(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
selectBtn.setEnabled(true);
|
selectBtn.setEnabled(true);
|
||||||
recyclerView.setLayoutFrozen(false);
|
recyclerView.suppressLayout(false);
|
||||||
uploadToNsBtn.setCompoundDrawablesWithIntrinsicBounds(null, getResources().getDrawable(R.drawable.ic_upload_btn), null, null);
|
uploadToNsBtn.setCompoundDrawablesWithIntrinsicBounds(null, getResources().getDrawable(R.drawable.ic_upload_btn), null, null);
|
||||||
uploadToNsBtn.setText(R.string.upload_btn);
|
uploadToNsBtn.setText(R.string.upload_btn);
|
||||||
uploadToNsBtn.setOnClickListener(e -> {
|
uploadToNsBtn.setOnClickListener(e -> this.uploadFiles() );
|
||||||
//this.uploadFiles();
|
|
||||||
this.uploadFiles();
|
|
||||||
});
|
|
||||||
progressBarMain.setVisibility(ProgressBar.INVISIBLE);
|
progressBarMain.setVisibility(ProgressBar.INVISIBLE);
|
||||||
|
this.updateUploadBtnState();
|
||||||
}
|
}
|
||||||
}
|
/**
|
||||||
// Handle service updates
|
* Handle service updates
|
||||||
|
* */
|
||||||
@Override
|
@Override
|
||||||
public void onRecieveResults(int code, Bundle bundle) {
|
public void onReceiveResults(int code, Bundle bundle) {
|
||||||
if (code == NsConstants.NS_RESULT_PROGRESS_INDETERMINATE)
|
if (code == NsConstants.NS_RESULT_PROGRESS_INDETERMINATE)
|
||||||
progressBarMain.setIndeterminate(true);
|
progressBarMain.setIndeterminate(true);
|
||||||
else{ // Else NsConstants.NS_RESULT_PROGRESS_VALUE ; ALSO THIS PART IS FULL OF SHIT
|
else{ // Else NsConstants.NS_RESULT_PROGRESS_VALUE ; ALSO THIS PART IS FULL OF SHIT
|
||||||
|
@ -491,7 +448,9 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
progressBarMain.setProgress(bundle.getInt("POSITION"));
|
progressBarMain.setProgress(bundle.getInt("POSITION"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Deal with global broadcast intents
|
/**
|
||||||
|
* Deal with global broadcast intents
|
||||||
|
* */
|
||||||
private class InnerBroadcastReceiver extends BroadcastReceiver{
|
private class InnerBroadcastReceiver extends BroadcastReceiver{
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
@ -516,20 +475,21 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
|
||||||
case UsbManager.ACTION_USB_DEVICE_DETACHED:
|
case UsbManager.ACTION_USB_DEVICE_DETACHED:
|
||||||
usbDevice = null;
|
usbDevice = null;
|
||||||
isUsbDeviceAccessible = false;
|
isUsbDeviceAccessible = false;
|
||||||
CommunicationsService.cancel();
|
stopService(new Intent(context, CommunicationsService.class));
|
||||||
break;
|
break;
|
||||||
case NsConstants.SERVICE_TRANSFER_TASK_FINISHED_INTENT:
|
case NsConstants.SERVICE_TRANSFER_TASK_FINISHED_INTENT:
|
||||||
ArrayList<NSPElement> nspElements = intent.getParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST);
|
ArrayList<NSPElement> nspElements = intent.getParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST);
|
||||||
|
if (nspElements == null)
|
||||||
|
break;
|
||||||
for (int i=0; i < mDataset.size(); i++){
|
for (int i=0; i < mDataset.size(); i++){
|
||||||
for (NSPElement recievedNSPe : nspElements)
|
for (NSPElement receivedNSPe : nspElements)
|
||||||
if (recievedNSPe.getFilename().equals(mDataset.get(i).getFilename()))
|
if (receivedNSPe.getFilename().equals(mDataset.get(i).getFilename()))
|
||||||
mDataset.get(i).setStatus(recievedNSPe.getStatus());
|
mDataset.get(i).setStatus(receivedNSPe.getStatus());
|
||||||
}
|
}
|
||||||
mAdapter.notifyDataSetChanged();
|
mAdapter.notifyDataSetChanged();
|
||||||
blockUI(false);
|
blockUI(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
//Log.i("LPR", "INTERNAL BR REC: " + intent.getAction()+" "+isUsbDeviceAccessible);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,6 @@ import androidx.core.app.NotificationManagerCompat;
|
||||||
|
|
||||||
public class NsBroadcastReceiver extends BroadcastReceiver {
|
public class NsBroadcastReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
private static final String NOTIFICATION_NS_CONNECTED_CHAN_ID = "com.blogspot.developersu.ns_usbloader.CHAN_ID_NS_CONNECTED";
|
|
||||||
private static final int NOTIFICATION_NS_CONNECTED_ID = 42;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void onReceive(Context context, Intent intent) {
|
public synchronized void onReceive(Context context, Intent intent) {
|
||||||
if (intent.getAction() == null)
|
if (intent.getAction() == null)
|
||||||
|
@ -44,7 +41,7 @@ public class NsBroadcastReceiver extends BroadcastReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showNotification(Context context, UsbDevice usbDevice){
|
private void showNotification(Context context, UsbDevice usbDevice){
|
||||||
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NOTIFICATION_NS_CONNECTED_CHAN_ID);
|
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NsConstants.NOTIFICATION_NS_CONNECTED_CHAN_ID);
|
||||||
notification.setSmallIcon(R.drawable.ic_notification)
|
notification.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(context.getString(R.string.ns_connected_info))
|
.setContentTitle(context.getString(R.string.ns_connected_info))
|
||||||
//.setAutoCancel(true)
|
//.setAutoCancel(true)
|
||||||
|
@ -52,11 +49,11 @@ public class NsBroadcastReceiver extends BroadcastReceiver {
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class).putExtra(UsbManager.EXTRA_DEVICE, usbDevice), 0));
|
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class).putExtra(UsbManager.EXTRA_DEVICE, usbDevice), 0));
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
CharSequence notificationChanName = context.getString(R.string.notification_channel_name);
|
CharSequence notificationChanName = context.getString(R.string.notification_chan_name_usb);
|
||||||
String notificationChanDesc = context.getString(R.string.notification_channel_description);
|
String notificationChanDesc = context.getString(R.string.notification_chan_desc_usb);
|
||||||
|
|
||||||
NotificationChannel channel = new NotificationChannel(
|
NotificationChannel channel = new NotificationChannel(
|
||||||
NOTIFICATION_NS_CONNECTED_CHAN_ID,
|
NsConstants.NOTIFICATION_NS_CONNECTED_CHAN_ID,
|
||||||
notificationChanName,
|
notificationChanName,
|
||||||
NotificationManager.IMPORTANCE_DEFAULT);
|
NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
channel.setDescription(notificationChanDesc);
|
channel.setDescription(notificationChanDesc);
|
||||||
|
@ -64,14 +61,14 @@ public class NsBroadcastReceiver extends BroadcastReceiver {
|
||||||
// or other notification behaviors after this
|
// or other notification behaviors after this
|
||||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
notificationManager.createNotificationChannel(channel);
|
notificationManager.createNotificationChannel(channel);
|
||||||
notificationManager.notify(NOTIFICATION_NS_CONNECTED_ID, notification.build());
|
notificationManager.notify(NsConstants.NOTIFICATION_NS_CONNECTED_ID, notification.build());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
NotificationManagerCompat.from(context).notify(NOTIFICATION_NS_CONNECTED_ID, notification.build()); // TODO: 42 is shit
|
NotificationManagerCompat.from(context).notify(NsConstants.NOTIFICATION_NS_CONNECTED_ID, notification.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideNotification(Context context){
|
private void hideNotification(Context context){
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIFICATION_NS_CONNECTED_ID);
|
NotificationManagerCompat.from(context).cancel(NsConstants.NOTIFICATION_NS_CONNECTED_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,6 +10,9 @@ public class NsConstants {
|
||||||
public static final String SERVICE_CONTENT_NSP_LIST = "NSP_LIST";
|
public static final String SERVICE_CONTENT_NSP_LIST = "NSP_LIST";
|
||||||
public static final String SERVICE_CONTENT_PROTOCOL = "PROTOCOL";
|
public static final String SERVICE_CONTENT_PROTOCOL = "PROTOCOL";
|
||||||
public static final String SERVICE_CONTENT_NS_DEVICE = "DEVICE";
|
public static final String SERVICE_CONTENT_NS_DEVICE = "DEVICE";
|
||||||
|
public static final String SERVICE_CONTENT_NS_DEVICE_IP = "DEVICE_IP";
|
||||||
|
public static final String SERVICE_CONTENT_PHONE_IP = "PHONE_IP";
|
||||||
|
public static final String SERVICE_CONTENT_PHONE_PORT = "PHONE_PORT";
|
||||||
// Result Reciever possible codes
|
// Result Reciever possible codes
|
||||||
public static final int NS_RESULT_PROGRESS_INDETERMINATE = -1; // upper limit would be 0; value would be 0
|
public static final int NS_RESULT_PROGRESS_INDETERMINATE = -1; // upper limit would be 0; value would be 0
|
||||||
public static final int NS_RESULT_PROGRESS_VALUE = 0;
|
public static final int NS_RESULT_PROGRESS_VALUE = 0;
|
||||||
|
@ -17,4 +20,10 @@ public class NsConstants {
|
||||||
public static final int PROTO_TF_USB = 10;
|
public static final int PROTO_TF_USB = 10;
|
||||||
public static final int PROTO_TF_NET = 20;
|
public static final int PROTO_TF_NET = 20;
|
||||||
public static final int PROTO_GL_USB = 30;
|
public static final int PROTO_GL_USB = 30;
|
||||||
|
|
||||||
|
public static final String NOTIFICATION_FOREGROUND_SERVICE_CHAN_ID = "com.blogspot.developersu.ns_usbloader.CHAN_ID_FOREGROUND_SERVICE";
|
||||||
|
public static final int NOTIFICATION_TRANSFER_ID = 1;
|
||||||
|
|
||||||
|
public static final String NOTIFICATION_NS_CONNECTED_CHAN_ID = "com.blogspot.developersu.ns_usbloader.CHAN_ID_NS_CONNECTED";
|
||||||
|
public static final int NOTIFICATION_NS_CONNECTED_ID = 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,582 +0,0 @@
|
||||||
package com.blogspot.developersu.ns_usbloader.Service;
|
|
||||||
|
|
||||||
import android.app.IntentService;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
import android.hardware.usb.UsbEndpoint;
|
|
||||||
import android.hardware.usb.UsbInterface;
|
|
||||||
import android.hardware.usb.UsbManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.ResultReceiver;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.blogspot.developersu.ns_usbloader.NsConstants;
|
|
||||||
import com.blogspot.developersu.ns_usbloader.PFS.PFSProvider;
|
|
||||||
import com.blogspot.developersu.ns_usbloader.R;
|
|
||||||
import com.blogspot.developersu.ns_usbloader.View.NSPElement;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public class CommunicationsService extends IntentService {
|
|
||||||
private static final String SERVICE_TAG = "com.blogspot.developersu.ns_usbloader.Service.CommunicationsService";
|
|
||||||
|
|
||||||
private static AtomicBoolean isActive = new AtomicBoolean(false);
|
|
||||||
private static AtomicBoolean interrupt = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
private ResultReceiver resultReceiver;
|
|
||||||
|
|
||||||
public static boolean isServiceActive(){
|
|
||||||
return isActive.get();
|
|
||||||
}
|
|
||||||
public static void cancel(){
|
|
||||||
interrupt.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommunicationsService() {
|
|
||||||
super(SERVICE_TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArrayList<NSPElement> nspElements;
|
|
||||||
private UsbDeviceConnection deviceConnection;
|
|
||||||
private UsbInterface usbInterface;
|
|
||||||
private UsbEndpoint epIn;
|
|
||||||
private UsbEndpoint epOut;
|
|
||||||
|
|
||||||
private String status;
|
|
||||||
private String issueDescription;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onHandleIntent(Intent intent) {
|
|
||||||
isActive.set(true);
|
|
||||||
interrupt.set(false);
|
|
||||||
status = getResources().getString(R.string.status_failed_to_upload);
|
|
||||||
resultReceiver = intent.getParcelableExtra(NsConstants.NS_RESULT_RECEIVER);
|
|
||||||
nspElements = intent.getParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST);
|
|
||||||
final int protocol = intent.getIntExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, -1); // -1 since it's impossible
|
|
||||||
UsbDevice usbDevice = intent.getParcelableExtra(NsConstants.SERVICE_CONTENT_NS_DEVICE);
|
|
||||||
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
|
||||||
if (nspElements == null || usbDevice == null || usbManager == null || protocol < 0) {
|
|
||||||
reportExecutionFinish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Start process
|
|
||||||
usbInterface = usbDevice.getInterface(0);
|
|
||||||
epIn = usbInterface.getEndpoint(0); // For bulk read
|
|
||||||
epOut = usbInterface.getEndpoint(1); // For bulk write
|
|
||||||
|
|
||||||
deviceConnection = usbManager.openDevice(usbDevice);
|
|
||||||
if ( ! deviceConnection.claimInterface(usbInterface, false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (protocol == NsConstants.PROTO_TF_USB){
|
|
||||||
new TinFoil();
|
|
||||||
}
|
|
||||||
else if (protocol == NsConstants.PROTO_GL_USB){
|
|
||||||
new GoldLeaf();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (NSPElement e: nspElements)
|
|
||||||
e.setStatus(status);
|
|
||||||
/*
|
|
||||||
Log.i("LPR", "Status " +status);
|
|
||||||
Log.i("LPR", "issue " +issueDescription);
|
|
||||||
Log.i("LPR", "Interrupt " +interrupt.get());
|
|
||||||
Log.i("LPR", "Active " +isActive.get());
|
|
||||||
*/
|
|
||||||
reportExecutionFinish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetProgressBar(){
|
|
||||||
resultReceiver.send(NsConstants.NS_RESULT_PROGRESS_INDETERMINATE, Bundle.EMPTY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProgressBar(int currentPosition){
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putInt("POSITION", currentPosition);
|
|
||||||
resultReceiver.send(NsConstants.NS_RESULT_PROGRESS_VALUE, bundle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportExecutionFinish(){
|
|
||||||
deviceConnection.releaseInterface(usbInterface);
|
|
||||||
deviceConnection.close();
|
|
||||||
isActive.set(false);
|
|
||||||
Intent executionFinishIntent = new Intent(NsConstants.SERVICE_TRANSFER_TASK_FINISHED_INTENT);
|
|
||||||
executionFinishIntent.putExtra(NsConstants.SERVICE_CONTENT_NSP_LIST, nspElements);
|
|
||||||
if (issueDescription != null) {
|
|
||||||
executionFinishIntent.putExtra("ISSUES", issueDescription);
|
|
||||||
}
|
|
||||||
this.sendBroadcast(executionFinishIntent);
|
|
||||||
interrupt.set(false);
|
|
||||||
}
|
|
||||||
/*============================================================================================*/
|
|
||||||
/**
|
|
||||||
* Sending any byte array to USB device
|
|
||||||
* @return 'false' if no issues
|
|
||||||
* 'true' if errors happened
|
|
||||||
* */
|
|
||||||
private boolean writeToUsb(byte[] message){
|
|
||||||
int result;
|
|
||||||
result = deviceConnection.bulkTransfer(epOut, message, message.length, 0); // last one is TIMEOUT. 0 stands for unlimited. Endpoint OUT = 0x01
|
|
||||||
//Log.i("LPR", "RES: "+result);
|
|
||||||
return (result != message.length);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Reading what USB device responded.
|
|
||||||
* @return byte array if data read successful
|
|
||||||
* 'null' if read failed
|
|
||||||
* */
|
|
||||||
private byte[] readFromUsb(){
|
|
||||||
byte[] readBuffer = new byte[512];
|
|
||||||
// We can limit it to 32 bytes, but there is a non-zero chance to got OVERFLOW from libusb.
|
|
||||||
int result;
|
|
||||||
result = deviceConnection.bulkTransfer(epIn, readBuffer, 512, 0); // last one is TIMEOUT. 0 stands for unlimited. Endpoint IN = 0x81
|
|
||||||
|
|
||||||
if (result > 0)
|
|
||||||
return Arrays.copyOf(readBuffer, result);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
/*============================================================================================*/
|
|
||||||
private class TinFoil{
|
|
||||||
TinFoil(){
|
|
||||||
|
|
||||||
if (!sendListOfNSP())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (proceedCommands()) // REPORT SUCCESS
|
|
||||||
status = getResources().getString(R.string.status_uploaded); // Don't change status that is already set to FAILED TODO: FIX
|
|
||||||
}
|
|
||||||
// Send what NSP will be transferred
|
|
||||||
|
|
||||||
private boolean sendListOfNSP(){
|
|
||||||
// Send list of NSP files:
|
|
||||||
// Proceed "TUL0"
|
|
||||||
if (writeToUsb("TUL0".getBytes())) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30} //"US-ASCII"?
|
|
||||||
issueDescription = "TF Send list of files: handshake failure";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//Collect file names
|
|
||||||
StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder
|
|
||||||
for(NSPElement element: nspElements) {
|
|
||||||
nspListNamesBuilder.append(element.getFilename()); // And here we come with java string default encoding (UTF-16)
|
|
||||||
nspListNamesBuilder.append('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] nspListNames = nspListNamesBuilder.toString().getBytes(); // android's .getBytes() default == UTF8
|
|
||||||
ByteBuffer byteBuffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN); // integer = 4 bytes; BTW Java is stored in big-endian format
|
|
||||||
byteBuffer.putInt(nspListNames.length); // This way we obtain length in int converted to byte array in correct Big-endian order. Trust me.
|
|
||||||
byte[] nspListSize = byteBuffer.array();
|
|
||||||
|
|
||||||
// Sending NSP list
|
|
||||||
if (writeToUsb(nspListSize)) { // size of the list we're going to transfer goes...
|
|
||||||
issueDescription = "TF Send list of files: [send list length]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(new byte[8])) { // 8 zero bytes goes...
|
|
||||||
issueDescription = "TF Send list of files: [send padding]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(nspListNames)) { // list of the names goes...
|
|
||||||
issueDescription = "TF Send list of files: [send list itself]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// After we sent commands to NS, this chain starts
|
|
||||||
|
|
||||||
private boolean proceedCommands(){
|
|
||||||
final byte[] magic = new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30}; // eq. 'TUC0' @ UTF-8 (actually ASCII lol, u know what I mean)
|
|
||||||
|
|
||||||
byte[] receivedArray;
|
|
||||||
|
|
||||||
while (true){
|
|
||||||
if (interrupt.get()) // Check if user interrupted process.
|
|
||||||
return false;
|
|
||||||
receivedArray = readFromUsb();
|
|
||||||
if (receivedArray == null)
|
|
||||||
return false; // catches exception
|
|
||||||
|
|
||||||
if (!Arrays.equals(Arrays.copyOfRange(receivedArray, 0,4), magic)) // Bytes from 0 to 3 should contain 'magic' TUC0, so must be verified like this
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// 8th to 12th(explicits) bytes in returned data stands for command ID as unsigned integer (Little-endian). Actually, we have to compare arrays here, but in real world it can't be greater then 0/1/2, thus:
|
|
||||||
// BTW also protocol specifies 4th byte to be 0x00 kinda indicating that that this command is valid. But, as you may see, never happens other situation when it's not = 0.
|
|
||||||
if (receivedArray[8] == 0x00){ //0x00 - exit
|
|
||||||
return true; // All interaction with USB device should be ended (expected);
|
|
||||||
}
|
|
||||||
else if ((receivedArray[8] == 0x01) || (receivedArray[8] == 0x02)){ //0x01 - file range; 0x02 unknown bug on backend side (dirty hack).
|
|
||||||
if (!fileRangeCmd()) // issueDescription inside
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This is what returns requested file (files)
|
|
||||||
* Executes multiple times
|
|
||||||
* @return 'true' if everything is ok
|
|
||||||
* 'false' is error/exception occurs
|
|
||||||
* */
|
|
||||||
|
|
||||||
private boolean fileRangeCmd(){
|
|
||||||
byte[] receivedArray;
|
|
||||||
// Here we take information of what other side wants
|
|
||||||
receivedArray = readFromUsb();
|
|
||||||
if (receivedArray == null) {
|
|
||||||
issueDescription = "TF Unable to get meta information @fileRangeCmd()";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// range_offset of the requested file. In the begining it will be 0x10.
|
|
||||||
long receivedRangeSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 0,8)).order(ByteOrder.LITTLE_ENDIAN).getLong();
|
|
||||||
byte[] receivedRangeSizeRAW = Arrays.copyOfRange(receivedArray, 0,8);
|
|
||||||
long receivedRangeOffset = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 8,16)).order(ByteOrder.LITTLE_ENDIAN).getLong();
|
|
||||||
|
|
||||||
// Requesting UTF-8 file name required:
|
|
||||||
receivedArray = readFromUsb();
|
|
||||||
if (receivedArray == null) {
|
|
||||||
issueDescription = "TF Unable to get file name @fileRangeCmd()";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String receivedRequestedNSP;
|
|
||||||
try {
|
|
||||||
receivedRequestedNSP = new String(receivedArray, "UTF-8"); //TODO:FIX
|
|
||||||
}
|
|
||||||
catch (java.io.UnsupportedEncodingException uee){
|
|
||||||
issueDescription = "TF UnsupportedEncodingException @fileRangeCmd()";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sending response header
|
|
||||||
if (!sendResponse(receivedRangeSizeRAW)) // Get receivedRangeSize in 'RAW' format exactly as it has been received. It's simply.
|
|
||||||
return false; // issueDescription handled by method
|
|
||||||
|
|
||||||
try {
|
|
||||||
BufferedInputStream bufferedInStream = null;
|
|
||||||
|
|
||||||
for (NSPElement e: nspElements){
|
|
||||||
if (e.getFilename().equals(receivedRequestedNSP)){
|
|
||||||
InputStream elementIS = getContentResolver().openInputStream(e.getUri());
|
|
||||||
if (elementIS == null) {
|
|
||||||
issueDescription = "TF Unable to obtain InputStream";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bufferedInStream = new BufferedInputStream(elementIS); // TODO: refactor?
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bufferedInStream == null) {
|
|
||||||
issueDescription = "TF Unable to create BufferedInputStream";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] readBuf ;//= new byte[1048576]; // eq. Allocate 1mb
|
|
||||||
|
|
||||||
if (bufferedInStream.skip(receivedRangeOffset) != receivedRangeOffset){
|
|
||||||
issueDescription = "TF Requested skip is out of file size. Nothing to transmit.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long readFrom = 0;
|
|
||||||
// 'End Offset' equal to receivedRangeSize.
|
|
||||||
int readPice = 16384; // = 8Mb
|
|
||||||
|
|
||||||
while (readFrom < receivedRangeSize){
|
|
||||||
if (interrupt.get()) // Check if user interrupted process.
|
|
||||||
return true;
|
|
||||||
if ((readFrom + readPice) >= receivedRangeSize )
|
|
||||||
readPice = (int)(receivedRangeSize - readFrom); // TODO: Troubles could raise here
|
|
||||||
|
|
||||||
readBuf = new byte[readPice]; // TODO: not perfect moment, consider refactoring.
|
|
||||||
|
|
||||||
if (bufferedInStream.read(readBuf) != readPice) {
|
|
||||||
issueDescription = "TF Reading of stream suddenly ended";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//write to USB
|
|
||||||
if (writeToUsb(readBuf)) {
|
|
||||||
issueDescription = "TF Failure during NSP transmission.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
readFrom += readPice;
|
|
||||||
|
|
||||||
updateProgressBar((int) ((readFrom+1)/(receivedRangeSize/100+1)));
|
|
||||||
Log.i("LPR", "CO: "+readFrom+"RRS: "+receivedRangeSize+"RES: "+(readFrom+1/(receivedRangeSize/100+1)));
|
|
||||||
}
|
|
||||||
bufferedInStream.close();
|
|
||||||
|
|
||||||
resetProgressBar();
|
|
||||||
} catch (java.io.IOException ioe){
|
|
||||||
issueDescription = "TF IOException: "+ioe.getMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Send response header.
|
|
||||||
* @return true if everything OK
|
|
||||||
* false if failed
|
|
||||||
* */
|
|
||||||
private boolean sendResponse(byte[] rangeSize){ // This method as separate function itself for application needed as a cookie in the middle of desert.
|
|
||||||
if (writeToUsb(new byte[] { (byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30, // 'TUC0'
|
|
||||||
(byte) 0x01, // CMD_TYPE_RESPONSE = 1
|
|
||||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, // kinda padding. Guys, didn't you want to use integer value for CMD semantic?
|
|
||||||
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00} ) // Send integer value of '1' in Little-endian format.
|
|
||||||
){
|
|
||||||
issueDescription = "TF Sending response: [1/3]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(writeToUsb(rangeSize)) { // Send EXACTLY what has been received
|
|
||||||
issueDescription = "TF Sending response: [2/3]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(writeToUsb(new byte[12])) { // kinda another one padding
|
|
||||||
issueDescription = "TF Sending response: [3/3] FAIL";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* GoldLeaf processing
|
|
||||||
* */
|
|
||||||
private class GoldLeaf{
|
|
||||||
// CMD G L U C
|
|
||||||
private final byte[] CMD_GLUC = new byte[]{0x47, 0x4c, 0x55, 0x43};
|
|
||||||
private final byte[] CMD_ConnectionRequest = new byte[]{0x00, 0x00, 0x00, 0x00}; // Write-only command
|
|
||||||
private final byte[] CMD_NSPName = new byte[]{0x02, 0x00, 0x00, 0x00}; // Write-only command
|
|
||||||
private final byte[] CMD_NSPData = new byte[]{0x04, 0x00, 0x00, 0x00}; // Write-only command
|
|
||||||
|
|
||||||
private final byte[] CMD_ConnectionResponse = new byte[]{0x01, 0x00, 0x00, 0x00};
|
|
||||||
private final byte[] CMD_Start = new byte[]{0x03, 0x00, 0x00, 0x00};
|
|
||||||
private final byte[] CMD_NSPContent = new byte[]{0x05, 0x00, 0x00, 0x00};
|
|
||||||
private final byte[] CMD_NSPTicket = new byte[]{0x06, 0x00, 0x00, 0x00};
|
|
||||||
private final byte[] CMD_Finish = new byte[]{0x07, 0x00, 0x00, 0x00};
|
|
||||||
|
|
||||||
GoldLeaf(){
|
|
||||||
String fileName;
|
|
||||||
InputStream fileInputStream;
|
|
||||||
try {
|
|
||||||
fileInputStream = getContentResolver().openInputStream(nspElements.get(0).getUri());
|
|
||||||
fileName = nspElements.get(0).getFilename();
|
|
||||||
}
|
|
||||||
catch (java.io.FileNotFoundException fnfe){
|
|
||||||
issueDescription = "GL FileNotFoundException @GoldLeaf()";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PFSProvider pfsElement = new PFSProvider(fileInputStream, fileName);
|
|
||||||
if (!pfsElement.init()) {
|
|
||||||
issueDescription = "GL File provided have incorrect structure and won't be uploaded.";
|
|
||||||
status = getResources().getString(R.string.status_wrong_file);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initGoldLeafProtocol(pfsElement))
|
|
||||||
status = getResources().getString(R.string.status_uploaded); // else - no change status that is already set to FAILED
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean initGoldLeafProtocol(PFSProvider pfsElement){
|
|
||||||
// Go parse commands
|
|
||||||
byte[] readByte;
|
|
||||||
|
|
||||||
// Go connect to GoldLeaf
|
|
||||||
if (writeToUsb(CMD_GLUC)){
|
|
||||||
issueDescription = "GL Initiating GoldLeaf connection: 1/2";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(CMD_ConnectionRequest)){
|
|
||||||
issueDescription = "GL Initiating GoldLeaf connection: 2/2";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
readByte = readFromUsb();
|
|
||||||
if (readByte == null)
|
|
||||||
return false;
|
|
||||||
if (Arrays.equals(readByte, CMD_GLUC)) {
|
|
||||||
readByte = readFromUsb();
|
|
||||||
if (readByte == null)
|
|
||||||
return false;
|
|
||||||
if (Arrays.equals(readByte, CMD_ConnectionResponse)) {
|
|
||||||
if (!handleConnectionResponse(pfsElement))
|
|
||||||
return false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Arrays.equals(readByte, CMD_Start)) {
|
|
||||||
if (!handleStart(pfsElement))
|
|
||||||
return false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Arrays.equals(readByte, CMD_NSPContent)) {
|
|
||||||
if (!handleNSPContent(pfsElement, true))
|
|
||||||
return false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Arrays.equals(readByte, CMD_NSPTicket)) {
|
|
||||||
if (!handleNSPContent(pfsElement, false))
|
|
||||||
return false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Arrays.equals(readByte, CMD_Finish)) { // All good
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* ConnectionResponse command handler
|
|
||||||
* */
|
|
||||||
private boolean handleConnectionResponse(PFSProvider pfsElement){
|
|
||||||
if (writeToUsb(CMD_GLUC)) {
|
|
||||||
issueDescription = "GL 'ConnectionResponse' command: INFO: [1/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(CMD_NSPName)) {
|
|
||||||
issueDescription = "GL 'ConnectionResponse' command: INFO: [2/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(pfsElement.getBytesNspFileNameLength())) {
|
|
||||||
issueDescription = "GL 'ConnectionResponse' command: INFO: [3/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(pfsElement.getBytesNspFileName())) {
|
|
||||||
issueDescription = "GL 'ConnectionResponse' command: INFO: [4/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Start command handler
|
|
||||||
* */
|
|
||||||
private boolean handleStart(PFSProvider pfsElement){
|
|
||||||
|
|
||||||
if (writeToUsb(CMD_GLUC)) {
|
|
||||||
issueDescription = "GL Handle 'Start' command: [Send command prepare]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(CMD_NSPData)) {
|
|
||||||
issueDescription = "GL Handle 'Start' command: [Send command]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(pfsElement.getBytesCountOfNca())) {
|
|
||||||
issueDescription = "GL Handle 'Start' command: [Send length]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ncaCount = pfsElement.getIntCountOfNca();
|
|
||||||
|
|
||||||
for (int i = 0; i < ncaCount; i++){
|
|
||||||
if (writeToUsb(pfsElement.getNca(i).getNcaFileNameLength())) {
|
|
||||||
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [1/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(pfsElement.getNca(i).getNcaFileName())) {
|
|
||||||
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [2/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getBodySize()+pfsElement.getNca(i).getNcaOffset()).array())) { // offset. real.
|
|
||||||
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [3/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getNca(i).getNcaSize()).array())) { // size
|
|
||||||
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [4/4]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* NSPContent command handler
|
|
||||||
* isItRawRequest - if True, just ask NS what's needed
|
|
||||||
* - if False, send ticket
|
|
||||||
* */
|
|
||||||
private boolean handleNSPContent(PFSProvider pfsElement, boolean isItRawRequest){
|
|
||||||
int requestedNcaID;
|
|
||||||
|
|
||||||
if (isItRawRequest) {
|
|
||||||
byte[] readByte = readFromUsb();
|
|
||||||
if (readByte == null || readByte.length != 4) {
|
|
||||||
issueDescription = "GL Handle 'Content' command: [Read requested ID]";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
requestedNcaID = ByteBuffer.wrap(readByte).order(ByteOrder.LITTLE_ENDIAN).getInt();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
requestedNcaID = pfsElement.getNcaTicketID();
|
|
||||||
}
|
|
||||||
|
|
||||||
long realNcaOffset = pfsElement.getNca(requestedNcaID).getNcaOffset()+pfsElement.getBodySize();
|
|
||||||
long realNcaSize = pfsElement.getNca(requestedNcaID).getNcaSize();
|
|
||||||
|
|
||||||
long readFrom = 0;
|
|
||||||
|
|
||||||
int readPice = 16384; // 8mb NOTE: consider switching to 1mb 1048576
|
|
||||||
byte[] readBuf;
|
|
||||||
|
|
||||||
try{
|
|
||||||
BufferedInputStream bufferedInStream = new BufferedInputStream(getContentResolver().openInputStream(nspElements.get(0).getUri())); // TODO: refactor?
|
|
||||||
if (bufferedInStream.skip(realNcaOffset) != realNcaOffset) {
|
|
||||||
issueDescription = "GL Failed to skip NCA offset";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (readFrom < realNcaSize){
|
|
||||||
if (interrupt.get()) // Check if user interrupted process.
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (readPice > (realNcaSize - readFrom))
|
|
||||||
readPice = (int)(realNcaSize - readFrom); // TODO: Troubles could raise here
|
|
||||||
readBuf = new byte[readPice];
|
|
||||||
if (bufferedInStream.read(readBuf) != readPice) {
|
|
||||||
issueDescription = "GL Failed to read data from file.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeToUsb(readBuf)) {
|
|
||||||
issueDescription = "GL Failed to write data into NS.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
readFrom += readPice;
|
|
||||||
|
|
||||||
updateProgressBar((int) ((readFrom+1)/(realNcaSize/100+1)));
|
|
||||||
}
|
|
||||||
bufferedInStream.close();
|
|
||||||
|
|
||||||
resetProgressBar();
|
|
||||||
}
|
|
||||||
catch (java.io.IOException ioe){
|
|
||||||
issueDescription = "GL Failed to read NCA ID "+requestedNcaID+". IO Exception: "+ioe.getMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.InputFilter;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Switch;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
private EditText nsIp;
|
||||||
|
private EditText servAddr;
|
||||||
|
private EditText servPort;
|
||||||
|
private Switch autoDetectIp;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
if (item.getItemId() == android.R.id.home)
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
// Set NS IP field
|
||||||
|
nsIp = findViewById(R.id.nsIpEditText);
|
||||||
|
servAddr = findViewById(R.id.servAddrTextEdit);
|
||||||
|
servPort = findViewById(R.id.servPortTextEdit);
|
||||||
|
autoDetectIp = findViewById(R.id.autoDetectIpSW);
|
||||||
|
|
||||||
|
nsIp.setFilters(new InputFilter[]{inputFilterForIP});
|
||||||
|
servAddr.setFilters(new InputFilter[]{inputFilterForIP});
|
||||||
|
servPort.setFilters(new InputFilter[]{inputFilterForPort});
|
||||||
|
autoDetectIp.setOnCheckedChangeListener((compoundButton, switchState) -> servAddr.setEnabled(! switchState));
|
||||||
|
|
||||||
|
// TODO: Disable controls
|
||||||
|
if (savedInstanceState == null){
|
||||||
|
SharedPreferences sp = getSharedPreferences("NSUSBloader", MODE_PRIVATE); //.getInt("PROTOCOL", NsConstants.PROTO_TF_USB);
|
||||||
|
nsIp.setText(sp.getString("SNsIP", "192.168.1.42"));
|
||||||
|
autoDetectIp.setChecked(sp.getBoolean("SAutoIP", true));
|
||||||
|
servAddr.setText(sp.getString("SServerIP", "192.168.1.142"));
|
||||||
|
servPort.setText(String.valueOf(sp.getInt("SServerPort", 6042)));
|
||||||
|
}
|
||||||
|
// else { } // not needed
|
||||||
|
|
||||||
|
// Shitcode practices begin
|
||||||
|
nsIp.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable editable) {
|
||||||
|
if (! editable.toString().matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"))
|
||||||
|
nsIp.setTextColor(Color.RED);
|
||||||
|
else
|
||||||
|
nsIp.setTextColor(Color.BLACK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
servAddr.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable editable) {
|
||||||
|
if (! editable.toString().matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"))
|
||||||
|
nsIp.setTextColor(Color.RED);
|
||||||
|
else
|
||||||
|
nsIp.setTextColor(Color.BLACK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
servPort.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable editable) {
|
||||||
|
String contentString = editable.toString();
|
||||||
|
//Log.i("LPR", contentString);
|
||||||
|
if (contentString.matches("^\\d{1,5}")){
|
||||||
|
if (Integer.valueOf(contentString) < 1024)
|
||||||
|
servPort.setTextColor(Color.RED);
|
||||||
|
else
|
||||||
|
servPort.setTextColor(Color.BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Shitcode practices end
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
SharedPreferences.Editor spEditor = getSharedPreferences("NSUSBloader", MODE_PRIVATE).edit();
|
||||||
|
|
||||||
|
if (nsIp.getText().toString().matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"))
|
||||||
|
spEditor.putString("SNsIP", nsIp.getText().toString());
|
||||||
|
|
||||||
|
spEditor.putBoolean("SAutoIP", autoDetectIp.isChecked());
|
||||||
|
|
||||||
|
if (servAddr.getText().toString().matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"))
|
||||||
|
spEditor.putString("SServerIP", servAddr.getText().toString());
|
||||||
|
|
||||||
|
final String contentString = servPort.getText().toString();
|
||||||
|
if (contentString.matches("^\\d{1,5}") && (Integer.valueOf(contentString) >= 1024)){
|
||||||
|
spEditor.putInt("SServerPort", Integer.valueOf(servPort.getText().toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
spEditor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InputFilter inputFilterForIP = new InputFilter() {
|
||||||
|
@Override
|
||||||
|
public CharSequence filter(CharSequence charSequence, int start, int end, Spanned destination, int dStart, int dEnd) {
|
||||||
|
if (end > start) {
|
||||||
|
String destTxt = destination.toString();
|
||||||
|
String resultingTxt = destTxt.substring(0, dStart) +
|
||||||
|
charSequence.subSequence(start, end) +
|
||||||
|
destTxt.substring(dEnd);
|
||||||
|
if (! resultingTxt.matches ("^\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3}(\\.(\\d{1,3})?)?)?)?)?)?"))
|
||||||
|
return "";
|
||||||
|
else {
|
||||||
|
String[] splits = resultingTxt.split("\\.");
|
||||||
|
for (String split : splits) {
|
||||||
|
if (Integer.valueOf(split) > 255)
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static InputFilter inputFilterForPort = new InputFilter() {
|
||||||
|
@Override
|
||||||
|
public CharSequence filter(CharSequence charSequence, int start, int end, Spanned destination, int dStart, int dEnd) {
|
||||||
|
if (end > start) {
|
||||||
|
String destTxt = destination.toString();
|
||||||
|
String resultingTxt = destTxt.substring(0, dStart) +
|
||||||
|
charSequence.subSequence(start, end) +
|
||||||
|
destTxt.substring(dEnd);
|
||||||
|
if (!resultingTxt.matches ("^[0-9]+"))
|
||||||
|
return "";
|
||||||
|
if (Integer.valueOf(resultingTxt) > 65535)
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.blogspot.developersu.ns_usbloader.Model;
|
package com.blogspot.developersu.ns_usbloader.model;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
@ -7,7 +7,7 @@ import android.os.ResultReceiver;
|
||||||
public class NsResultReciever extends ResultReceiver {
|
public class NsResultReciever extends ResultReceiver {
|
||||||
|
|
||||||
public interface Receiver{
|
public interface Receiver{
|
||||||
void onRecieveResults(int code, Bundle bundle);
|
void onReceiveResults(int code, Bundle bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Receiver mReceiver;
|
private Receiver mReceiver;
|
||||||
|
@ -23,6 +23,6 @@ public class NsResultReciever extends ResultReceiver {
|
||||||
@Override
|
@Override
|
||||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
if (mReceiver != null)
|
if (mReceiver != null)
|
||||||
mReceiver.onRecieveResults(resultCode, resultData);
|
mReceiver.onReceiveResults(resultCode, resultData);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.blogspot.developersu.ns_usbloader.PFS;
|
package com.blogspot.developersu.ns_usbloader.pfs;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.blogspot.developersu.ns_usbloader.PFS;
|
package com.blogspot.developersu.ns_usbloader.pfs;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
|
@ -0,0 +1,141 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader.service;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbManager;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
|
|
||||||
|
import com.blogspot.developersu.ns_usbloader.MainActivity;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.NsConstants;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.R;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.view.NSPElement;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class CommunicationsService extends IntentService {
|
||||||
|
private static final String SERVICE_TAG = "com.blogspot.developersu.ns_usbloader.Service.CommunicationsService";
|
||||||
|
private static final int PROTOCOL_UNKNOWN = -1;
|
||||||
|
|
||||||
|
private static AtomicBoolean isActive = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private String issueDescription;
|
||||||
|
private TransferTask transferTask;
|
||||||
|
|
||||||
|
private ArrayList<NSPElement> nspElements;
|
||||||
|
private String status = "";
|
||||||
|
|
||||||
|
UsbDevice usbDevice;
|
||||||
|
|
||||||
|
String nsIp;
|
||||||
|
String phoneIp;
|
||||||
|
int phonePort;
|
||||||
|
|
||||||
|
public CommunicationsService() {super(SERVICE_TAG);}
|
||||||
|
public static boolean isServiceActive(){
|
||||||
|
return isActive.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
isActive.set(true);
|
||||||
|
|
||||||
|
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getBaseContext(), NsConstants.NOTIFICATION_FOREGROUND_SERVICE_CHAN_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setContentTitle(getString(R.string.notification_transfer_in_progress))
|
||||||
|
.setProgress(0, 0, true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0));
|
||||||
|
|
||||||
|
startForeground(NsConstants.NOTIFICATION_TRANSFER_ID, notificationBuilder.build());
|
||||||
|
|
||||||
|
|
||||||
|
ResultReceiver resultReceiver = intent.getParcelableExtra(NsConstants.NS_RESULT_RECEIVER);
|
||||||
|
nspElements = intent.getParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST);
|
||||||
|
final int protocol = intent.getIntExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, PROTOCOL_UNKNOWN);
|
||||||
|
|
||||||
|
// Clear statuses
|
||||||
|
for (NSPElement e: nspElements)
|
||||||
|
e.setStatus("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (protocol){
|
||||||
|
case NsConstants.PROTO_TF_USB:
|
||||||
|
getDataForUsbTransfer(intent);
|
||||||
|
transferTask = new TinfoilUSB(resultReceiver, getApplicationContext(), usbDevice, (UsbManager) getSystemService(Context.USB_SERVICE), nspElements);
|
||||||
|
break;
|
||||||
|
case NsConstants.PROTO_GL_USB:
|
||||||
|
getDataForUsbTransfer(intent);
|
||||||
|
transferTask = new GoldLeaf(resultReceiver, getApplicationContext(), usbDevice, (UsbManager) getSystemService(Context.USB_SERVICE), nspElements);
|
||||||
|
break;
|
||||||
|
case NsConstants.PROTO_TF_NET:
|
||||||
|
getDataForNetTransfer(intent);
|
||||||
|
transferTask = new TinfoilNET(resultReceiver, getApplicationContext(), nspElements, nsIp, phoneIp, phonePort);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e){
|
||||||
|
this.issueDescription = e.getMessage();
|
||||||
|
status = getString(R.string.status_failed_to_upload);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
transferTask.run();
|
||||||
|
this.issueDescription = transferTask.getIssueDescription();
|
||||||
|
status = transferTask.getStatus();
|
||||||
|
/*
|
||||||
|
Log.i("LPR", "Status " +status);
|
||||||
|
Log.i("LPR", "issue " +issueDescription);
|
||||||
|
Log.i("LPR", "Interrupt " +transferTask.interrupt.get());
|
||||||
|
Log.i("LPR", "Active " +isActive.get());
|
||||||
|
*/
|
||||||
|
finish();
|
||||||
|
stopForeground(true);
|
||||||
|
// Now we have to hide what has to be hidden. This part of code MUST be here right after stopForeground():
|
||||||
|
this.hideNotification(getApplicationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getDataForUsbTransfer(Intent intent){
|
||||||
|
this.usbDevice = intent.getParcelableExtra(NsConstants.SERVICE_CONTENT_NS_DEVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getDataForNetTransfer(Intent intent){
|
||||||
|
this.nsIp = intent.getStringExtra(NsConstants.SERVICE_CONTENT_NS_DEVICE_IP);
|
||||||
|
this.phoneIp = intent.getStringExtra(NsConstants.SERVICE_CONTENT_PHONE_IP);
|
||||||
|
this.phonePort = intent.getIntExtra(NsConstants.SERVICE_CONTENT_PHONE_PORT, 6042);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finish(){
|
||||||
|
// Set status if not already set
|
||||||
|
for (NSPElement e: nspElements)
|
||||||
|
if (e.getStatus().isEmpty())
|
||||||
|
e.setStatus(status);
|
||||||
|
isActive.set(false);
|
||||||
|
Intent executionFinishIntent = new Intent(NsConstants.SERVICE_TRANSFER_TASK_FINISHED_INTENT);
|
||||||
|
executionFinishIntent.putExtra(NsConstants.SERVICE_CONTENT_NSP_LIST, nspElements);
|
||||||
|
if (issueDescription != null) {
|
||||||
|
executionFinishIntent.putExtra("ISSUES", issueDescription);
|
||||||
|
}
|
||||||
|
this.sendBroadcast(executionFinishIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideNotification(Context context){
|
||||||
|
NotificationManagerCompat.from(context).cancel(NsConstants.NOTIFICATION_TRANSFER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (transferTask != null)
|
||||||
|
transferTask.cancel();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,249 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader.service;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbManager;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import com.blogspot.developersu.ns_usbloader.pfs.PFSProvider;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.R;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.view.NSPElement;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
class GoldLeaf extends UsbTransfer {
|
||||||
|
|
||||||
|
private ArrayList<NSPElement> nspElements;
|
||||||
|
private PFSProvider pfsElement;
|
||||||
|
|
||||||
|
// CMD G L U C
|
||||||
|
private final byte[] CMD_GLUC = new byte[]{0x47, 0x4c, 0x55, 0x43};
|
||||||
|
private final byte[] CMD_ConnectionRequest = new byte[]{0x00, 0x00, 0x00, 0x00}; // Write-only command
|
||||||
|
private final byte[] CMD_NSPName = new byte[]{0x02, 0x00, 0x00, 0x00}; // Write-only command
|
||||||
|
private final byte[] CMD_NSPData = new byte[]{0x04, 0x00, 0x00, 0x00}; // Write-only command
|
||||||
|
|
||||||
|
private final byte[] CMD_ConnectionResponse = new byte[]{0x01, 0x00, 0x00, 0x00};
|
||||||
|
private final byte[] CMD_Start = new byte[]{0x03, 0x00, 0x00, 0x00};
|
||||||
|
private final byte[] CMD_NSPContent = new byte[]{0x05, 0x00, 0x00, 0x00};
|
||||||
|
private final byte[] CMD_NSPTicket = new byte[]{0x06, 0x00, 0x00, 0x00};
|
||||||
|
private final byte[] CMD_Finish = new byte[]{0x07, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
|
|
||||||
|
GoldLeaf(ResultReceiver resultReceiver,
|
||||||
|
Context context,
|
||||||
|
UsbDevice usbDevice,
|
||||||
|
UsbManager usbManager,
|
||||||
|
ArrayList<NSPElement> nspElements) throws Exception {
|
||||||
|
super(resultReceiver, context, usbDevice, usbManager);
|
||||||
|
|
||||||
|
this.nspElements = nspElements;
|
||||||
|
String fileName;
|
||||||
|
InputStream fileInputStream;
|
||||||
|
|
||||||
|
fileInputStream = context.getContentResolver().openInputStream(nspElements.get(0).getUri());
|
||||||
|
fileName = nspElements.get(0).getFilename();
|
||||||
|
pfsElement = new PFSProvider(fileInputStream, fileName);
|
||||||
|
if (! pfsElement.init())
|
||||||
|
throw new Exception("GL File provided have incorrect structure and won't be uploaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean run() {
|
||||||
|
if (initGoldLeafProtocol(pfsElement))
|
||||||
|
status = context.getResources().getString(R.string.status_uploaded); // else - no change status that is already set to FAILED
|
||||||
|
|
||||||
|
finish();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initGoldLeafProtocol(PFSProvider pfsElement){
|
||||||
|
// Go parse commands
|
||||||
|
byte[] readByte;
|
||||||
|
|
||||||
|
// Go connect to GoldLeaf
|
||||||
|
if (writeUsb(CMD_GLUC)){
|
||||||
|
issueDescription = "GL Initiating GoldLeaf connection: 1/2";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(CMD_ConnectionRequest)){
|
||||||
|
issueDescription = "GL Initiating GoldLeaf connection: 2/2";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
readByte = readUsb();
|
||||||
|
if (readByte == null)
|
||||||
|
return false;
|
||||||
|
if (Arrays.equals(readByte, CMD_GLUC)) {
|
||||||
|
readByte = readUsb();
|
||||||
|
if (readByte == null)
|
||||||
|
return false;
|
||||||
|
if (Arrays.equals(readByte, CMD_ConnectionResponse)) {
|
||||||
|
if (!handleConnectionResponse(pfsElement))
|
||||||
|
return false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Arrays.equals(readByte, CMD_Start)) {
|
||||||
|
if (!handleStart(pfsElement))
|
||||||
|
return false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Arrays.equals(readByte, CMD_NSPContent)) {
|
||||||
|
if (!handleNSPContent(pfsElement, true))
|
||||||
|
return false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Arrays.equals(readByte, CMD_NSPTicket)) {
|
||||||
|
if (!handleNSPContent(pfsElement, false))
|
||||||
|
return false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Arrays.equals(readByte, CMD_Finish)) { // All good
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* ConnectionResponse command handler
|
||||||
|
* */
|
||||||
|
private boolean handleConnectionResponse(PFSProvider pfsElement){
|
||||||
|
if (writeUsb(CMD_GLUC)) {
|
||||||
|
issueDescription = "GL 'ConnectionResponse' command: INFO: [1/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(CMD_NSPName)) {
|
||||||
|
issueDescription = "GL 'ConnectionResponse' command: INFO: [2/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(pfsElement.getBytesNspFileNameLength())) {
|
||||||
|
issueDescription = "GL 'ConnectionResponse' command: INFO: [3/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(pfsElement.getBytesNspFileName())) {
|
||||||
|
issueDescription = "GL 'ConnectionResponse' command: INFO: [4/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Start command handler
|
||||||
|
* */
|
||||||
|
private boolean handleStart(PFSProvider pfsElement){
|
||||||
|
|
||||||
|
if (writeUsb(CMD_GLUC)) {
|
||||||
|
issueDescription = "GL Handle 'Start' command: [Send command prepare]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(CMD_NSPData)) {
|
||||||
|
issueDescription = "GL Handle 'Start' command: [Send command]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(pfsElement.getBytesCountOfNca())) {
|
||||||
|
issueDescription = "GL Handle 'Start' command: [Send length]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ncaCount = pfsElement.getIntCountOfNca();
|
||||||
|
|
||||||
|
for (int i = 0; i < ncaCount; i++){
|
||||||
|
if (writeUsb(pfsElement.getNca(i).getNcaFileNameLength())) {
|
||||||
|
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [1/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(pfsElement.getNca(i).getNcaFileName())) {
|
||||||
|
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [2/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getBodySize()+pfsElement.getNca(i).getNcaOffset()).array())) { // offset. real.
|
||||||
|
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [3/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getNca(i).getNcaSize()).array())) { // size
|
||||||
|
issueDescription = "GL Handle 'Start' command: File # "+i+"/"+ncaCount+" step: [4/4]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* NSPContent command handler
|
||||||
|
* isItRawRequest - if True, just ask NS what's needed
|
||||||
|
* - if False, send ticket
|
||||||
|
* */
|
||||||
|
private boolean handleNSPContent(PFSProvider pfsElement, boolean isItRawRequest){
|
||||||
|
int requestedNcaID;
|
||||||
|
|
||||||
|
if (isItRawRequest) {
|
||||||
|
byte[] readByte = readUsb();
|
||||||
|
if (readByte == null || readByte.length != 4) {
|
||||||
|
issueDescription = "GL Handle 'Content' command: [Read requested ID]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
requestedNcaID = ByteBuffer.wrap(readByte).order(ByteOrder.LITTLE_ENDIAN).getInt();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
requestedNcaID = pfsElement.getNcaTicketID();
|
||||||
|
}
|
||||||
|
|
||||||
|
long realNcaOffset = pfsElement.getNca(requestedNcaID).getNcaOffset()+pfsElement.getBodySize();
|
||||||
|
long realNcaSize = pfsElement.getNca(requestedNcaID).getNcaSize();
|
||||||
|
|
||||||
|
long readFrom = 0;
|
||||||
|
|
||||||
|
int readPice = 16384; // 8mb NOTE: consider switching to 1mb 1048576
|
||||||
|
byte[] readBuf;
|
||||||
|
|
||||||
|
try{
|
||||||
|
BufferedInputStream bufferedInStream = new BufferedInputStream(context.getContentResolver().openInputStream(nspElements.get(0).getUri())); // TODO: refactor?
|
||||||
|
if (bufferedInStream.skip(realNcaOffset) != realNcaOffset) {
|
||||||
|
issueDescription = "GL Failed to skip NCA offset";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int updateProgressPeriods = 0;
|
||||||
|
while (readFrom < realNcaSize){
|
||||||
|
if (readPice > (realNcaSize - readFrom))
|
||||||
|
readPice = (int)(realNcaSize - readFrom); // TODO: Troubles could raise here
|
||||||
|
readBuf = new byte[readPice];
|
||||||
|
if (bufferedInStream.read(readBuf) != readPice) {
|
||||||
|
issueDescription = "GL Failed to read data from file.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(readBuf)) {
|
||||||
|
issueDescription = "GL Failed to write data into NS.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
readFrom += readPice;
|
||||||
|
if (updateProgressPeriods++ % 1024 == 0) // Update progress bar after every 16mb goes to NS
|
||||||
|
updateProgressBar((int) ((readFrom+1)/(realNcaSize/100+1)));
|
||||||
|
}
|
||||||
|
bufferedInStream.close();
|
||||||
|
|
||||||
|
resetProgressBar();
|
||||||
|
}
|
||||||
|
catch (java.io.IOException ioe){
|
||||||
|
issueDescription = "GL Failed to read NCA ID "+requestedNcaID+". IO Exception: "+ioe.getMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader.service;
|
||||||
|
|
||||||
|
import com.blogspot.developersu.ns_usbloader.BuildConfig;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
public class NETPacket {
|
||||||
|
private static final String CODE_200 =
|
||||||
|
"HTTP/1.0 200 OK\r\n" +
|
||||||
|
"Server: NS-USBloader-M-v."+ BuildConfig.VERSION_NAME+"\r\n" +
|
||||||
|
"Date: %s\r\n" +
|
||||||
|
"Content-type: application/octet-stream\r\n" +
|
||||||
|
"Accept-Ranges: bytes\r\n" +
|
||||||
|
"Content-Range: bytes 0-%d/%d\r\n" +
|
||||||
|
"Content-Length: %d\r\n" +
|
||||||
|
"Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT\r\n\r\n";
|
||||||
|
private static final String CODE_206 =
|
||||||
|
"HTTP/1.0 206 Partial Content\r\n"+
|
||||||
|
"Server: NS-USBloader-M-v."+BuildConfig.VERSION_NAME+"\r\n" +
|
||||||
|
"Date: %s\r\n" +
|
||||||
|
"Content-type: application/octet-stream\r\n"+
|
||||||
|
"Accept-Ranges: bytes\r\n"+
|
||||||
|
"Content-Range: bytes %d-%d/%d\r\n"+
|
||||||
|
"Content-Length: %d\r\n"+
|
||||||
|
"Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT\r\n\r\n";
|
||||||
|
private static final String CODE_400 =
|
||||||
|
"HTTP/1.0 400 invalid range\r\n"+
|
||||||
|
"Server: NS-USBloader-M-v."+BuildConfig.VERSION_NAME+"\r\n" +
|
||||||
|
"Date: %s\r\n" +
|
||||||
|
"Connection: close\r\n"+
|
||||||
|
"Content-Type: text/html;charset=utf-8\r\n"+
|
||||||
|
"Content-Length: 0\r\n\r\n";
|
||||||
|
private static final String CODE_404 =
|
||||||
|
"HTTP/1.0 404 Not Found\r\n"+
|
||||||
|
"Server: NS-USBloader-M-v."+BuildConfig.VERSION_NAME+"\r\n" +
|
||||||
|
"Date: %s\r\n" +
|
||||||
|
"Connection: close\r\n"+
|
||||||
|
"Content-Type: text/html;charset=utf-8\r\n"+
|
||||||
|
"Content-Length: 0\r\n\r\n";
|
||||||
|
private static final String CODE_416 =
|
||||||
|
"HTTP/1.0 416 Requested Range Not Satisfiable\r\n"+
|
||||||
|
"Server: NS-USBloader-M-v."+BuildConfig.VERSION_NAME+"\r\n" +
|
||||||
|
"Date: %s\r\n" +
|
||||||
|
"Connection: close\r\n"+
|
||||||
|
"Content-Type: text/html;charset=utf-8\r\n"+
|
||||||
|
"Content-Length: 0\r\n\r\n";
|
||||||
|
public static String getCode200(long nspFileSize){
|
||||||
|
return String.format(Locale.US, CODE_200, getTime(), nspFileSize-1, nspFileSize, nspFileSize);
|
||||||
|
}
|
||||||
|
public static String getCode206(long nspFileSize, long startPos, long endPos){
|
||||||
|
return String.format(Locale.US, CODE_206, getTime(), startPos, endPos, nspFileSize, endPos-startPos+1);
|
||||||
|
}
|
||||||
|
public static String getCode404(){
|
||||||
|
return String.format(Locale.US, CODE_404, getTime());
|
||||||
|
}
|
||||||
|
public static String getCode416(){
|
||||||
|
return String.format(Locale.US, CODE_416, getTime());
|
||||||
|
}
|
||||||
|
public static String getCode400(){
|
||||||
|
return String.format(Locale.US, CODE_400, getTime());
|
||||||
|
}
|
||||||
|
private static String getTime(){
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US);
|
||||||
|
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
return sdf.format(Calendar.getInstance().getTime());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,333 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader.service;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import com.blogspot.developersu.ns_usbloader.R;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.view.NSPElement;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static android.content.Context.WIFI_SERVICE;
|
||||||
|
|
||||||
|
class TinfoilNET extends TransferTask {
|
||||||
|
|
||||||
|
private HashMap<String, NSPElement> nspMap;
|
||||||
|
|
||||||
|
private Socket handShakeSocket;
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
|
||||||
|
private OutputStream currSockOS;
|
||||||
|
private PrintWriter currSockPW;
|
||||||
|
|
||||||
|
private String nsIp;
|
||||||
|
private String phoneIp;
|
||||||
|
private int phonePort;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void cancel(){
|
||||||
|
super.cancel();
|
||||||
|
|
||||||
|
try{
|
||||||
|
handShakeSocket.close();
|
||||||
|
serverSocket.close();
|
||||||
|
}
|
||||||
|
catch (IOException | NullPointerException ignored){}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple constructor that everybody uses
|
||||||
|
* */
|
||||||
|
TinfoilNET(ResultReceiver resultReceiver,
|
||||||
|
Context context,
|
||||||
|
ArrayList<NSPElement> nspElements,
|
||||||
|
String nsIp,
|
||||||
|
String phoneIp,
|
||||||
|
int phonePort) throws Exception {
|
||||||
|
super(resultReceiver, context);
|
||||||
|
this.nsIp = nsIp;
|
||||||
|
this.phoneIp = phoneIp;
|
||||||
|
this.phonePort = phonePort;
|
||||||
|
|
||||||
|
this.nspMap = new HashMap<>();
|
||||||
|
// Collect and encode NSP files list
|
||||||
|
|
||||||
|
for (NSPElement nspElem : nspElements)
|
||||||
|
nspMap.put(URLEncoder.encode(nspElem.getFilename(), "UTF-8").replaceAll("\\+", "%20"), nspElem); // replace + to %20
|
||||||
|
|
||||||
|
// Resolve IP
|
||||||
|
if (phoneIp.isEmpty())
|
||||||
|
resolvePhoneIp();
|
||||||
|
// Open Server Socket on port
|
||||||
|
try {
|
||||||
|
serverSocket = new ServerSocket(phonePort);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new Exception("NET: Can't open socket using port: " + phonePort + ". Returned: "+ioe.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resolvePhoneIp() throws Exception{
|
||||||
|
WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(WIFI_SERVICE);
|
||||||
|
if (wm == null)
|
||||||
|
throw new Exception("NET: Unable to auto-resolve IP address.");
|
||||||
|
|
||||||
|
int intIp = wm.getConnectionInfo().getIpAddress();
|
||||||
|
phoneIp = String.format(Locale.US, "%d.%d.%d.%d",
|
||||||
|
(intIp & 0xff),
|
||||||
|
(intIp >> 8 & 0xff),
|
||||||
|
(intIp >> 16 & 0xff),
|
||||||
|
(intIp >> 24 & 0xff));
|
||||||
|
if (phoneIp.equals("0.0.0.0"))
|
||||||
|
throw new Exception("NET: Unable to auto-resolve IP address (0.0.0.0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean run(){
|
||||||
|
if (interrupt)
|
||||||
|
return false;
|
||||||
|
// Create string that we'll send to TF and which initiates chain
|
||||||
|
StringBuilder myStrBuilder;
|
||||||
|
|
||||||
|
myStrBuilder = new StringBuilder();
|
||||||
|
for (String fileNameEncoded : nspMap.keySet()) {
|
||||||
|
myStrBuilder.append(phoneIp);
|
||||||
|
myStrBuilder.append(':');
|
||||||
|
myStrBuilder.append(phonePort);
|
||||||
|
myStrBuilder.append('/');
|
||||||
|
myStrBuilder.append(fileNameEncoded);
|
||||||
|
myStrBuilder.append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] nspListNames = myStrBuilder.toString().getBytes(); // android's .getBytes() default == UTF8 // Follow the
|
||||||
|
byte[] nspListSize = ByteBuffer.allocate(4).putInt(nspListNames.length).array(); // defining order ; Integer size = 4 bytes
|
||||||
|
|
||||||
|
try {
|
||||||
|
handShakeSocket = new Socket();
|
||||||
|
handShakeSocket.connect(new InetSocketAddress(InetAddress.getByName(nsIp), 2000), 1000); // e.g. 1sec
|
||||||
|
OutputStream os = handShakeSocket.getOutputStream();
|
||||||
|
os.write(nspListSize);
|
||||||
|
os.write(nspListNames);
|
||||||
|
os.flush();
|
||||||
|
|
||||||
|
handShakeSocket.close();
|
||||||
|
}
|
||||||
|
catch (IOException uhe){
|
||||||
|
issueDescription = "NET: Unable to connect to NS and send files list. Returned: "+uhe.getMessage();
|
||||||
|
close(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Go transfer
|
||||||
|
work_routine:
|
||||||
|
while (true){
|
||||||
|
try {
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
|
||||||
|
BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(clientSocket.getInputStream())
|
||||||
|
);
|
||||||
|
|
||||||
|
currSockOS = clientSocket.getOutputStream();
|
||||||
|
currSockPW = new PrintWriter(new OutputStreamWriter(currSockOS));
|
||||||
|
|
||||||
|
String line;
|
||||||
|
LinkedList<String> tcpPacket = new LinkedList<>();
|
||||||
|
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
//System.out.println(line); // Debug
|
||||||
|
if (line.trim().isEmpty()) { // If TCP packet is ended
|
||||||
|
if (handleRequest(tcpPacket)) // Proceed required things
|
||||||
|
break work_routine;
|
||||||
|
tcpPacket.clear(); // Clear data and wait for next TCP packet
|
||||||
|
}
|
||||||
|
else
|
||||||
|
tcpPacket.add(line); // Otherwise collect data
|
||||||
|
}
|
||||||
|
clientSocket.close();
|
||||||
|
}
|
||||||
|
catch (IOException ioe){ // If server socket closed, then client socket also closed.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 200 206 400 (inv range) 404 416 (Range Not Satisfiable )
|
||||||
|
/**
|
||||||
|
* Handle requests
|
||||||
|
* @return true if failed
|
||||||
|
* */
|
||||||
|
private boolean handleRequest(LinkedList<String> packet){
|
||||||
|
String reqFileName = packet.get(0).replaceAll("(^[A-z\\s]+/)|(\\s+?.*$)", "");
|
||||||
|
|
||||||
|
if (! nspMap.containsKey(reqFileName)){
|
||||||
|
currSockPW.write(NETPacket.getCode404());
|
||||||
|
currSockPW.flush();
|
||||||
|
issueDescription = "NET: File "+reqFileName+" doesn't exists or have 0 size. Returning 404";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
NSPElement requestedElement = nspMap.get(reqFileName);
|
||||||
|
|
||||||
|
long reqFileSize = requestedElement.getSize();
|
||||||
|
|
||||||
|
if (reqFileSize == 0){ // well.. tell 404 if file exists with 0 length is against standard, but saves time
|
||||||
|
currSockPW.write(NETPacket.getCode404());
|
||||||
|
currSockPW.flush();
|
||||||
|
issueDescription = "NET: File "+reqFileName+" doesn't exists or have 0 size. Returning 404";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (packet.get(0).startsWith("HEAD")){
|
||||||
|
currSockPW.write(NETPacket.getCode200(reqFileSize));
|
||||||
|
currSockPW.flush();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (packet.get(0).startsWith("GET")) {
|
||||||
|
for (String line: packet) {
|
||||||
|
if (line.toLowerCase().startsWith("range")) { //todo: fix
|
||||||
|
try {
|
||||||
|
String[] rangeStr = line.toLowerCase().replaceAll("^range:\\s+?bytes=", "").split("-", 2);
|
||||||
|
if (!rangeStr[0].isEmpty() && !rangeStr[1].isEmpty()) { // If both ranges defined: Read requested
|
||||||
|
if (Long.parseLong(rangeStr[0]) > Long.parseLong(rangeStr[1])){ // If start bytes greater then end bytes
|
||||||
|
currSockPW.write(NETPacket.getCode400());
|
||||||
|
currSockPW.flush();
|
||||||
|
issueDescription = "NET: Requested range for "+requestedElement.getFilename()+" is incorrect. Returning 400";
|
||||||
|
requestedElement.setStatus(context.getResources().getString(R.string.status_failed_to_upload));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (writeToSocket(requestedElement, Long.parseLong(rangeStr[0]), Long.parseLong(rangeStr[1]))) // DO WRITE
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (!rangeStr[0].isEmpty()) { // If only START defined: Read all
|
||||||
|
if (writeToSocket(requestedElement, Long.parseLong(rangeStr[0]), reqFileSize)) // DO WRITE
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (!rangeStr[1].isEmpty()) { // If only END defined: Try to read last 500 bytes
|
||||||
|
if (reqFileSize > 500){
|
||||||
|
if (writeToSocket(requestedElement, reqFileSize-500, reqFileSize)) // DO WRITE
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else { // If file smaller than 500 bytes
|
||||||
|
currSockPW.write(NETPacket.getCode416());
|
||||||
|
currSockPW.flush();
|
||||||
|
issueDescription = "NET: File size requested for "+requestedElement.getFilename()+" while actual size of it: "+requestedElement.getSize()+". Returning 416";
|
||||||
|
requestedElement.setStatus(context.getResources().getString(R.string.status_failed_to_upload));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
currSockPW.write(NETPacket.getCode400()); // If Range not defined: like "Range: bytes=-"
|
||||||
|
currSockPW.flush();
|
||||||
|
issueDescription = "NET: Requested range for "+requestedElement.getFilename()+" is incorrect (empty start & end). Returning 400";
|
||||||
|
requestedElement.setStatus(context.getResources().getString(R.string.status_failed_to_upload));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (NumberFormatException nfe){
|
||||||
|
currSockPW.write(NETPacket.getCode400());
|
||||||
|
currSockPW.flush();
|
||||||
|
issueDescription = "NET: Requested range for "+requestedElement.getFilename()+" has incorrect format. Returning 400\n\t"+nfe.getMessage();
|
||||||
|
requestedElement.setStatus(context.getResources().getString(R.string.status_failed_to_upload));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Send files.
|
||||||
|
* */
|
||||||
|
private boolean writeToSocket(NSPElement nspElem, long start, long end){
|
||||||
|
if (interrupt)
|
||||||
|
return true;
|
||||||
|
currSockPW.write(NETPacket.getCode206(nspElem.getSize(), start, end));
|
||||||
|
currSockPW.flush();
|
||||||
|
try{
|
||||||
|
long count = end - start + 1; // Meeh. Somehow it works
|
||||||
|
|
||||||
|
InputStream elementIS = context.getContentResolver().openInputStream(nspElem.getUri());
|
||||||
|
if (elementIS == null) {
|
||||||
|
issueDescription = "NET Unable to obtain InputStream";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedInputStream bis = new BufferedInputStream(elementIS);
|
||||||
|
|
||||||
|
int readPice = 8388608; // = 8Mb
|
||||||
|
byte[] byteBuf;
|
||||||
|
|
||||||
|
if (bis.skip(start) != start){
|
||||||
|
issueDescription = "NET: Unable to skip requested range.";
|
||||||
|
nspElem.setStatus(context.getResources().getString(R.string.status_failed_to_upload));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
long currentOffset = 0;
|
||||||
|
while (currentOffset < count){
|
||||||
|
if (interrupt)
|
||||||
|
return true;
|
||||||
|
if ((currentOffset+readPice) >= count){
|
||||||
|
readPice = (int) (count - currentOffset);
|
||||||
|
}
|
||||||
|
byteBuf = new byte[readPice];
|
||||||
|
|
||||||
|
if (bis.read(byteBuf) != readPice){
|
||||||
|
issueDescription = "NET: Reading of nspElem stream suddenly ended.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
currSockOS.write(byteBuf);
|
||||||
|
|
||||||
|
currentOffset += readPice;
|
||||||
|
|
||||||
|
updateProgressBar((int) ((currentOffset+1)/(count/100+1)));
|
||||||
|
|
||||||
|
}
|
||||||
|
currSockOS.flush(); // TODO: check if this really needed.
|
||||||
|
bis.close();
|
||||||
|
resetProgressBar();
|
||||||
|
}
|
||||||
|
catch (IOException ioe){
|
||||||
|
issueDescription = "NET: File transmission failed. Returned: "+ioe.getMessage();
|
||||||
|
nspElem.setStatus(context.getResources().getString(R.string.status_failed_to_upload)); // TODO: REDUNDANT?
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Close when done
|
||||||
|
* */
|
||||||
|
private void close(boolean isFailed){
|
||||||
|
if (isFailed)
|
||||||
|
status = context.getResources().getString(R.string.status_failed_to_upload);
|
||||||
|
else
|
||||||
|
status = context.getResources().getString(R.string.status_unkown);
|
||||||
|
try {
|
||||||
|
if (serverSocket != null)
|
||||||
|
serverSocket.close(); // Closing server socket.
|
||||||
|
}
|
||||||
|
catch (IOException | NullPointerException ignored){}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getIssueDescription() {
|
||||||
|
return issueDescription;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader.service;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbManager;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import com.blogspot.developersu.ns_usbloader.R;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.view.NSPElement;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
class TinfoilUSB extends UsbTransfer {
|
||||||
|
private ArrayList<NSPElement> nspElements;
|
||||||
|
|
||||||
|
private byte[] replyConstArray = new byte[] { (byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30, // 'TUC0'
|
||||||
|
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, // CMD_TYPE_RESPONSE = 1
|
||||||
|
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
|
||||||
|
|
||||||
|
TinfoilUSB(ResultReceiver resultReceiver,
|
||||||
|
Context context,
|
||||||
|
UsbDevice usbDevice,
|
||||||
|
UsbManager usbManager,
|
||||||
|
ArrayList<NSPElement> nspElements) throws Exception{
|
||||||
|
super(resultReceiver, context, usbDevice, usbManager);
|
||||||
|
this.nspElements = nspElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean run(){
|
||||||
|
if (! sendListOfNSP()) {
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proceedCommands()) // REPORT SUCCESS
|
||||||
|
status = context.getResources().getString(R.string.status_uploaded); // Don't change status that is already set to FAILED TODO: FIX
|
||||||
|
finish();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send what NSP will be transferred
|
||||||
|
private boolean sendListOfNSP(){
|
||||||
|
// Send list of NSP files:
|
||||||
|
// Proceed "TUL0"
|
||||||
|
if (writeUsb("TUL0".getBytes())) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30} //"US-ASCII"?
|
||||||
|
issueDescription = "TF Send list of files: handshake failure";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//Collect file names
|
||||||
|
StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder
|
||||||
|
for(NSPElement element: nspElements) {
|
||||||
|
nspListNamesBuilder.append(element.getFilename()); // And here we come with java string default encoding (UTF-16)
|
||||||
|
nspListNamesBuilder.append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] nspListNames = nspListNamesBuilder.toString().getBytes(); // android's .getBytes() default == UTF8
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN); // integer = 4 bytes; BTW Java is stored in big-endian format
|
||||||
|
byteBuffer.putInt(nspListNames.length); // This way we obtain length in int converted to byte array in correct Big-endian order. Trust me.
|
||||||
|
byte[] nspListSize = byteBuffer.array();
|
||||||
|
|
||||||
|
// Sending NSP list
|
||||||
|
if (writeUsb(nspListSize)) { // size of the list we're going to transfer goes...
|
||||||
|
issueDescription = "TF Send list of files: [send list length]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(new byte[8])) { // 8 zero bytes goes...
|
||||||
|
issueDescription = "TF Send list of files: [send padding]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeUsb(nspListNames)) { // list of the names goes...
|
||||||
|
issueDescription = "TF Send list of files: [send list itself]";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After we sent commands to NS, this chain starts
|
||||||
|
|
||||||
|
private boolean proceedCommands(){
|
||||||
|
final byte[] magic = new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30}; // eq. 'TUC0' @ UTF-8 (actually ASCII lol, u know what I mean)
|
||||||
|
|
||||||
|
byte[] receivedArray;
|
||||||
|
|
||||||
|
while (true){
|
||||||
|
receivedArray = readUsb();
|
||||||
|
if (receivedArray == null)
|
||||||
|
return false; // catches exception
|
||||||
|
|
||||||
|
if (!Arrays.equals(Arrays.copyOfRange(receivedArray, 0,4), magic)) // Bytes from 0 to 3 should contain 'magic' TUC0, so must be verified like this
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// 8th to 12th(explicits) bytes in returned data stands for command ID as unsigned integer (Little-endian). Actually, we have to compare arrays here, but in real world it can't be greater then 0/1/2, thus:
|
||||||
|
// BTW also protocol specifies 4th byte to be 0x00 kinda indicating that that this command is valid. But, as you may see, never happens other situation when it's not = 0.
|
||||||
|
if (receivedArray[8] == 0x00){ //0x00 - exit
|
||||||
|
return true; // All interaction with USB device should be ended (expected);
|
||||||
|
}
|
||||||
|
else if ((receivedArray[8] == 0x01) || (receivedArray[8] == 0x02)){ //0x01 - file range; 0x02 unknown bug on backend side (dirty hack).
|
||||||
|
if (!fileRangeCmd()) // issueDescription inside
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This is what returns requested file (files)
|
||||||
|
* Executes multiple times
|
||||||
|
* @return 'true' if everything is ok
|
||||||
|
* 'false' is error/exception occurs
|
||||||
|
* */
|
||||||
|
|
||||||
|
private boolean fileRangeCmd(){
|
||||||
|
byte[] receivedArray;
|
||||||
|
// Here we take information of what other side wants
|
||||||
|
receivedArray = readUsb();
|
||||||
|
if (receivedArray == null) {
|
||||||
|
issueDescription = "TF Unable to get meta information @fileRangeCmd()";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// range_offset of the requested file. In the begining it will be 0x10.
|
||||||
|
long receivedRangeSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 0,8)).order(ByteOrder.LITTLE_ENDIAN).getLong();
|
||||||
|
byte[] receivedRangeSizeRAW = Arrays.copyOfRange(receivedArray, 0,8);
|
||||||
|
long receivedRangeOffset = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 8,16)).order(ByteOrder.LITTLE_ENDIAN).getLong();
|
||||||
|
|
||||||
|
// Requesting UTF-8 file name required:
|
||||||
|
receivedArray = readUsb();
|
||||||
|
if (receivedArray == null) {
|
||||||
|
issueDescription = "TF Unable to get file name @fileRangeCmd()";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String receivedRequestedNSP;
|
||||||
|
try {
|
||||||
|
receivedRequestedNSP = new String(receivedArray, "UTF-8"); //TODO:FIX
|
||||||
|
}
|
||||||
|
catch (java.io.UnsupportedEncodingException uee){
|
||||||
|
issueDescription = "TF UnsupportedEncodingException @fileRangeCmd()";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending response header
|
||||||
|
if (sendResponse(receivedRangeSizeRAW)) // Get receivedRangeSize in 'RAW' format exactly as it has been received. It's simply.
|
||||||
|
return false; // issueDescription handled by method
|
||||||
|
|
||||||
|
try {
|
||||||
|
BufferedInputStream bufferedInStream = null;
|
||||||
|
|
||||||
|
for (NSPElement e: nspElements){
|
||||||
|
if (e.getFilename().equals(receivedRequestedNSP)){
|
||||||
|
InputStream elementIS = context.getContentResolver().openInputStream(e.getUri());
|
||||||
|
if (elementIS == null) {
|
||||||
|
issueDescription = "TF Unable to obtain InputStream";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bufferedInStream = new BufferedInputStream(elementIS); // TODO: refactor?
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferedInStream == null) {
|
||||||
|
issueDescription = "TF Unable to create BufferedInputStream";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] readBuf;//= new byte[1048576]; // eq. Allocate 1mb
|
||||||
|
|
||||||
|
if (bufferedInStream.skip(receivedRangeOffset) != receivedRangeOffset){
|
||||||
|
issueDescription = "TF Requested skip is out of file size. Nothing to transmit.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long readFrom = 0;
|
||||||
|
// 'End Offset' equal to receivedRangeSize.
|
||||||
|
int readPice = 16384; // 8388608 = 8Mb
|
||||||
|
int updateProgressPeriods = 0;
|
||||||
|
|
||||||
|
while (readFrom < receivedRangeSize){
|
||||||
|
if ((readFrom + readPice) >= receivedRangeSize )
|
||||||
|
readPice = (int)(receivedRangeSize - readFrom); // TODO: Troubles could raise here
|
||||||
|
|
||||||
|
readBuf = new byte[readPice]; // TODO: not perfect moment, consider refactoring.
|
||||||
|
|
||||||
|
if (bufferedInStream.read(readBuf) != readPice) {
|
||||||
|
issueDescription = "TF Reading of stream suddenly ended";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//write to USB
|
||||||
|
if (writeUsb(readBuf)) {
|
||||||
|
issueDescription = "TF Failure during NSP transmission.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readFrom += readPice;
|
||||||
|
|
||||||
|
if (updateProgressPeriods++ % 1024 == 0) // Update progress bar after every 16mb goes to NS
|
||||||
|
updateProgressBar((int) ((readFrom+1)/(receivedRangeSize/100+1))); // This shit takes too much time
|
||||||
|
//Log.i("LPR", "CO: "+readFrom+"RRS: "+receivedRangeSize+"RES: "+(readFrom+1/(receivedRangeSize/100+1)));
|
||||||
|
}
|
||||||
|
bufferedInStream.close();
|
||||||
|
|
||||||
|
resetProgressBar();
|
||||||
|
} catch (java.io.IOException ioe){
|
||||||
|
issueDescription = "TF IOException: "+ioe.getMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Send response header.
|
||||||
|
* @return false if everything OK
|
||||||
|
* true if failed
|
||||||
|
* */
|
||||||
|
private boolean sendResponse(byte[] rangeSize){
|
||||||
|
if (writeUsb(replyConstArray)){
|
||||||
|
issueDescription = "TF Response: [1/3]";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(writeUsb(rangeSize)) { // Send EXACTLY what has been received
|
||||||
|
issueDescription = "TF Response: [2/3]";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(writeUsb(new byte[12])) { // kinda another one padding
|
||||||
|
issueDescription = "TF Response: [3/3]";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader.service;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
|
|
||||||
|
import com.blogspot.developersu.ns_usbloader.MainActivity;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.NsConstants;
|
||||||
|
import com.blogspot.developersu.ns_usbloader.R;
|
||||||
|
|
||||||
|
abstract class TransferTask {
|
||||||
|
|
||||||
|
private final static boolean isModernAndroidOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||||
|
private NotificationManager notificationManager;
|
||||||
|
|
||||||
|
private NotificationCompat.Builder notificationBuilder;
|
||||||
|
|
||||||
|
private ResultReceiver resultReceiver;
|
||||||
|
Context context;
|
||||||
|
|
||||||
|
String issueDescription;
|
||||||
|
String status = "";
|
||||||
|
|
||||||
|
volatile boolean interrupt;
|
||||||
|
|
||||||
|
TransferTask(ResultReceiver resultReceiver, Context context){
|
||||||
|
this.interrupt = false;
|
||||||
|
this.resultReceiver = resultReceiver;
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
this.createNotificationChannel();
|
||||||
|
this.notificationBuilder = new NotificationCompat.Builder(context, NsConstants.NOTIFICATION_FOREGROUND_SERVICE_CHAN_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setContentTitle(context.getString(R.string.notification_transfer_in_progress))
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
String getIssueDescription() {
|
||||||
|
return issueDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetProgressBar(){
|
||||||
|
resultReceiver.send(NsConstants.NS_RESULT_PROGRESS_INDETERMINATE, Bundle.EMPTY);
|
||||||
|
resetNotificationProgressBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProgressBar(int currentPosition){
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putInt("POSITION", currentPosition);
|
||||||
|
resultReceiver.send(NsConstants.NS_RESULT_PROGRESS_VALUE, bundle);
|
||||||
|
updateNotificationProgressBar(currentPosition);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Main work routine here
|
||||||
|
* @return true if issue, false if not
|
||||||
|
* */
|
||||||
|
abstract boolean run();
|
||||||
|
/**
|
||||||
|
* What shall we do in case of user interruption
|
||||||
|
* */
|
||||||
|
void cancel(){
|
||||||
|
interrupt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNotificationProgressBar(int value){
|
||||||
|
final Notification notify = notificationBuilder.setProgress(100, value, false).setContentText(value+"%").build();
|
||||||
|
if (isModernAndroidOs) {
|
||||||
|
notificationManager.notify(NsConstants.NOTIFICATION_TRANSFER_ID, notify);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationManagerCompat.from(context).notify(NsConstants.NOTIFICATION_TRANSFER_ID, notify);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetNotificationProgressBar(){
|
||||||
|
final Notification notify = notificationBuilder.setProgress(0, 0, true).setContentText("").build();
|
||||||
|
|
||||||
|
if (isModernAndroidOs) {
|
||||||
|
notificationManager.notify(NsConstants.NOTIFICATION_TRANSFER_ID, notify);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationManagerCompat.from(context).notify(NsConstants.NOTIFICATION_TRANSFER_ID, notify);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel(){
|
||||||
|
if (isModernAndroidOs) {
|
||||||
|
CharSequence notificationChanName = context.getString(R.string.notification_chan_name_progress);
|
||||||
|
String notificationChanDesc = context.getString(R.string.notification_chan_desc_progress);
|
||||||
|
|
||||||
|
NotificationChannel notificationChannel = new NotificationChannel(
|
||||||
|
NsConstants.NOTIFICATION_FOREGROUND_SERVICE_CHAN_ID,
|
||||||
|
notificationChanName,
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
notificationChannel.setDescription(notificationChanDesc);
|
||||||
|
notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
|
notificationManager.createNotificationChannel(notificationChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.blogspot.developersu.ns_usbloader.service;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
import android.hardware.usb.UsbEndpoint;
|
||||||
|
import android.hardware.usb.UsbInterface;
|
||||||
|
import android.hardware.usb.UsbManager;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
abstract class UsbTransfer extends TransferTask {
|
||||||
|
|
||||||
|
private UsbDeviceConnection deviceConnection;
|
||||||
|
private UsbInterface usbInterface;
|
||||||
|
private UsbEndpoint epIn;
|
||||||
|
private UsbEndpoint epOut;
|
||||||
|
|
||||||
|
UsbTransfer(ResultReceiver resultReceiver, Context context, UsbDevice usbDevice, UsbManager usbManager) throws Exception{
|
||||||
|
super(resultReceiver, context);
|
||||||
|
|
||||||
|
if (usbManager == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usbInterface = usbDevice.getInterface(0);
|
||||||
|
epIn = usbInterface.getEndpoint(0); // For bulk read
|
||||||
|
epOut = usbInterface.getEndpoint(1); // For bulk write
|
||||||
|
|
||||||
|
deviceConnection = usbManager.openDevice(usbDevice);
|
||||||
|
|
||||||
|
if ( ! deviceConnection.claimInterface(usbInterface, false)) {
|
||||||
|
issueDescription = "USB: failed to claim interface";
|
||||||
|
throw new Exception("USB: failed to claim interface");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sending any byte array to USB device
|
||||||
|
* @return 'false' if no issues
|
||||||
|
* 'true' if errors happened
|
||||||
|
* */
|
||||||
|
boolean writeUsb(byte[] message){
|
||||||
|
int bytesWritten;
|
||||||
|
while (! interrupt){
|
||||||
|
bytesWritten = deviceConnection.bulkTransfer(epOut, message, message.length, 5050); // timeout 0 - unlimited
|
||||||
|
if (bytesWritten != 0)
|
||||||
|
return (bytesWritten != message.length);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reading what USB device responded.
|
||||||
|
* @return byte array if data read successful
|
||||||
|
* 'null' if read failed
|
||||||
|
* */
|
||||||
|
byte[] readUsb(){
|
||||||
|
byte[] readBuffer = new byte[512];
|
||||||
|
int readResult;
|
||||||
|
while (! interrupt) {
|
||||||
|
readResult = deviceConnection.bulkTransfer(epIn, readBuffer, 512, 1000); // timeout 0 - unlimited
|
||||||
|
if (readResult > 0)
|
||||||
|
return Arrays.copyOf(readBuffer, readResult);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish(){
|
||||||
|
deviceConnection.releaseInterface(usbInterface);
|
||||||
|
deviceConnection.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.blogspot.developersu.ns_usbloader.View;
|
package com.blogspot.developersu.ns_usbloader.view;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.blogspot.developersu.ns_usbloader.View;
|
package com.blogspot.developersu.ns_usbloader.view;
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
9
app/src/main/res/drawable/ic_settings.xml
Normal file
9
app/src/main/res/drawable/ic_settings.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
|
||||||
|
</vector>
|
25
app/src/main/res/layout/activity_settings.xml
Normal file
25
app/src/main/res/layout/activity_settings.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".SettingsActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:theme="@style/AppTheme.AppBarOverlay">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
app:popupTheme="@style/AppTheme.PopupOverlay" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<include layout="@layout/content_settings" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -33,6 +33,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:paddingTop="5dp"
|
||||||
android:text="@string/about_line_1"
|
android:text="@string/about_line_1"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
|
||||||
|
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
android:id="@+id/textView2"
|
android:id="@+id/textView2"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="5dp"
|
||||||
android:text="@string/about_line_2"
|
android:text="@string/about_line_2"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||||
|
|
||||||
|
@ -47,6 +49,7 @@
|
||||||
android:id="@+id/textView3"
|
android:id="@+id/textView3"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="5dp"
|
||||||
android:text="@string/about_line_3"
|
android:text="@string/about_line_3"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||||
|
|
||||||
|
@ -54,6 +57,7 @@
|
||||||
android:id="@+id/textView4"
|
android:id="@+id/textView4"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="5dp"
|
||||||
android:text="@string/about_line_4"
|
android:text="@string/about_line_4"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||||
|
|
||||||
|
@ -61,6 +65,7 @@
|
||||||
android:id="@+id/textView5"
|
android:id="@+id/textView5"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="5dp"
|
||||||
android:text="@string/about_line_5"
|
android:text="@string/about_line_5"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||||
|
|
||||||
|
@ -68,6 +73,7 @@
|
||||||
android:id="@+id/textView6"
|
android:id="@+id/textView6"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="5dp"
|
||||||
android:text="@string/about_line_6"
|
android:text="@string/about_line_6"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
105
app/src/main/res/layout/content_settings.xml
Normal file
105
app/src/main/res/layout/content_settings.xml
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:context=".SettingsActivity"
|
||||||
|
tools:showIn="@layout/activity_settings">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/tf_net" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/nsIpTv"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_nsIp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/nsIpEditText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:digits="0123456789."
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="xxx.xxx.xxx.xxx"
|
||||||
|
android:inputType="number" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/autoDetectIpSW"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_autodtct_phn_ip" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/servAddrTv"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_phone_ip" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/servAddrTextEdit"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:digits="0123456789."
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="xxx.xxx.xxx.xxx"
|
||||||
|
android:inputType="number" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/servPortTv"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_phone_port" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/servPortTextEdit"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:digits="0123456789"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="1024-65535"
|
||||||
|
android:inputType="number" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -31,6 +31,10 @@
|
||||||
|
|
||||||
<item android:title="@string/other">
|
<item android:title="@string/other">
|
||||||
<menu>
|
<menu>
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_settings"
|
||||||
|
android:icon="@drawable/ic_settings"
|
||||||
|
android:title="@string/settings" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_about"
|
android:id="@+id/nav_about"
|
||||||
android:icon="@drawable/ic_info"
|
android:icon="@drawable/ic_info"
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">NS-USBloader</string>
|
<string name="select_file_btn">Выбрать файлы</string>
|
||||||
<string name="select_file_btn">Выбрать .NSP</string>
|
<string name="popup_non_supported_format">Тип файла не поддерживается</string>
|
||||||
<string name="popup_wrong_file">Неприавильный тип файла</string>
|
|
||||||
<string name="popup_error">Ошибка</string>
|
<string name="popup_error">Ошибка</string>
|
||||||
<string name="upload_btn">Отправить в NS</string>
|
<string name="upload_btn">Отправить в NS</string>
|
||||||
<string name="install_file_explorer">Не установлено ни одного файлового менеджера.\n Пожалуйста установите один. Например Total Commander.</string>
|
<string name="install_file_explorer">Не установлено ни одного файлового менеджера.\n Пожалуйста установите один. Например Total Commander.</string>
|
||||||
<string name="ns_connected_info">NS подключён</string>
|
<string name="ns_connected_info">NS подключён</string>
|
||||||
<string name="notification_channel_name">Устройство подключено</string>
|
<string name="notification_chan_name_usb">Устройство подключено</string>
|
||||||
<string name="notification_channel_description">Уведомление повяляется когда пользователь подключает NS к устройству</string>
|
<string name="notification_chan_desc_usb">Уведомление повяляется когда пользователь подключает NS к устройству</string>
|
||||||
<string name="ns_not_found_in_connected">NS не найден среди подключенных устройств.</string>
|
<string name="ns_not_found_in_connected">NS не найден среди подключенных устройств.</string>
|
||||||
<string name="transfer_protocol">Протокол</string>
|
<string name="transfer_protocol">Протокол</string>
|
||||||
<string name="transfer_transport">Транспортный уровень</string>
|
<string name="transfer_transport">Транспортный уровень</string>
|
||||||
|
@ -19,16 +18,25 @@
|
||||||
<string name="navigation_drawer_close">Закрыть панель навигации</string>
|
<string name="navigation_drawer_close">Закрыть панель навигации</string>
|
||||||
<string name="about_app">О приложении</string>
|
<string name="about_app">О приложении</string>
|
||||||
<string name="other">Другое</string>
|
<string name="other">Другое</string>
|
||||||
<string name="one_item_for_gl_notification">Только один файл может быть выделен для GoldLeaf</string>
|
<string name="one_item_for_gl_notification">Только один файл может быть выделен для GoldLeaf v0.5</string>
|
||||||
<string name="no_protocol_selected_message">Не выбрано ни одного протокола</string>
|
<string name="no_protocol_selected_message">Не выбрано ни одного протокола</string>
|
||||||
<string name="title_activity_about">О приложении</string>
|
<string name="title_activity_about">О приложении</string>
|
||||||
<string name="about_line_2">Это приложение <a href="https://www.gnu.org/licenses/gpl-3.0.html">распространяется под лицензией GNU GPLv3.</a></string>
|
<string name="about_line_2">Это приложение <a href="https://www.gnu.org/licenses/gpl-3.0.html">распространяется под лицензией GNU GPLv3</a> или любой более поздней версии.</string>
|
||||||
<string name="about_line_3">Автор: Дмитрий Исаенко.</string>
|
<string name="about_line_3">Автор: Дмитрий Исаенко.</string>
|
||||||
<string name="about_line_4">Домашняя страница: <a href="https://github.com/developersu/ns-usbloader-mobile">https://github.com/developersu/ns-usbloader-mobile</a></string>
|
<string name="about_line_4">Домашняя страница: <a href="https://github.com/developersu/ns-usbloader-mobile">https://github.com/developersu/ns-usbloader-mobile</a></string>
|
||||||
<string name="about_line_5">Переводчики: Если таковые имеются, то все они указаны на домашней странице.</string>
|
<string name="about_line_5">Переводчики: Если таковые имеются, то все они указаны на домашней странице.</string>
|
||||||
<string name="about_line_6">Если вам нравится эта программа, можете поставить звёздочку на GitHub. Если вы хотите поддержать приложение, пожалуйста обратитесь к домашней странице проекта за информацией. Пожалуйста учтите, что это некоммерческое приложение. Ваши вложения рассматриваются как подарок, и, вероятнее всего, они будут потрачены на покупку шоколадок :)</string>
|
<string name="about_line_6">Если вам нравится эта программа, можете поставить звёздочку на GitHub. Донаты по желанию <a href="https://www.paypal.me/developersu">paypal.me/developersu</a>, <a href="https://money.yandex.ru/to/410014301951665">Яндекс.Деньги</a>. Больше информации на домашней странице.</string>
|
||||||
<string name="popup_not_supported">Не поддерживается</string>
|
<string name="popup_incorrect_file">Неправильный файл</string>
|
||||||
<string name="status_uploaded">(Передан)</string>
|
<string name="status_uploaded">(Передан)</string>
|
||||||
<string name="status_failed_to_upload">(Передача провалилась)</string>
|
<string name="status_failed_to_upload">(Передача провалилась)</string>
|
||||||
<string name="status_wrong_file">(Плохой NSP файл)</string>
|
<string name="status_wrong_file">(Плохой NSP файл)</string>
|
||||||
|
<string name="settings">Настройки</string>
|
||||||
|
<string name="title_activity_settings">Настройки</string>
|
||||||
|
<string name="settings_phone_ip">IP смартфона</string>
|
||||||
|
<string name="settings_phone_port">Порт смартфона</string>
|
||||||
|
<string name="settings_autodtct_phn_ip">Авто-определение IP смартфона</string>
|
||||||
|
<string name="unknown_protocol_error">Выбран неизвестный протокол (?)</string>
|
||||||
|
<string name="notification_transfer_in_progress">Идет передача данных</string>
|
||||||
|
<string name="notification_chan_desc_progress">Уведомление демонстрирует прогресс передачи данных</string>
|
||||||
|
<string name="notification_chan_name_progress">Передача данных</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">NS-USBloader</string>
|
<string name="app_name" translatable="false">NS-USBloader</string>
|
||||||
<string name="select_file_btn">Select .NSP</string>
|
<string name="select_file_btn">Select files</string>
|
||||||
<string name="popup_wrong_file">Incorrect file type</string>
|
<string name="popup_non_supported_format">File type not supported</string>
|
||||||
<string name="popup_error">Error</string>
|
<string name="popup_error">Error</string>
|
||||||
<string name="upload_btn">Upload to NS</string>
|
<string name="upload_btn">Upload to NS</string>
|
||||||
<string name="install_file_explorer">No file explorer installed.\\n Please install one. For example Total Commander.</string>
|
<string name="install_file_explorer">No file explorer installed.\\n Please install one. For example Total Commander.</string>
|
||||||
<string name="ns_connected_info">NS Connected</string>
|
<string name="ns_connected_info">NS Connected</string>
|
||||||
<string name="notification_channel_name">Device connected</string>
|
<string name="notification_chan_name_usb">Device connected</string>
|
||||||
<string name="notification_channel_description">Notification appears when user connects NS to device</string>
|
<string name="notification_chan_desc_usb">Notification appears when user connects NS to device</string>
|
||||||
<string name="ns_not_found_in_connected">NS not found in connected devices.</string>
|
<string name="ns_not_found_in_connected">NS not found in connected devices.</string>
|
||||||
<string name="transfer_protocol">Protocol</string>
|
<string name="transfer_protocol">Protocol</string>
|
||||||
<string name="transfer_transport">Transport level</string>
|
<string name="transfer_transport">Transport level</string>
|
||||||
|
@ -17,21 +17,32 @@
|
||||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||||
<string name="tf_usb" translatable="false">TinFoil USB</string>
|
<string name="tf_usb" translatable="false">TinFoil USB</string>
|
||||||
<string name="gl" translatable="false">GoldLeaf</string>
|
<string name="gl" translatable="false">GoldLeaf v0.5</string>
|
||||||
<string name="tf_net" translatable="false">TinFoil NET</string>
|
<string name="tf_net" translatable="false">TinFoil NET</string>
|
||||||
<string name="about_app">About this app</string>
|
<string name="about_app">About this app</string>
|
||||||
<string name="other">Other</string>
|
<string name="other">Other</string>
|
||||||
<string name="one_item_for_gl_notification">Only one file could be selected for GoldLeaf</string>
|
<string name="one_item_for_gl_notification">Only one file could be selected for GoldLeaf v0.5</string>
|
||||||
<string name="no_protocol_selected_message">No protocol selected</string>
|
<string name="no_protocol_selected_message">No protocol selected</string>
|
||||||
<string name="title_activity_about">About application</string>
|
<string name="title_activity_about">About application</string>
|
||||||
<string name="about_line_1" translatable="false">NS-USBloader mobile</string>
|
<string name="about_line_1" translatable="false">NS-USBloader mobile</string>
|
||||||
<string name="about_line_2">This application <a href="https://www.gnu.org/licenses/gpl-3.0.html">licensed under GNU GPLv3</a>.</string>
|
<string name="about_line_2">This application <a href="https://www.gnu.org/licenses/gpl-3.0.html">licensed under GNU GPLv3</a> or any later version.</string>
|
||||||
<string name="about_line_3">Author: Dmitry Isaenko.</string>
|
<string name="about_line_3">Author: Dmitry Isaenko.</string>
|
||||||
<string name="about_line_4">Home page: <a href="https://github.com/developersu/ns-usbloader-mobile">https://github.com/developersu/ns-usbloader-mobile</a></string>
|
<string name="about_line_4">Home page: <a href="https://github.com/developersu/ns-usbloader-mobile">https://github.com/developersu/ns-usbloader-mobile</a></string>
|
||||||
<string name="about_line_5">Translators: If there are any, they\'re noted on home page.</string>
|
<string name="about_line_5">Translators: If there are any, they\'re noted on home page.</string>
|
||||||
<string name="about_line_6">If you found this application useful, you\'re more than welcome to give a star on GitHub page. If you want to support application please refer to home page for information. Please notice: it\'s not a commercial product. Any donations are considered as a gift and most likely would be spent for buying chocolates :)</string>
|
<string name="about_line_6">If you found this application useful, you\'re more than welcome to give a star on GitHub page. Donations (optional) <a href="https://www.paypal.me/developersu">paypal.me/developersu</a>, <a href="https://money.yandex.ru/to/410014301951665">Yandex.Money</a>. More information on home page.</string>
|
||||||
<string name="popup_not_supported">Not supported</string>
|
<string name="popup_incorrect_file">Incorrect file</string>
|
||||||
<string name="status_failed_to_upload">(Upload failure)</string>
|
<string name="status_failed_to_upload">(Upload failure)</string>
|
||||||
<string name="status_wrong_file">(Bad NSP)</string>
|
<string name="status_wrong_file">(Bad NSP)</string>
|
||||||
<string name="status_uploaded">(Uploaded)</string>
|
<string name="status_uploaded">(Uploaded)</string>
|
||||||
|
<string name="status_unkown" translatable="false">(?)</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="title_activity_settings">Settings</string>
|
||||||
|
<string name="settings_nsIp" translatable="false">NS IP</string>
|
||||||
|
<string name="settings_phone_ip">Phone IP</string>
|
||||||
|
<string name="settings_phone_port">Phone port</string>
|
||||||
|
<string name="settings_autodtct_phn_ip">Auto-detect phone IP</string>
|
||||||
|
<string name="unknown_protocol_error">Unknown protocol selected (?)</string>
|
||||||
|
<string name="notification_transfer_in_progress">Data transfer in progress</string>
|
||||||
|
<string name="notification_chan_name_progress">Transfer in progress</string>
|
||||||
|
<string name="notification_chan_desc_progress">Notification indicates transfer progress</string>
|
||||||
</resources>
|
</resources>
|
|
@ -7,7 +7,7 @@ buildscript {
|
||||||
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
#Mon Jun 17 23:55:11 MSK 2019
|
#Mon Mar 23 02:19:51 MSK 2020
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
||||||
|
|
Loading…
Reference in a new issue