Tinfoil network transfer support

Change progress bar behavior.
Add settings.
Add NCZ/XCI/XCZ 'support' (Fix bug #1).
Make IntentService foreground service.
master
Dmitry Isaenko 2020-03-23 20:33:17 +03:00
parent 32a5d36caf
commit 0061551e6b
31 changed files with 1885 additions and 805 deletions

View 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>

View File

@ -12,7 +12,7 @@ Sometimes I add new posts about this project [on my home page](https://developer
#### 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.
@ -23,11 +23,13 @@ Logo font: [Play](https://fonts.google.com/specimen/Play) by Jonas Hecksher. Ope
* Maximum: Android 10.0 (Q)
* USB-OTG support
* USB-OTG support / WiFi
### Usage
1. Open TinFoil/GoldLeaf
#### USB
1. Open Awoo Installer/Tinfoil/GoldLeaf v0.5
2. Connect NS to Android device using OTG cable.
3. Allow interaction request. Application opens.
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.
#### 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
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
'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:
- [ ] Tinfoil NET transfer support
- [x] Tinfoil NET transfer support
- [ ] Better UI
- [ ] Multi-select files (if possible)

View File

@ -7,8 +7,8 @@ android {
applicationId "com.blogspot.developersu.ns_usbloader"
minSdkVersion 15
targetSdkVersion 29
versionCode 1
versionName "0.1"
versionCode 2
versionName "0.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
android.defaultConfig.vectorDrawables.useSupportLibrary = true
}
@ -26,12 +26,12 @@ android {
dependencies {
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'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.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 'com.google.android.material:material:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
}

View File

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.blogspot.developersu.ns_usbloader">
<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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For Android 9 -->
<application
android:allowBackup="true"
@ -13,11 +17,16 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
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
android:name=".AboutActivity"
android:label="@string/title_activity_about"
android:theme="@style/AppTheme.NoActionBar"></activity>
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -39,7 +48,7 @@
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_DETACHED"
android:resource="@xml/device_filter" />
<!--
<!-- TODO: fix, implement, etc.
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
@ -74,7 +83,8 @@
android:resource="@xml/device_filter" />
</receiver>
<service android:name=".Service.CommunicationsService" />
<service android:name=".service.CommunicationsService"
android:exported="false" />
</application>
</manifest>

View File

@ -23,12 +23,13 @@ public class AboutActivity extends AppCompatActivity {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
TextView t1 = (TextView) findViewById(R.id.textView1);
final TextView t1 = findViewById(R.id.textView1);
t1.append(" v."+BuildConfig.VERSION_NAME);
TextView t2 = (TextView) findViewById(R.id.textView2);
final TextView t2 = findViewById(R.id.textView2);
t2.setMovementMethod(LinkMovementMethod.getInstance());
TextView t4 = (TextView) findViewById(R.id.textView4);
final TextView t4 = findViewById(R.id.textView4);
t4.setMovementMethod(LinkMovementMethod.getInstance());
final TextView t6 = findViewById(R.id.textView6);
t6.setMovementMethod(LinkMovementMethod.getInstance());
}
}

View File

@ -26,12 +26,11 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.blogspot.developersu.ns_usbloader.Model.NsResultReciever;
import com.blogspot.developersu.ns_usbloader.Service.CommunicationsService;
import com.blogspot.developersu.ns_usbloader.View.NSPElement;
import com.blogspot.developersu.ns_usbloader.View.NspItemsAdapter;
import com.blogspot.developersu.ns_usbloader.model.NsResultReciever;
import com.blogspot.developersu.ns_usbloader.service.CommunicationsService;
import com.blogspot.developersu.ns_usbloader.view.NSPElement;
import com.blogspot.developersu.ns_usbloader.view.NspItemsAdapter;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
@ -64,7 +63,7 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
private NsResultReciever nsResultReciever;
@Override
protected void onSaveInstanceState(Bundle outState) {
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("DATASET_LIST", mDataset);
outState.putParcelable("USB_DEVICE", usbDevice);
@ -89,6 +88,7 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
registerReceiver(innerBroadcastReceiver, intentFilter);
nsResultReciever.setReceiver(this);
blockUI(CommunicationsService.isServiceActive());
}
@Override
@ -140,36 +140,31 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
// Handle back button push when drawer opened
@Override
public void onBackPressed() {
DrawerLayout drawer = findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START))
drawer.closeDrawer(GravityCompat.START);
} else {
else
super.onBackPressed();
}
}
// Drawer actions
@Override
public boolean onNavigationItemSelected(MenuItem item) {
// Handle navigation view item clicks here.
int id = item.getItemId();
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()){
for (NSPElement element: mDataset){
element.setSelected(false);
switch (item.getItemId()){
case R.id.nav_gl:
if (! mDataset.isEmpty()){
for (NSPElement element: mDataset)
element.setSelected(false);
mAdapter.notifyDataSetChanged();
}
mAdapter.notifyDataSetChanged();
}
}
else if (id == R.id.nav_about) {
//Log.i("LPR", "ABOUT");
startActivity(new Intent(this, AboutActivity.class));
break;
case R.id.nav_settings:
startActivity(new Intent(this, SettingsActivity.class));
break;
case R.id.nav_about:
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);
@ -192,12 +187,10 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
drawer.addDrawerListener(toggle);
toggle.syncState();
drawerNavView.setNavigationItemSelectedListener(this);
// Initialize Progress Bar
progressBarMain = findViewById(R.id.mainProgressBar);
// Configure data set in case it's restored from screen rotation or something
if (savedInstanceState != null){
//Log.i("LPR", "NOT EMPTY INSTANCE-"+getIntent().getAction());
mDataset = savedInstanceState.getParcelableArrayList("DATASET_LIST");
// Restore USB device information
usbDevice = savedInstanceState.getParcelable("USB_DEVICE");
@ -239,6 +232,29 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
recyclerView.setLayoutManager(layoutManager);
mAdapter = new NspItemsAdapter(mDataset);
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() {
@Override
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.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
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();
if (requestCode != ADD_NSP_INTENT_CODE || data == null)
return;
Uri uri = data.getData();
if (uri == null)
return;
if (uri.getScheme() == null || ! uri.getScheme().equals("content"))
if (uri == null || uri.getScheme() == null || ! uri.getScheme().equals("content"))
return;
String fileName = LoperHelpers.getFileNameFromUri(uri, this);
long fileSize = LoperHelpers.getFileSizeFromUri(uri, this);
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;
}
if (! fileName.endsWith(".nsp")){
NsNotificationPopUp.getAlertWindow(this, getResources().getString(R.string.popup_error), getResources().getString(R.string.popup_wrong_file));
return;
String fileExtension = fileName.replaceAll("^.*\\.", "").toLowerCase();
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;
}
boolean shouldBeAdded = true;
boolean isAlreadyAddedElement = false;
for (NSPElement element: mDataset){
if (element.getFilename().equals(fileName)) {
shouldBeAdded = false;
isAlreadyAddedElement = true;
break;
}
}
NSPElement element;
if (shouldBeAdded){
element = new NSPElement(uri, fileName, fileSize);
if (drawerNavView.getCheckedItem() != null && drawerNavView.getCheckedItem().getItemId() != R.id.nav_gl)
element.setSelected(true);
mDataset.add(element);
}
else
if (isAlreadyAddedElement)
return;
NSPElement element = new NSPElement(uri, fileName, fileSize);
if (drawerNavView.getCheckedItem() != null) // && drawerNavView.getCheckedItem().getItemId() != R.id.nav_gl
element.setSelected(true);
mDataset.add(element);
mAdapter.notifyDataSetChanged();
// Enable upload button
updateUploadBtnState();
}
/*
private void uploadFilesTemp(){
ArrayList<NSPElement> toSentArray = new ArrayList<>();
private void uploadFiles(){
ArrayList<NSPElement> NSPElementsToSend = new ArrayList<>();
for (NSPElement element: mDataset){
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();
return;
}
Intent serviceStartIntent = new Intent(this, CommunicationsService.class);
serviceStartIntent.putExtra(NsConstants.NS_RESULT_RECEIVER, nsResultReciever);
serviceStartIntent.putParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST, toSentArray);
// Do we have selected protocol?
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()){
case R.id.nav_tf_usb:
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_USB);
break;
case R.id.nav_tf_net:
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_NET);
break;
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;
}
Intent serviceStartIntent = new Intent(this, CommunicationsService.class);
serviceStartIntent.putExtra(NsConstants.NS_RESULT_RECEIVER, nsResultReciever);
serviceStartIntent.putParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST, NSPElementsToSend);
// Is it TF Net transfer?
if (drawerNavView.getCheckedItem().getItemId() == R.id.nav_tf_net){
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_NET);
SharedPreferences sp = getSharedPreferences("NSUSBloader", MODE_PRIVATE);
startService(serviceStartIntent);
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;
}
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);
blockUI(true);
return;
}
catch (NullPointerException npe){
Snackbar.make(findViewById(android.R.id.content), getString(R.string.no_protocol_selected_message), Snackbar.LENGTH_LONG).show();
}
//**************************************************************************************************************************************
// Ok, so it's something USB related
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
// If somehow we can't get system service
// Do we have manager?
if (usbManager == 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 (usbDevice == null){
@ -419,70 +399,47 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
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()){
case R.id.nav_tf_usb:
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_USB);
break;
case R.id.nav_tf_net:
serviceStartIntent.putExtra(NsConstants.SERVICE_CONTENT_PROTOCOL, NsConstants.PROTO_TF_NET);
break;
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;
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);
startService(serviceStartIntent);
blockUI(true);
}
private void blockUI(boolean shouldBeBlocked){
if (shouldBeBlocked){
private void blockUI(boolean shouldBlock){
if (shouldBlock) {
selectBtn.setEnabled(false);
recyclerView.setLayoutFrozen(true);
recyclerView.suppressLayout(true);
uploadToNsBtn.setCompoundDrawablesWithIntrinsicBounds(null, getResources().getDrawable(R.drawable.ic_cancel), null, null);
uploadToNsBtn.setText(R.string.interrupt_btn);
uploadToNsBtn.setOnClickListener(e -> {
CommunicationsService.cancel();
});
uploadToNsBtn.setOnClickListener(e -> stopService(new Intent(this, CommunicationsService.class)));
progressBarMain.setVisibility(ProgressBar.VISIBLE);
progressBarMain.setIndeterminate(true);//TODO
uploadToNsBtn.setEnabled(true);
return;
}
else {
selectBtn.setEnabled(true);
recyclerView.setLayoutFrozen(false);
uploadToNsBtn.setCompoundDrawablesWithIntrinsicBounds(null, getResources().getDrawable(R.drawable.ic_upload_btn), null, null);
uploadToNsBtn.setText(R.string.upload_btn);
uploadToNsBtn.setOnClickListener(e -> {
//this.uploadFiles();
this.uploadFiles();
});
progressBarMain.setVisibility(ProgressBar.INVISIBLE);
}
selectBtn.setEnabled(true);
recyclerView.suppressLayout(false);
uploadToNsBtn.setCompoundDrawablesWithIntrinsicBounds(null, getResources().getDrawable(R.drawable.ic_upload_btn), null, null);
uploadToNsBtn.setText(R.string.upload_btn);
uploadToNsBtn.setOnClickListener(e -> this.uploadFiles() );
progressBarMain.setVisibility(ProgressBar.INVISIBLE);
this.updateUploadBtnState();
}
// Handle service updates
/**
* Handle service updates
* */
@Override
public void onRecieveResults(int code, Bundle bundle) {
public void onReceiveResults(int code, Bundle bundle) {
if (code == NsConstants.NS_RESULT_PROGRESS_INDETERMINATE)
progressBarMain.setIndeterminate(true);
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"));
}
}
// Deal with global broadcast intents
/**
* Deal with global broadcast intents
* */
private class InnerBroadcastReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
@ -516,20 +475,21 @@ public class MainActivity extends AppCompatActivity implements NsResultReciever.
case UsbManager.ACTION_USB_DEVICE_DETACHED:
usbDevice = null;
isUsbDeviceAccessible = false;
CommunicationsService.cancel();
stopService(new Intent(context, CommunicationsService.class));
break;
case NsConstants.SERVICE_TRANSFER_TASK_FINISHED_INTENT:
ArrayList<NSPElement> nspElements = intent.getParcelableArrayListExtra(NsConstants.SERVICE_CONTENT_NSP_LIST);
if (nspElements == null)
break;
for (int i=0; i < mDataset.size(); i++){
for (NSPElement recievedNSPe : nspElements)
if (recievedNSPe.getFilename().equals(mDataset.get(i).getFilename()))
mDataset.get(i).setStatus(recievedNSPe.getStatus());
for (NSPElement receivedNSPe : nspElements)
if (receivedNSPe.getFilename().equals(mDataset.get(i).getFilename()))
mDataset.get(i).setStatus(receivedNSPe.getStatus());
}
mAdapter.notifyDataSetChanged();
blockUI(false);
break;
}
//Log.i("LPR", "INTERNAL BR REC: " + intent.getAction()+" "+isUsbDeviceAccessible);
}
}
}

View File

@ -16,9 +16,6 @@ import androidx.core.app.NotificationManagerCompat;
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
public synchronized void onReceive(Context context, Intent intent) {
if (intent.getAction() == null)
@ -44,7 +41,7 @@ public class NsBroadcastReceiver extends BroadcastReceiver {
}
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)
.setContentTitle(context.getString(R.string.ns_connected_info))
//.setAutoCancel(true)
@ -52,11 +49,11 @@ public class NsBroadcastReceiver extends BroadcastReceiver {
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.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) {
CharSequence notificationChanName = context.getString(R.string.notification_channel_name);
String notificationChanDesc = context.getString(R.string.notification_channel_description);
CharSequence notificationChanName = context.getString(R.string.notification_chan_name_usb);
String notificationChanDesc = context.getString(R.string.notification_chan_desc_usb);
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_NS_CONNECTED_CHAN_ID,
NsConstants.NOTIFICATION_NS_CONNECTED_CHAN_ID,
notificationChanName,
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(notificationChanDesc);
@ -64,14 +61,14 @@ public class NsBroadcastReceiver extends BroadcastReceiver {
// or other notification behaviors after this
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
notificationManager.notify(NOTIFICATION_NS_CONNECTED_ID, notification.build());
notificationManager.notify(NsConstants.NOTIFICATION_NS_CONNECTED_ID, notification.build());
}
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){
NotificationManagerCompat.from(context).cancel(NOTIFICATION_NS_CONNECTED_ID);
NotificationManagerCompat.from(context).cancel(NsConstants.NOTIFICATION_NS_CONNECTED_ID);
}
}

View File

@ -10,6 +10,9 @@ public class NsConstants {
public static final String SERVICE_CONTENT_NSP_LIST = "NSP_LIST";
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_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
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;
@ -17,4 +20,10 @@ public class NsConstants {
public static final int PROTO_TF_USB = 10;
public static final int PROTO_TF_NET = 20;
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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}
};
}

View File

@ -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.Handler;
@ -7,7 +7,7 @@ import android.os.ResultReceiver;
public class NsResultReciever extends ResultReceiver {
public interface Receiver{
void onRecieveResults(int code, Bundle bundle);
void onReceiveResults(int code, Bundle bundle);
}
private Receiver mReceiver;
@ -23,6 +23,6 @@ public class NsResultReciever extends ResultReceiver {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (mReceiver != null)
mReceiver.onRecieveResults(resultCode, resultData);
mReceiver.onReceiveResults(resultCode, resultData);
}
}

View File

@ -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.ByteOrder;

View File

@ -1,4 +1,4 @@
package com.blogspot.developersu.ns_usbloader.PFS;
package com.blogspot.developersu.ns_usbloader.pfs;
import java.io.*;
import java.nio.ByteBuffer;

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -1,4 +1,4 @@
package com.blogspot.developersu.ns_usbloader.View;
package com.blogspot.developersu.ns_usbloader.view;
import android.net.Uri;
import android.os.Parcel;

View File

@ -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.View;

View 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>

View 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>

View File

@ -33,6 +33,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="5dp"
android:text="@string/about_line_1"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
@ -40,6 +41,7 @@
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:text="@string/about_line_2"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
@ -47,6 +49,7 @@
android:id="@+id/textView3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:text="@string/about_line_3"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
@ -54,6 +57,7 @@
android:id="@+id/textView4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:text="@string/about_line_4"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
@ -61,6 +65,7 @@
android:id="@+id/textView5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:text="@string/about_line_5"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
@ -68,6 +73,7 @@
android:id="@+id/textView6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:text="@string/about_line_6"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
</LinearLayout>

View 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>

View File

@ -31,6 +31,10 @@
<item android:title="@string/other">
<menu>
<item
android:id="@+id/nav_settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings" />
<item
android:id="@+id/nav_about"
android:icon="@drawable/ic_info"

View File

@ -1,14 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NS-USBloader</string>
<string name="select_file_btn">Выбрать .NSP</string>
<string name="popup_wrong_file">Неприавильный тип файла</string>
<string name="select_file_btn">Выбрать файлы</string>
<string name="popup_non_supported_format">Тип файла не поддерживается</string>
<string name="popup_error">Ошибка</string>
<string name="upload_btn">Отправить в NS</string>
<string name="install_file_explorer">Не установлено ни одного файлового менеджера.\n Пожалуйста установите один. Например Total Commander.</string>
<string name="ns_connected_info">NS подключён</string>
<string name="notification_channel_name">Устройство подключено</string>
<string name="notification_channel_description">Уведомление повяляется когда пользователь подключает NS к устройству</string>
<string name="notification_chan_name_usb">Устройство подключено</string>
<string name="notification_chan_desc_usb">Уведомление повяляется когда пользователь подключает NS к устройству</string>
<string name="ns_not_found_in_connected">NS не найден среди подключенных устройств.</string>
<string name="transfer_protocol">Протокол</string>
<string name="transfer_transport">Транспортный уровень</string>
@ -19,16 +18,25 @@
<string name="navigation_drawer_close">Закрыть панель навигации</string>
<string name="about_app">О приложении</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="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_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_6">Если вам нравится эта программа, можете поставить звёздочку на GitHub. Если вы хотите поддержать приложение, пожалуйста обратитесь к домашней странице проекта за информацией. Пожалуйста учтите, что это некоммерческое приложение. Ваши вложения рассматриваются как подарок, и, вероятнее всего, они будут потрачены на покупку шоколадок :)</string>
<string name="popup_not_supported">Не поддерживается</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_incorrect_file">Неправильный файл</string>
<string name="status_uploaded">(Передан)</string>
<string name="status_failed_to_upload">(Передача провалилась)</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>

View File

@ -1,13 +1,13 @@
<resources>
<string name="app_name">NS-USBloader</string>
<string name="select_file_btn">Select .NSP</string>
<string name="popup_wrong_file">Incorrect file type</string>
<string name="app_name" translatable="false">NS-USBloader</string>
<string name="select_file_btn">Select files</string>
<string name="popup_non_supported_format">File type not supported</string>
<string name="popup_error">Error</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="ns_connected_info">NS Connected</string>
<string name="notification_channel_name">Device connected</string>
<string name="notification_channel_description">Notification appears when user connects NS to device</string>
<string name="notification_chan_name_usb">Device connected</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="transfer_protocol">Protocol</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_close">Close navigation drawer</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="about_app">About this app</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="title_activity_about">About application</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_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_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="popup_not_supported">Not supported</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_incorrect_file">Incorrect file</string>
<string name="status_failed_to_upload">(Upload failure)</string>
<string name="status_wrong_file">(Bad NSP)</string>
<string name="status_uploaded">(Uploaded)</string>
</resources>
<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>

View File

@ -7,7 +7,7 @@ buildscript {
}
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
// in the individual module build.gradle files

View File

@ -1,6 +1,6 @@
#Mon Jun 17 23:55:11 MSK 2019
#Mon Mar 23 02:19:51 MSK 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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