diff --git a/.idea/misc.xml b/.idea/misc.xml index 5b2dcd9..da7759f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ - + diff --git a/README.md b/README.md index 53d2489..b1da1cd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ -# MISPBump +# What does this app? +1. Exchange public keys to make following communication private (via QR code) + + Diffie Hellman key exchange +2. Exchange information needed to sync two MISP instances +3. Upload information to MISP instance. +# Problems ++ Androidx migration - bug in material design dependency (just for editor) ++ Loading of self signed certificates (currently not supported) + +# TODOs ++ custom password for syncUser ++ upload ack screen ++ translation + +# MISPBump ![Alt text](./poster/mispbump.png) diff --git a/app/build.gradle b/app/build.gradle index 9dab2d3..43f699e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { targetSdkVersion 28 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } buildTypes { @@ -22,35 +22,30 @@ android { } dependencies { + // android + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.appcompat:appcompat:1.1.0-beta01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' // retrofit - implementation 'com.squareup.retrofit2:retrofit:2.5.0' + implementation 'com.squareup.retrofit2:retrofit:2.6.0' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' - implementation 'com.squareup.okhttp3:logging-interceptor:3.8.0' + implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' - // android - implementation 'com.android.support:support-v4:28.0.0' - implementation 'com.android.support:design:28.0.0' - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - implementation 'com.android.support:cardview-v7:28.0.0' - implementation 'com.android.support:recyclerview-v7:28.0.0' - - // barcode scanning + // barcode reading implementation 'com.google.android.gms:play-services-vision:17.0.2' // barcode generation implementation 'com.journeyapps:zxing-android-embedded:3.2.0@aar' - implementation 'com.google.zxing:core:3.3.0' - - // override due to play-services-vision version conflicts - implementation 'com.android.support:support-media-compat:28.0.0' - implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.google.zxing:core:3.4.0' implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } repositories { diff --git a/app/src/androidTest/java/lu/circl/mispbump/ExampleInstrumentedTest.java b/app/src/androidTest/java/lu/circl/mispbump/ExampleInstrumentedTest.java index 75c255d..2eda88f 100644 --- a/app/src/androidTest/java/lu/circl/mispbump/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/lu/circl/mispbump/ExampleInstrumentedTest.java @@ -1,8 +1,8 @@ package lu.circl.mispbump; import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 97c6395..b0c9e79 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,10 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> + + @@ -30,14 +34,14 @@ android:label="@string/login" /> - + android:parentActivityName=".activities.HomeActivity" + android:theme="@style/AppTheme.Translucent" /> \ No newline at end of file diff --git a/app/src/main/java/lu/circl/mispbump/activities/HomeActivity.java b/app/src/main/java/lu/circl/mispbump/activities/HomeActivity.java index 9e28d72..b35b766 100644 --- a/app/src/main/java/lu/circl/mispbump/activities/HomeActivity.java +++ b/app/src/main/java/lu/circl/mispbump/activities/HomeActivity.java @@ -2,27 +2,30 @@ package lu.circl.mispbump.activities; import android.content.Intent; import android.os.Bundle; -import android.support.design.widget.CoordinatorLayout; -import android.support.design.widget.FloatingActionButton; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; -import android.util.Log; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; +import com.google.gson.Gson; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.appcompat.widget.Toolbar; + import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; -import android.widget.Toast; import java.util.List; import lu.circl.mispbump.R; import lu.circl.mispbump.adapters.SyncAdapter; +import lu.circl.mispbump.auxiliary.DialogManager; import lu.circl.mispbump.auxiliary.PreferenceManager; +import lu.circl.mispbump.interfaces.IOnItemClickListener; import lu.circl.mispbump.models.UploadInformation; -import lu.circl.mispbump.restful_client.MispRestClient; public class HomeActivity extends AppCompatActivity { @@ -54,7 +57,8 @@ public class HomeActivity extends AppCompatActivity { @Override protected void onResume() { super.onResume(); - populateRecyclerView(); + initializeRecyclerView(); + refreshSyncInformation(); } @Override @@ -66,12 +70,12 @@ public class HomeActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.menu_settings) { + startActivity(new Intent(HomeActivity.this, PreferenceActivity.class)); return true; } if (item.getItemId() == R.id.menu_profile) { - Intent profile = new Intent(HomeActivity.this, ProfileActivity.class); - startActivity(profile); + startActivity(new Intent(HomeActivity.this, ProfileActivity.class)); return true; } @@ -98,21 +102,59 @@ public class HomeActivity extends AppCompatActivity { sync_fab.setOnClickListener(onFabClicked); } - private void populateRecyclerView() { + private void refreshSyncInformation () { List uploadInformationList = preferenceManager.getUploadInformation(); - TextView empty = findViewById(R.id.emtpy); + // no sync information available if (uploadInformationList == null) { empty.setVisibility(View.VISIBLE); recyclerView.setVisibility(View.GONE); return; } + // sync information available empty.setVisibility(View.GONE); recyclerView.setVisibility(View.VISIBLE); - SyncAdapter syncAdapter = new SyncAdapter(uploadInformationList, HomeActivity.this); + SyncAdapter adapter = (SyncAdapter) recyclerView.getAdapter(); + assert adapter != null; + adapter.setUploadInformationList(uploadInformationList); + } + + private void initializeRecyclerView() { + SyncAdapter syncAdapter = new SyncAdapter(HomeActivity.this); + syncAdapter.setOnDeleteClickListener(new IOnItemClickListener() { + @Override + public void onItemClick(final UploadInformation clickedObject) { + DialogManager.deleteSyncInformationDialog(HomeActivity.this, new DialogManager.IDialogFeedback() { + @Override + public void positive() { + boolean status = preferenceManager.removeUploadInformation(clickedObject.getId()); + + if (status) { + Snackbar.make(layout, "Successfully deleted sync information", Snackbar.LENGTH_LONG).show(); + refreshSyncInformation(); + } else { + Snackbar.make(layout, "Failed to delete sync information", Snackbar.LENGTH_LONG).show(); + } + } + + @Override + public void negative() { } + }); + } + }); + + syncAdapter.setOnRetryClickListener(new IOnItemClickListener() { + @Override + public void onItemClick(UploadInformation clickedObject) { + Intent upload = new Intent(HomeActivity.this, UploadActivity.class); + upload.putExtra(UploadActivity.EXTRA_UPLOAD_INFO, new Gson().toJson(clickedObject)); + startActivity(upload); + } + }); + recyclerView.setAdapter(syncAdapter); } } diff --git a/app/src/main/java/lu/circl/mispbump/activities/LoginActivity.java b/app/src/main/java/lu/circl/mispbump/activities/LoginActivity.java index 8c3bdcd..f2ddce9 100644 --- a/app/src/main/java/lu/circl/mispbump/activities/LoginActivity.java +++ b/app/src/main/java/lu/circl/mispbump/activities/LoginActivity.java @@ -1,17 +1,23 @@ package lu.circl.mispbump.activities; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; -import android.support.constraint.ConstraintLayout; -import android.support.design.widget.Snackbar; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; +import androidx.constraintlayout.widget.ConstraintLayout; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputLayout; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; import android.widget.Button; import android.widget.ProgressBar; @@ -40,25 +46,7 @@ public class LoginActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); - - // populate Toolbar (Actionbar) - Toolbar myToolbar = findViewById(R.id.appbar); - setSupportActionBar(myToolbar); - - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setDisplayHomeAsUpEnabled(false); - } - - constraintLayout = findViewById(R.id.rootLayout); - progressBar = findViewById(R.id.login_progressbar); - serverUrl = findViewById(R.id.login_server_url); - serverAutomationKey = findViewById(R.id.login_automation_key); - Button downloadInfoButton = findViewById(R.id.login_download_button); - - downloadInfoButton.setOnClickListener(onClickDownload); - - preferenceManager = PreferenceManager.getInstance(this); + initializeViews(); } @Override @@ -69,16 +57,35 @@ public class LoginActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_login_help: - DialogManager.loginHelpDialog(LoginActivity.this); - return true; - - default: - // invoke superclass to handle unrecognized item (eg. homeAsUp) - return super.onOptionsItemSelected(item); - + if (item.getItemId() == R.id.menu_login_help) { + DialogManager.loginHelpDialog(LoginActivity.this); + return true; } + + // invoke superclass to handle unrecognized item (eg. homeAsUp) + return super.onOptionsItemSelected(item); + } + + private void initializeViews() { + // populate Toolbar (Actionbar) + Toolbar myToolbar = findViewById(R.id.appbar); + setSupportActionBar(myToolbar); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(false); + } + + constraintLayout = findViewById(R.id.rootLayout); + + serverUrl = findViewById(R.id.login_server_url); + serverAutomationKey = findViewById(R.id.login_automation_key); + Button downloadInfoButton = findViewById(R.id.login_download_button); + downloadInfoButton.setOnClickListener(onClickDownload); + + progressBar = findViewById(R.id.login_progressbar); + + preferenceManager = PreferenceManager.getInstance(this); } /** @@ -112,6 +119,7 @@ public class LoginActivity extends AppCompatActivity { // save authkey preferenceManager.setAutomationKey(authkey); + // save url preferenceManager.setServerUrl(url); @@ -122,20 +130,29 @@ public class LoginActivity extends AppCompatActivity { progressBar.setVisibility(View.VISIBLE); // get my user information and the organisation associated with my user - mispRestClient.getMyUser(new MispRestClient.UserCallback() { + mispRestClient.isAvailable(new MispRestClient.AvailableCallback() { @Override - public void success(final User user) { - - preferenceManager.setUserInfo(user); - - mispRestClient.getOrganisation(user.org_id, new MispRestClient.OrganisationCallback() { + public void available() { + mispRestClient.getMyUser(new MispRestClient.UserCallback() { @Override - public void success(Organisation organisation) { - preferenceManager.setUserOrgInfo(organisation); - progressBar.setVisibility(View.GONE); - Intent home = new Intent(getApplicationContext(), HomeActivity.class); - startActivity(home); - finish(); + public void success(final User user) { + preferenceManager.setUserInfo(user); + mispRestClient.getOrganisation(user.org_id, new MispRestClient.OrganisationCallback() { + @Override + public void success(Organisation organisation) { + preferenceManager.setUserOrgInfo(organisation); + progressBar.setVisibility(View.GONE); + Intent home = new Intent(getApplicationContext(), HomeActivity.class); + startActivity(home); + finish(); + } + + @Override + public void failure(String error) { + progressBar.setVisibility(View.GONE); + Snackbar.make(constraintLayout, error, Snackbar.LENGTH_LONG).show(); + } + }); } @Override @@ -147,9 +164,10 @@ public class LoginActivity extends AppCompatActivity { } @Override - public void failure(String error) { + public void unavailable(String error) { progressBar.setVisibility(View.GONE); - Snackbar.make(constraintLayout, error, Snackbar.LENGTH_LONG).show(); + Snackbar sb = Snackbar.make(constraintLayout, error, Snackbar.LENGTH_LONG); + sb.show(); } }); } @@ -162,7 +180,13 @@ public class LoginActivity extends AppCompatActivity { * @return true or false */ private boolean isValidUrl(String url) { - return url.startsWith("https://") || url.startsWith("http://"); + Uri uri = Uri.parse(url); + + if (uri == null) { + return false; + } + + return uri.getScheme() != null; } /** diff --git a/app/src/main/java/lu/circl/mispbump/activities/PreferenceActivity.java b/app/src/main/java/lu/circl/mispbump/activities/PreferenceActivity.java new file mode 100644 index 0000000..9785f9e --- /dev/null +++ b/app/src/main/java/lu/circl/mispbump/activities/PreferenceActivity.java @@ -0,0 +1,35 @@ +package lu.circl.mispbump.activities; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +import lu.circl.mispbump.R; +import lu.circl.mispbump.auxiliary.PreferenceManager; + +public class PreferenceActivity extends AppCompatActivity { + + private PreferenceManager preferenceManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_preference); + + preferenceManager = PreferenceManager.getInstance(PreferenceActivity.this); + + initializeViews(); + } + + private void initializeViews() { + Button deleteSyncs = findViewById(R.id.deleteSyncs); + deleteSyncs.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + preferenceManager.clearUploadInformation(); + } + }); + } +} diff --git a/app/src/main/java/lu/circl/mispbump/activities/ProfileActivity.java b/app/src/main/java/lu/circl/mispbump/activities/ProfileActivity.java index 793f596..8279469 100644 --- a/app/src/main/java/lu/circl/mispbump/activities/ProfileActivity.java +++ b/app/src/main/java/lu/circl/mispbump/activities/ProfileActivity.java @@ -6,20 +6,17 @@ import android.graphics.Shader; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.design.widget.CoordinatorLayout; -import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; -import android.support.v4.widget.ContentLoadingProgressBar; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.animation.RotateAnimation; import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; import java.util.Random; @@ -85,19 +82,15 @@ public class ProfileActivity extends AppCompatActivity { MaterialPreferenceText uuid = findViewById(R.id.uuid); uuid.setSubText(organisation.uuid); - uuid.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - TextView tv = v.findViewById(R.id.material_preference_subtitle); - Snackbar.make(rootLayout, "clicked: " + tv.getText().toString(), Snackbar.LENGTH_LONG).show(); - } - }); - MaterialPreferenceText nationality = findViewById(R.id.nationality); nationality.setSubText(organisation.nationality); MaterialPreferenceText sector = findViewById(R.id.sector); - sector.setSubText(organisation.sector); + if (organisation.sector == null) { + sector.setVisibility(View.GONE); + } else { + sector.setSubText(organisation.sector); + } MaterialPreferenceText description = findViewById(R.id.description); description.setSubText(organisation.description); diff --git a/app/src/main/java/lu/circl/mispbump/activities/StartUpActivity.java b/app/src/main/java/lu/circl/mispbump/activities/StartUpActivity.java index 3569a7f..e0f5b18 100644 --- a/app/src/main/java/lu/circl/mispbump/activities/StartUpActivity.java +++ b/app/src/main/java/lu/circl/mispbump/activities/StartUpActivity.java @@ -2,7 +2,7 @@ package lu.circl.mispbump.activities; import android.content.Intent; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import lu.circl.mispbump.auxiliary.PreferenceManager; import lu.circl.mispbump.restful_client.User; diff --git a/app/src/main/java/lu/circl/mispbump/activities/SyncActivity.java b/app/src/main/java/lu/circl/mispbump/activities/SyncActivity.java index 160a368..2d91574 100644 --- a/app/src/main/java/lu/circl/mispbump/activities/SyncActivity.java +++ b/app/src/main/java/lu/circl/mispbump/activities/SyncActivity.java @@ -1,41 +1,38 @@ package lu.circl.mispbump.activities; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; -import android.support.design.bottomappbar.BottomAppBar; -import android.support.design.widget.CoordinatorLayout; -import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.util.Log; import android.view.View; +import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.snackbar.Snackbar; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; -import java.util.List; import lu.circl.mispbump.R; -import lu.circl.mispbump.auxiliary.DialogManager; import lu.circl.mispbump.auxiliary.PreferenceManager; import lu.circl.mispbump.auxiliary.QrCodeGenerator; import lu.circl.mispbump.auxiliary.RandomString; import lu.circl.mispbump.cam.CameraFragment; +import lu.circl.mispbump.custom_views.ExtendedBottomSheetBehavior; import lu.circl.mispbump.fragments.SyncOptionsFragment; import lu.circl.mispbump.models.SyncInformation; import lu.circl.mispbump.models.UploadInformation; -import lu.circl.mispbump.restful_client.MispRestClient; -import lu.circl.mispbump.restful_client.MispServer; -import lu.circl.mispbump.restful_client.Organisation; -import lu.circl.mispbump.restful_client.Server; -import lu.circl.mispbump.restful_client.User; import lu.circl.mispbump.security.DiffieHellman; /** @@ -45,365 +42,382 @@ import lu.circl.mispbump.security.DiffieHellman; */ public class SyncActivity extends AppCompatActivity { - private static final String TAG = "SyncActivity"; - + // layout private CoordinatorLayout layout; - private ImageView qrCodeView; - private FloatingActionButton continueButton; + private ImageView qrCodeView, bottomSheetIcon; + private TextView bottomSheetText; + private ImageButton prevButton, nextButton; + private ExtendedBottomSheetBehavior bottomSheetBehavior; - private CameraFragment cameraFragment; + // dependencies + private PreferenceManager preferenceManager; private DiffieHellman diffieHellman; - private MispRestClient restClient; private UploadInformation uploadInformation; - private SyncState currentSyncState = SyncState.publicKeyExchange; + // fragments + private CameraFragment cameraFragment; + private SyncOptionsFragment syncOptionsFragment; - private PreferenceManager preferenceManager; + // qr codes + private QrCodeGenerator qrCodeGenerator; + private Bitmap publicKeyQr, syncInfoQr; + + private SyncState currentSyncState = SyncState.settings; private enum SyncState { - publicKeyExchange, - dataExchange + settings(0), + publicKeyExchange(1), + dataExchange(2); + + + private final int value; + + SyncState(final int value) { + this.value = value; + } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_sync_2); - -// BottomAppBar myToolbar = findViewById(R.id.bottomNavigation); -// setSupportActionBar(myToolbar); -// -// ActionBar ab = getSupportActionBar(); -// if (ab != null) { -// ab.setDisplayHomeAsUpEnabled(true); -// } + setContentView(R.layout.activity_sync); + initializeViews(); + } + private void initializeViews() { + // Root Layout layout = findViewById(R.id.rootLayout); + // prev button + prevButton = findViewById(R.id.prevButton); + prevButton.setOnClickListener(onPrevClicked); + + // next button + nextButton = findViewById(R.id.nextButton); + nextButton.setOnClickListener(onNextClicked); + + // QR Code View qrCodeView = findViewById(R.id.qrcode); -// continueButton = findViewById(R.id.fab); -// continueButton.setOnClickListener(onContinueClicked); -// continueButton.hide(); + qrCodeGenerator = new QrCodeGenerator(SyncActivity.this); + + bottomSheetIcon = findViewById(R.id.bottomSheetIcon); + bottomSheetText = findViewById(R.id.bottomSheetText); diffieHellman = DiffieHellman.getInstance(); - restClient = new MispRestClient(this); - preferenceManager = PreferenceManager.getInstance(this); - enableSyncOptionsFragment(); + View bottomSheet = findViewById(R.id.bottomSheet); + bottomSheetBehavior = (ExtendedBottomSheetBehavior) BottomSheetBehavior.from(bottomSheet); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.setSwipeable(false); + bottomSheetBehavior.setHideable(false); + + publicKeyQr = generatePublicKeyQr(); + + switchState(SyncState.settings); } /** - * This callback is called at the end of each sync step. + * Called when "next button" is pressed */ - private View.OnClickListener onContinueClicked = new View.OnClickListener() { + private View.OnClickListener onNextClicked = new View.OnClickListener() { @Override public void onClick(View v) { - - cameraFragment.setReadQrEnabled(false); - switch (currentSyncState) { - case publicKeyExchange: - DialogManager.confirmProceedDialog(SyncActivity.this, - new DialogManager.IDialogFeedback() { - @Override - public void positive() { - currentSyncState = SyncState.dataExchange; - continueButton.hide(); - cameraFragment.setReadQrEnabled(true); - showInformationQr(); - } + case settings: + uploadInformation.setCached(syncOptionsFragment.cache.isChecked()); + uploadInformation.setPush(syncOptionsFragment.push.isChecked()); + uploadInformation.setPull(syncOptionsFragment.pull.isChecked()); + uploadInformation.setAllowSelfSigned(syncOptionsFragment.allowSelfSigned.isChecked()); - @Override - public void negative() { - } - }); + switchState(SyncState.publicKeyExchange); + break; + + case publicKeyExchange: + switchState(SyncState.dataExchange); break; case dataExchange: - DialogManager.confirmProceedDialog(SyncActivity.this, new DialogManager.IDialogFeedback() { - @Override - public void positive() { - startUpload(); - } - - @Override - public void negative() { - } - }); + Intent upload = new Intent(SyncActivity.this, UploadActivity.class); + upload.putExtra(UploadActivity.EXTRA_UPLOAD_INFO, new Gson().toJson(uploadInformation)); + startActivity(upload); + overridePendingTransition(R.anim.slide_in_right, android.R.anim.slide_out_right); + finish(); break; } } }; /** - * Callback for the camera fragment. - * Delivers the content of a scanned QR code. + * Called when "prev button" is clicked + */ + private View.OnClickListener onPrevClicked = new View.OnClickListener() { + @Override + public void onClick(View v) { + switch (currentSyncState) { + case settings: + finish(); + break; + + case publicKeyExchange: + switchState(SyncState.settings); + break; + + case dataExchange: + switchState(SyncState.publicKeyExchange); + break; + } + } + }; + + /** + * Called when the camera fragment detects a qr code */ private CameraFragment.QrScanCallback onQrCodeScanned = new CameraFragment.QrScanCallback() { @Override public void qrScanResult(String qrData) { cameraFragment.setReadQrEnabled(false); - switch (currentSyncState) { case publicKeyExchange: try { final PublicKey pk = DiffieHellman.publicKeyFromString(qrData); diffieHellman.setForeignPublicKey(pk); + syncInfoQr = generateSyncInfoQr(); + runOnUiThread(new Runnable() { @Override public void run() { - - continueButton.show(); - - Snackbar sb = Snackbar.make(continueButton, "Public key received", Snackbar.LENGTH_LONG); - sb.setAction("Details", new View.OnClickListener() { - @Override - public void onClick(View v) { - DialogManager.publicKeyDialog(pk, SyncActivity.this, null); - } - }); - sb.show(); + nextButton.setVisibility(View.VISIBLE); + cameraFragment.disablePreview(); + qrReceivedFeedback(); } }); } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { Snackbar.make(layout, "Invalid key", Snackbar.LENGTH_SHORT).show(); + cameraFragment.setReadQrEnabled(true); } break; case dataExchange: cameraFragment.setReadQrEnabled(false); - final SyncInformation remoteSyncInfo = new Gson().fromJson(diffieHellman.decrypt(qrData), SyncInformation.class); + try { + final SyncInformation remoteSyncInfo = new Gson().fromJson(diffieHellman.decrypt(qrData), SyncInformation.class); + uploadInformation.setRemote(remoteSyncInfo); - DialogManager.syncInformationDialog(remoteSyncInfo, - SyncActivity.this, - new DialogManager.IDialogFeedback() { - @Override - public void positive() { - uploadInformation.setRemote(remoteSyncInfo); - continueButton.show(); - } - - @Override - public void negative() { - cameraFragment.setReadQrEnabled(true); - } - }); + runOnUiThread(new Runnable() { + @Override + public void run() { + cameraFragment.disablePreview(); + nextButton.setVisibility(View.VISIBLE); + qrReceivedFeedback(); + } + }); + } catch (JsonSyntaxException e) { + Snackbar.make(layout, "Sync information unreadable", Snackbar.LENGTH_SHORT).show(); + cameraFragment.setReadQrEnabled(true); + } break; } } }; - private void startUpload() { - // check if misp instance is available - restClient.isAvailable(new MispRestClient.AvailableCallback() { - @Override - public void unavailable() { - Snackbar sb = Snackbar.make(layout, "MISP instance not available", Snackbar.LENGTH_LONG); - sb.setAction("Retry", new View.OnClickListener() { - @Override - public void onClick(View v) { - startUpload(); // TODO check if this works - } - }); - sb.show(); - } - @Override - public void available() { + private void switchUiState(SyncState state) { - restClient.addOrganisation(uploadInformation.getRemote().organisation, new MispRestClient.OrganisationCallback() { - @Override - public void success(final Organisation organisation) { - // create syncUser object from syncInfo - User syncUser = new User(); - syncUser.org_id = organisation.id; - syncUser.role_id = User.ROLE_SYNC_USER; + bottomSheetIcon.setVisibility(View.INVISIBLE); + bottomSheetBehavior.setSwipeable(false); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - // syncuser_ORG@REMOTE_ORG_EMAIL_DOMAIN - String emailSaveOrgName = organisation.name.replace(" ", "").toLowerCase(); - syncUser.email = "syncuser_" + emailSaveOrgName + "@misp.de"; + switch (state) { + case settings: + prevButton.setImageDrawable(getDrawable(R.drawable.ic_close)); + prevButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + hideQrCode(); + break; + case publicKeyExchange: + prevButton.setImageDrawable(getDrawable(R.drawable.ic_arrow_back)); + prevButton.setVisibility(View.VISIBLE); - syncUser.password = uploadInformation.getRemote().syncUserPassword; - syncUser.authkey = uploadInformation.getRemote().syncUserAuthkey; - syncUser.termsaccepted = true; + nextButton.setImageDrawable(getDrawable(R.drawable.ic_arrow_forward)); + nextButton.setVisibility(View.GONE); + showQrCode(publicKeyQr); + break; + case dataExchange: + prevButton.setImageDrawable(getDrawable(R.drawable.ic_arrow_back)); + prevButton.setVisibility(View.VISIBLE); - // add user to local organisation - restClient.addUser(syncUser, new MispRestClient.UserCallback() { - @Override - public void success(User user) { - Server server = new Server(); - server.name = organisation.name + "'s Sync Server"; - server.url = uploadInformation.getRemote().baseUrl; - server.remote_org_id = organisation.id; - server.authkey = uploadInformation.getLocal().syncUserAuthkey; - server.self_signed = true; + nextButton.setImageDrawable(getDrawable(R.drawable.ic_cloud_upload)); + nextButton.setVisibility(View.GONE); - restClient.addServer(server, new MispRestClient.ServerCallback() { - @Override - public void success(List servers) { - } - - @Override - public void success(MispServer server) { - } - - @Override - public void success(Server server) { - uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.COMPLETE); - preferenceManager.setUploadInformation(uploadInformation); - finish(); - } - - @Override - public void failure(String error) { - uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.FAILURE); - preferenceManager.setUploadInformation(uploadInformation); - Snackbar.make(layout, error, Snackbar.LENGTH_LONG).show(); - Log.e(TAG, error); - } - }); - } - - @Override - public void failure(String error) { - uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.FAILURE); - preferenceManager.setUploadInformation(uploadInformation); - Snackbar.make(layout, error, Snackbar.LENGTH_LONG).show(); - Log.e(TAG, error); - } - }); - } - - @Override - public void failure(String error) { - uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.FAILURE); - preferenceManager.setUploadInformation(uploadInformation); - Snackbar.make(layout, error, Snackbar.LENGTH_LONG).show(); - Log.e(TAG, error); - } - }); - } - }); + cameraFragment.enablePreview(); + cameraFragment.setReadQrEnabled(true); + showQrCode(syncInfoQr); + break; + } } - /** - * Creates the camera fragment used to scan the QR codes. - * Automatically starts processing images (search QR codes). - */ - private void enableCameraFragment() { - cameraFragment = new CameraFragment(); - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction transaction = fragmentManager.beginTransaction(); - transaction.replace(R.id.sync_fragment_container, cameraFragment, cameraFragment.getClass().getSimpleName()); - transaction.commit(); - - cameraFragment.setReadQrEnabled(true); - cameraFragment.setOnQrAvailableListener(onQrCodeScanned); - } - - /** - * Creates fragment to tweak sync options. - */ - private void enableSyncOptionsFragment() { - SyncOptionsFragment syncOptionsFragment = new SyncOptionsFragment(); - - syncOptionsFragment.setOnOptionsReadyCallback(new SyncOptionsFragment.OptionsReadyCallback() { - @Override - public void ready(boolean share_events, boolean push, boolean pull, boolean caching) { - showPublicKeyQr(); - enableCameraFragment(); - } - }); + private void switchState(SyncState state) { FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); - transaction.replace(R.id.sync_fragment_container, syncOptionsFragment, syncOptionsFragment.getClass().getSimpleName()); - transaction.commit(); + + if (currentSyncState != state) { + if (state.value < currentSyncState.value) { + transaction.setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right); + } else { + transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left); + } + } + + currentSyncState = state; + + switchUiState(currentSyncState); + + switch (currentSyncState) { + case settings: + String fragTag = SyncOptionsFragment.class.getSimpleName(); + + syncOptionsFragment = (SyncOptionsFragment) fragmentManager.findFragmentByTag(fragTag); + + if (syncOptionsFragment == null) { + syncOptionsFragment = new SyncOptionsFragment(); + } + + transaction.replace(R.id.sync_fragment_container, syncOptionsFragment, fragTag); + transaction.commit(); + break; + + case publicKeyExchange: + fragTag = CameraFragment.class.getSimpleName(); + cameraFragment = (CameraFragment) fragmentManager.findFragmentByTag(fragTag); + + if (cameraFragment == null) { + cameraFragment = new CameraFragment(); + cameraFragment.setOnQrAvailableListener(onQrCodeScanned); + } + + transaction.replace(R.id.sync_fragment_container, cameraFragment, fragTag); + transaction.commit(); + break; + + case dataExchange: + fragTag = CameraFragment.class.getSimpleName(); + cameraFragment = (CameraFragment) fragmentManager.findFragmentByTag(fragTag); + + if (cameraFragment == null) { + cameraFragment = new CameraFragment(); + cameraFragment.setOnQrAvailableListener(onQrCodeScanned); + } + + transaction.replace(R.id.sync_fragment_container, cameraFragment, fragTag); + transaction.commit(); + break; + } } - /** - * Display QR code that contains the public key . - */ - private void showPublicKeyQr() { - QrCodeGenerator qrCodeGenerator = new QrCodeGenerator(this); - Bitmap bm = qrCodeGenerator.generateQrCode(DiffieHellman.publicKeyToString(diffieHellman.getPublicKey())); - qrCodeView.setImageBitmap(bm); - qrCodeView.setVisibility(View.VISIBLE); + + private Bitmap generatePublicKeyQr() { + return qrCodeGenerator.generateQrCode(DiffieHellman.publicKeyToString(diffieHellman.getPublicKey())); } - /** - * Display QR code that contains mandatory information for a sync. - */ - private void showInformationQr() { - PreferenceManager preferenceManager = PreferenceManager.getInstance(this); - + private Bitmap generateSyncInfoQr() { SyncInformation syncInformation = new SyncInformation(); - - syncInformation.organisation = preferenceManager.getUserOrganisation().syncOrganisation(); + syncInformation.organisation = preferenceManager.getUserOrganisation().toSyncOrganisation(); syncInformation.syncUserAuthkey = new RandomString(40).nextString(); syncInformation.baseUrl = preferenceManager.getServerUrl(); - syncInformation.syncUserPassword = "abcdefghijklmnop"; + syncInformation.syncUserPassword = new RandomString(16).nextString(); + + String myEmailDomain = preferenceManager.getUserInfo().email.split("@")[1]; + syncInformation.syncUserEmail = "syncuser_[ORG]@" + myEmailDomain; uploadInformation = new UploadInformation(syncInformation); // encrypt serialized content String encrypted = diffieHellman.encrypt(new Gson().toJson(syncInformation)); - // Generate QR code - QrCodeGenerator qrCodeGenerator = new QrCodeGenerator(this); - final Bitmap bm = qrCodeGenerator.generateQrCode(encrypted); + // generate QR code + return qrCodeGenerator.generateQrCode(encrypted); + } + + private void showQrCode(final Bitmap bitmap) { runOnUiThread(new Runnable() { @Override public void run() { - qrCodeView.setImageBitmap(bm); + + qrCodeView.setImageBitmap(bitmap); + qrCodeView.setAlpha(0f); qrCodeView.setVisibility(View.VISIBLE); + qrCodeView.setScaleX(0.9f); + qrCodeView.setScaleY(0.6f); + qrCodeView.animate() + .scaleX(1f) + .scaleY(1f) + .alpha(1f) + .setDuration(250) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + qrCodeView.setVisibility(View.VISIBLE); + } + }); } }); } -// /** -// * Display toast on UI thread. -// * -// * @param message message to display -// */ -// private void MakeToast(final String message) { -// this.runOnUiThread(new Runnable() { -// @Override -// public void run() { -// Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); -// } -// }); -// } + private void hideQrCode() { + + if (qrCodeView.getVisibility() == View.GONE) { + return; + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + qrCodeView.setAlpha(1f); + qrCodeView.setVisibility(View.VISIBLE); + qrCodeView.setScaleX(1f); + qrCodeView.setScaleY(1f); + qrCodeView.animate() + .scaleX(0f) + .scaleY(0f) + .alpha(0f) + .setDuration(250) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + qrCodeView.setVisibility(View.GONE); + } + }); + } + }); + } + + private void qrReceivedFeedback() { + bottomSheetIcon.setScaleX(0f); + bottomSheetIcon.setScaleY(0f); + bottomSheetIcon.setVisibility(View.VISIBLE); + bottomSheetIcon.animate() + .scaleY(1f) + .scaleX(1f) + .setDuration(250); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + bottomSheetBehavior.setSwipeable(true); + + switch (currentSyncState) { + case publicKeyExchange: + bottomSheetText.setText("Received public key from partner"); + break; + + case dataExchange: + bottomSheetText.setText("Received sync information from partner"); + break; + } + } -// private View.OnClickListener onGetServers = new View.OnClickListener() { -// @Override -// public void onClick(View v) { -// restClient.getServers(new MispRestClient.ServerCallback() { -// @Override -// public void success(List servers) { -// for (MispServer server : servers) { -// resultView.append(server.server.toString() + "\n\n"); -// resultView.append(server.organisation.toString() + "\n\n"); -// resultView.append(server.remoteOrg.toString()); -// } -// } -// -// @Override -// public void success(MispServer server) { -// -// } -// -// @Override -// public void failure(String error) { -// resultView.setText(error); -// } -// }); -// } -// }; } diff --git a/app/src/main/java/lu/circl/mispbump/activities/UploadActivity.java b/app/src/main/java/lu/circl/mispbump/activities/UploadActivity.java new file mode 100644 index 0000000..01cae03 --- /dev/null +++ b/app/src/main/java/lu/circl/mispbump/activities/UploadActivity.java @@ -0,0 +1,274 @@ +package lu.circl.mispbump.activities; + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; +import com.google.gson.Gson; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import lu.circl.mispbump.R; +import lu.circl.mispbump.auxiliary.DialogManager; +import lu.circl.mispbump.auxiliary.PreferenceManager; +import lu.circl.mispbump.custom_views.UploadAction; +import lu.circl.mispbump.models.UploadInformation; +import lu.circl.mispbump.restful_client.MispRestClient; +import lu.circl.mispbump.restful_client.MispServer; +import lu.circl.mispbump.restful_client.Organisation; +import lu.circl.mispbump.restful_client.Server; +import lu.circl.mispbump.restful_client.User; + +public class UploadActivity extends AppCompatActivity { + + public static final String EXTRA_UPLOAD_INFO = "uploadInformation"; + + private PreferenceManager preferenceManager; + private MispRestClient restClient; + private UploadInformation uploadInformation; + + private CoordinatorLayout rootLayout; + + private UploadAction availableAction, orgAction, userAction, serverAction; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_upload); + parseExtra(); + init(); + } + + private void parseExtra() { + String uploadInfoString = getIntent().getStringExtra(EXTRA_UPLOAD_INFO); + uploadInformation = new Gson().fromJson(uploadInfoString, UploadInformation.class); + assert uploadInformation != null; + } + + private void init() { + preferenceManager = PreferenceManager.getInstance(this); + restClient = new MispRestClient(this); + rootLayout = findViewById(R.id.rootLayout); + + // toolbar + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar ab = getSupportActionBar(); + assert ab != null; + ab.setDisplayShowTitleEnabled(true); + ab.setDisplayHomeAsUpEnabled(true); + ab.setHomeAsUpIndicator(R.drawable.ic_close); + + // fab + FloatingActionButton fab = findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startUpload(); + } + }); + + availableAction = findViewById(R.id.availableAction); + orgAction = findViewById(R.id.orgAction); + userAction = findViewById(R.id.userAction); + serverAction = findViewById(R.id.serverAction); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + preferenceManager.addUploadInformation(uploadInformation); + finish(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Start upload to misp instance. + */ + private void startUpload() { + availableAction.setCurrentUploadState(UploadAction.UploadState.LOADING); + restClient.isAvailable(availableCallback); + } + + private User generateSyncUser(Organisation organisation) { + User syncUser = new User(); + syncUser.org_id = organisation.id; + syncUser.role_id = User.ROLE_SYNC_USER; + + String emailSaveOrgName = organisation.name.replace(" ", "").toLowerCase(); + String syncUserEmailFormat = uploadInformation.getRemote().syncUserEmail; + syncUser.email = syncUserEmailFormat.replace("[ORG]", emailSaveOrgName); + uploadInformation.getRemote().syncUserEmail = syncUser.email; + + syncUser.password = uploadInformation.getRemote().syncUserPassword; + syncUser.authkey = uploadInformation.getRemote().syncUserAuthkey; + syncUser.termsaccepted = true; + + return syncUser; + } + + private MispRestClient.AvailableCallback availableCallback = new MispRestClient.AvailableCallback() { + @Override + public void available() { + availableAction.setCurrentUploadState(UploadAction.UploadState.DONE); + orgAction.setCurrentUploadState(UploadAction.UploadState.LOADING); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + int orgId = organisationExists(); + if (orgId != -1) { + Snackbar.make(rootLayout, "exists", Snackbar.LENGTH_INDEFINITE).show(); + uploadInformation.getRemote().organisation.id = orgId; + // TODO if exists: add User + } else { + restClient.addOrganisation(uploadInformation.getRemote().organisation, organisationCallback); + Snackbar.make(rootLayout, "does not exist", Snackbar.LENGTH_INDEFINITE).show(); + } + } catch (IOException e) { + Snackbar.make(rootLayout, "Some error", Snackbar.LENGTH_INDEFINITE).show(); + e.printStackTrace(); + } + } + }); + + t.start(); + } + + @Override + public void unavailable(String error) { + availableAction.setCurrentUploadState(UploadAction.UploadState.ERROR); + availableAction.setError(error); + uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.FAILURE); + + Snackbar sb = Snackbar.make(rootLayout, error, Snackbar.LENGTH_INDEFINITE); + sb.setAction("Retry", new View.OnClickListener() { + @Override + public void onClick(View v) { + availableAction.setError(null); + availableAction.setCurrentUploadState(UploadAction.UploadState.LOADING); + startUpload(); + } + }); + sb.show(); + } + }; + + private MispRestClient.OrganisationCallback organisationCallback = new MispRestClient.OrganisationCallback() { + @Override + public void success(Organisation organisation) { + orgAction.setCurrentUploadState(UploadAction.UploadState.DONE); + userAction.setCurrentUploadState(UploadAction.UploadState.LOADING); + + // for later reference in add user callback + uploadInformation.getRemote().organisation.id = organisation.id; + + restClient.addUser(generateSyncUser(organisation), userCallback); + } + + @Override + public void failure(String error) { + + + // IF error = org already exists: + // resClient.addUser() + + orgAction.setCurrentUploadState(UploadAction.UploadState.ERROR); + uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.FAILURE); + preferenceManager.addUploadInformation(uploadInformation); + Snackbar.make(rootLayout, error, Snackbar.LENGTH_LONG).show(); + } + }; + + private MispRestClient.UserCallback userCallback = new MispRestClient.UserCallback() { + @Override + public void success(User user) { + userAction.setCurrentUploadState(UploadAction.UploadState.DONE); + + Server server = new Server(); + server.name = uploadInformation.getRemote().organisation.name + "'s Sync Server"; + server.url = uploadInformation.getRemote().baseUrl; + server.remote_org_id = uploadInformation.getRemote().organisation.id; + server.authkey = uploadInformation.getLocal().syncUserAuthkey; + server.pull = uploadInformation.isPull(); + server.push = uploadInformation.isPush(); + server.caching_enabled = uploadInformation.isCached(); + server.self_signed = uploadInformation.isAllowSelfSigned(); + + restClient.addServer(server, serverCallback); + } + + @Override + public void failure(String error) { + userAction.setCurrentUploadState(UploadAction.UploadState.ERROR); + uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.FAILURE); + preferenceManager.addUploadInformation(uploadInformation); + Snackbar.make(rootLayout, error, Snackbar.LENGTH_LONG).show(); + } + }; + + private MispRestClient.ServerCallback serverCallback = new MispRestClient.ServerCallback() { + @Override + public void success(List servers) { + + } + + @Override + public void success(MispServer server) { + + } + + @Override + public void success(Server server) { + serverAction.setCurrentUploadState(UploadAction.UploadState.DONE); + uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.COMPLETE); + preferenceManager.addUploadInformation(uploadInformation); + finish(); + } + + @Override + public void failure(String error) { + serverAction.setCurrentUploadState(UploadAction.UploadState.ERROR); + uploadInformation.setCurrentSyncStatus(UploadInformation.SyncStatus.FAILURE); + preferenceManager.addUploadInformation(uploadInformation); + Snackbar.make(rootLayout, error, Snackbar.LENGTH_LONG).show(); + } + }; + + + private int organisationExists() throws IOException { + final UUID uuidToCheck = UUID.fromString(uploadInformation.getRemote().organisation.uuid); + + Organisation[] organisations = restClient.getAllOrganisations(); + + if (organisations != null) { + for (Organisation organisation : organisations) { + if (uuidToCheck.compareTo(UUID.fromString(organisation.uuid)) == 0) { + return organisation.id; + } + } + } + + return -1; + } + + private int userExists() { + + return -1; + } +} diff --git a/app/src/main/java/lu/circl/mispbump/adapters/SyncAdapter.java b/app/src/main/java/lu/circl/mispbump/adapters/SyncAdapter.java index b7a1a40..973d443 100644 --- a/app/src/main/java/lu/circl/mispbump/adapters/SyncAdapter.java +++ b/app/src/main/java/lu/circl/mispbump/adapters/SyncAdapter.java @@ -1,66 +1,112 @@ package lu.circl.mispbump.adapters; import android.content.Context; -import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.v7.widget.CardView; -import android.support.v7.widget.RecyclerView; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.widget.ImageViewCompat; +import androidx.recyclerview.widget.RecyclerView; + +import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; -import android.widget.Toast; import java.util.List; import lu.circl.mispbump.R; -import lu.circl.mispbump.activities.HomeActivity; -import lu.circl.mispbump.activities.ProfileActivity; +import lu.circl.mispbump.custom_views.MaterialPreferenceText; +import lu.circl.mispbump.interfaces.IOnItemClickListener; import lu.circl.mispbump.models.UploadInformation; public class SyncAdapter extends RecyclerView.Adapter { private Context context; private List uploadInformationList; + private IOnItemClickListener deleteListener, retryListener; static class SyncViewHolder extends RecyclerView.ViewHolder { + MaterialPreferenceText email, password; TextView orgName, date; ImageView syncStatus; ImageButton retry, delete; - SyncViewHolder(View v, final Context context) { + ConstraintLayout collapsedContent, expandedContent; + + SyncViewHolder(View v) { super(v); + expandedContent = v.findViewById(R.id.expandedContent); + expandedContent.setVisibility(View.GONE); + + collapsedContent = v.findViewById(R.id.collapsedContent); + collapsedContent.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (expandedContent.getVisibility() == View.GONE) { + expandedContent.setVisibility(View.VISIBLE); + } else { + expandedContent.setVisibility(View.GONE); + } + } + }); + orgName = v.findViewById(R.id.orgName); date = v.findViewById(R.id.date); + email = v.findViewById(R.id.email); + password = v.findViewById(R.id.password); + syncStatus = v.findViewById(R.id.syncStatus); retry = v.findViewById(R.id.retryButton); delete = v.findViewById(R.id.deleteButton); + } - v.setOnClickListener(new View.OnClickListener() { + void bindDeleteListener(final UploadInformation item, final IOnItemClickListener listener) { + delete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - // context.startActivity(new Intent(context, ProfileActivity.class)); + listener.onItemClick(item); + } + }); + } + + void bindRetryListener(final UploadInformation item, final IOnItemClickListener listener) { + retry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onItemClick(item); } }); } } - public SyncAdapter(List uploadInformationList, Context context) { - this.uploadInformationList = uploadInformationList; + public SyncAdapter(Context context) { this.context = context; } + public void setUploadInformationList(List uploadInformationList) { + this.uploadInformationList = uploadInformationList; + notifyDataSetChanged(); + } + + public void setOnDeleteClickListener(IOnItemClickListener listener) { + deleteListener = listener; + } + + public void setOnRetryClickListener(IOnItemClickListener listener) { + retryListener = listener; + } + @NonNull @Override public SyncViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.viewholder_sync_card, viewGroup, false); - return new SyncViewHolder(v, context); + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.viewholder_sync, viewGroup, false); + return new SyncViewHolder(v); } @Override @@ -68,23 +114,29 @@ public class SyncAdapter extends RecyclerView.Adapter uploadInformationList) { + KeyStoreWrapper ksw = new KeyStoreWrapper(KeyStoreWrapper.UPLOAD_INFORMATION_ALIAS); - Type type = new TypeToken>() {}.getType(); - List dataList; - - if (storedUploadInfoString.isEmpty()) { - dataList = new ArrayList<>(); - } else { - dataList = new Gson().fromJson(storedUploadInfoString, type); + try { + String cipherText = ksw.encrypt(new Gson().toJson(uploadInformationList)); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(UPLOAD_INFO, cipherText); + editor.apply(); + } catch (NoSuchPaddingException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (BadPaddingException e) { + e.printStackTrace(); + } catch (IllegalBlockSizeException e) { + e.printStackTrace(); } - - dataList.add(uploadInformation); - - SharedPreferences.Editor editor = preferences.edit(); - editor.putString(UPLOAD_INFO, new Gson().toJson(dataList)); - editor.apply(); } public List getUploadInformation() { - String storedUploadInfoString = preferences.getString(UPLOAD_INFO, ""); + KeyStoreWrapper ksw = new KeyStoreWrapper(KeyStoreWrapper.UPLOAD_INFORMATION_ALIAS); + String storedUploadInfoString = preferences.getString(UPLOAD_INFO, null); - if (storedUploadInfoString.isEmpty()) { + Type type = new TypeToken>() {}.getType(); + + if (storedUploadInfoString == null) { return null; } - Type type = new TypeToken>() {}.getType(); + try { + storedUploadInfoString = ksw.decrypt(storedUploadInfoString); + } catch (NoSuchPaddingException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidAlgorithmParameterException e) { + e.printStackTrace(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (BadPaddingException e) { + e.printStackTrace(); + } catch (IllegalBlockSizeException e) { + e.printStackTrace(); + } + return new Gson().fromJson(storedUploadInfoString, type); } + public void addUploadInformation(UploadInformation uploadInformation) { + List uploadInformationList = getUploadInformation(); + + if (uploadInformationList == null) { + uploadInformationList = new ArrayList<>(); + uploadInformationList.add(uploadInformation); + setUploadInformationList(uploadInformationList); + } else { + + // check if upload information already exists + for (int i = 0; i < uploadInformationList.size(); i++) { + if (uploadInformationList.get(i).getId().compareTo(uploadInformation.getId()) == 0) { + uploadInformationList.set(i, uploadInformation); + setUploadInformationList(uploadInformationList); + return; + } + } + + uploadInformationList.add(uploadInformation); + setUploadInformationList(uploadInformationList); + } + } + + public boolean containsUploadInformation(UUID uuid) { + List uploadInformationList = getUploadInformation(); + + if (uploadInformationList == null) { + return false; + } + + for (UploadInformation ui : uploadInformationList) { + if (ui.getId().compareTo(uuid) == 0) { + return true; + } + } + + return false; + } + + public boolean removeUploadInformation(UUID uuid) { + Log.d("PREFS", "uuid to delete: " + uuid.toString()); + + List uploadInformationList = getUploadInformation(); + + for (UploadInformation ui : uploadInformationList) { + + Log.d("PREFS", "checking uuid: " + ui.getId().toString()); + + if (ui.getId().compareTo(uuid) == 0) { + if (uploadInformationList.size() == 1) { + clearUploadInformation(); + } else { + uploadInformationList.remove(ui); + setUploadInformationList(uploadInformationList); + } + return true; + } + } + + + return false; + } + + public void clearUploadInformation() { + KeyStoreWrapper keyStoreWrapper = new KeyStoreWrapper(KeyStoreWrapper.UPLOAD_INFORMATION_ALIAS); + keyStoreWrapper.deleteStoredKey(); + + SharedPreferences.Editor editor = preferences.edit(); + editor.remove(UPLOAD_INFO); + editor.apply(); + } + /** * Set if credentials (authkey & server url) should be saved locally. diff --git a/app/src/main/java/lu/circl/mispbump/cam/CameraFragment.java b/app/src/main/java/lu/circl/mispbump/cam/CameraFragment.java index 209d844..7cbc4f4 100644 --- a/app/src/main/java/lu/circl/mispbump/cam/CameraFragment.java +++ b/app/src/main/java/lu/circl/mispbump/cam/CameraFragment.java @@ -1,6 +1,8 @@ package lu.circl.mispbump.cam; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -8,7 +10,12 @@ import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.graphics.*; +import android.graphics.Bitmap; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; @@ -21,19 +28,28 @@ import android.media.ImageReader; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; -import android.renderscript.*; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicYuvToRGB; +import android.renderscript.Type; import android.util.Log; import android.util.Size; import android.util.SparseArray; import android.util.SparseIntArray; -import android.view.*; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; + import com.google.android.gms.vision.Frame; import com.google.android.gms.vision.barcode.Barcode; import com.google.android.gms.vision.barcode.BarcodeDetector; @@ -108,6 +124,10 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest } } + private static final String TAG = "CAMERA"; + + private View hideCamView; + private QrScanCallback qrResultCallback; @Override @@ -126,8 +146,6 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest ORIENTATIONS.append(Surface.ROTATION_270, 180); } - private static final String TAG = "CameraFragment"; - /** * Max preview width that is guaranteed by Camera2 API */ @@ -163,7 +181,8 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest } @Override - public void onSurfaceTextureUpdated(SurfaceTexture texture) { } + public void onSurfaceTextureUpdated(SurfaceTexture texture) { + } }; private ImageProcessingThread imageProcessingThread; @@ -338,6 +357,10 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_camera, container, false); + + hideCamView = v.findViewById(R.id.hideCam); + hideCamView.setVisibility(View.GONE); + initRenderScript(); setUpBarcodeDetector(); return v; @@ -356,20 +379,21 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest @Override public void onResume() { super.onResume(); - startBackgroundThread(); - - imageProcessingThread = new ImageProcessingThread(); - imageProcessingThread.start(); - - // When the screen is turned off and turned back on, the SurfaceTexture is already - // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open - // a camera and start preview from here (otherwise, we wait until the surface is ready in - // the SurfaceTextureListener). - if (autoFitTextureView.isAvailable()) { - openCamera(autoFitTextureView.getWidth(), autoFitTextureView.getHeight()); - } else { - autoFitTextureView.setSurfaceTextureListener(mSurfaceTextureListener); - } + enablePreview(); +// startBackgroundThread(); +// +// imageProcessingThread = new ImageProcessingThread(); +// imageProcessingThread.start(); +// +// // When the screen is turned off and turned back on, the SurfaceTexture is already +// // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open +// // a camera and start preview from here (otherwise, we wait until the surface is ready in +// // the SurfaceTextureListener). +// if (autoFitTextureView.isAvailable()) { +// openCamera(autoFitTextureView.getWidth(), autoFitTextureView.getHeight()); +// } else { +// autoFitTextureView.setSurfaceTextureListener(mSurfaceTextureListener); +// } } @Override @@ -590,8 +614,13 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest * Stops the background thread and its {@link Handler}. */ private void stopBackgroundThread() { - mBackgroundThread.quitSafely(); + + if (mBackgroundThread == null) { + return; + } + try { + mBackgroundThread.quitSafely(); mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; @@ -773,6 +802,11 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest void qrScanResult(String qrData); } + public interface CameraReadyCallback { + void ready(); + } + + private CameraReadyCallback cameraReadyCallback; private boolean readQrEnabled = true; private BarcodeDetector barcodeDetector; private RenderScript renderScript; @@ -826,10 +860,58 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest } public void setReadQrEnabled(boolean enabled) { - Log.d(TAG, "setReadQrEnabled() called with: enabled = [" + enabled + "]"); readQrEnabled = enabled; } + public void disablePreview() { + hideCamView.setAlpha(0f); + hideCamView.setVisibility(View.VISIBLE); + hideCamView.animate() + .alpha(1f) + .setDuration(250) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + closeCamera(); + stopBackgroundThread(); + + if (imageProcessingThread.isAlive()) { + imageProcessingThread.isRunning = false; + } + } + }); + } + + public void enablePreview() { + + startBackgroundThread(); + + imageProcessingThread = new ImageProcessingThread(); + imageProcessingThread.start(); + + if (autoFitTextureView.isAvailable()) { + openCamera(autoFitTextureView.getWidth(), autoFitTextureView.getHeight()); + } else { + autoFitTextureView.setSurfaceTextureListener(mSurfaceTextureListener); + } + + hideCamView.setAlpha(1f); + hideCamView.setVisibility(View.VISIBLE); + hideCamView.animate() + .alpha(0f) + .setStartDelay(100) + .setDuration(1000) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + hideCamView.setVisibility(View.GONE); + if (cameraReadyCallback != null) { + cameraReadyCallback.ready(); + } + } + }); + } + private void setUpBarcodeDetector() { barcodeDetector = new BarcodeDetector.Builder(getActivity()) .setBarcodeFormats(Barcode.QR_CODE) @@ -843,5 +925,9 @@ public class CameraFragment extends Fragment implements ActivityCompat.OnRequest public void setOnQrAvailableListener(QrScanCallback callback) { qrResultCallback = callback; } + + public void setCameraReadyCallback(CameraReadyCallback callback) { + this.cameraReadyCallback = callback; + } } diff --git a/app/src/main/java/lu/circl/mispbump/custom_views/ExtendedBottomSheetBehavior.java b/app/src/main/java/lu/circl/mispbump/custom_views/ExtendedBottomSheetBehavior.java new file mode 100644 index 0000000..700489a --- /dev/null +++ b/app/src/main/java/lu/circl/mispbump/custom_views/ExtendedBottomSheetBehavior.java @@ -0,0 +1,58 @@ +package lu.circl.mispbump.custom_views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; + +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; + +public class ExtendedBottomSheetBehavior extends BottomSheetBehavior { + + private boolean swipeable = false; + private Context context; + + public ExtendedBottomSheetBehavior() { + super(); + } + + public ExtendedBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + + @Override + public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + if (swipeable) { + return super.onInterceptTouchEvent(parent, child, event); + } + return false; + } + + @Override + public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + if (swipeable) { + return super.onTouchEvent(parent, child, event); + } + return false; + } + + @Override + public boolean onNestedPreFling(CoordinatorLayout parent, V child, View target, float velocityX, float velocityY) { + if (swipeable) { + return super.onNestedPreFling(parent, child, target, velocityX, velocityY); + } + return false; + } + + public void setSwipeable(boolean swipeable) { + this.swipeable = swipeable; + } + + public boolean isSwipeable() { + return swipeable; + } +} diff --git a/app/src/main/java/lu/circl/mispbump/custom_views/MaterialPreferenceText.java b/app/src/main/java/lu/circl/mispbump/custom_views/MaterialPreferenceText.java index 7196361..b00de52 100644 --- a/app/src/main/java/lu/circl/mispbump/custom_views/MaterialPreferenceText.java +++ b/app/src/main/java/lu/circl/mispbump/custom_views/MaterialPreferenceText.java @@ -2,7 +2,7 @@ package lu.circl.mispbump.custom_views; import android.content.Context; import android.content.res.TypedArray; -import android.support.constraint.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintLayout; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -39,9 +39,4 @@ public class MaterialPreferenceText extends ConstraintLayout { public void setSubText(String subText) { subtitle.setText(subText); } - -// @Override -// public void onClick(View v) { -// -// } } diff --git a/app/src/main/java/lu/circl/mispbump/custom_views/UploadAction.java b/app/src/main/java/lu/circl/mispbump/custom_views/UploadAction.java new file mode 100644 index 0000000..221fc74 --- /dev/null +++ b/app/src/main/java/lu/circl/mispbump/custom_views/UploadAction.java @@ -0,0 +1,109 @@ +package lu.circl.mispbump.custom_views; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.core.widget.ImageViewCompat; + +import lu.circl.mispbump.R; + +public class UploadAction extends LinearLayoutCompat { + + private Context context; + + public enum UploadState { + PENDING, + LOADING, + DONE, + ERROR + } + + private TextView errorView; + private UploadState currentUploadState; + private ImageView stateView; + private ProgressBar progressBar; + + + public UploadAction(Context context) { + super(context); + this.context = context; + } + + public UploadAction(Context context, AttributeSet attrs) { + super(context, attrs); + + this.context = context; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UploadAction); + String title = a.getString(R.styleable.UploadAction_title); + a.recycle(); + + LayoutInflater inflater = LayoutInflater.from(context); + View baseView = inflater.inflate(R.layout.view_upload_action, this); + + errorView = findViewById(R.id.error); + + TextView titleView = baseView.findViewById(R.id.title); + titleView.setText(title); + + stateView = findViewById(R.id.stateView); + progressBar = findViewById(R.id.progressBar); + } + + /** + * Displays an error message for the upload action. + * @param error a string to show or null to hide + */ + public void setError(String error) { + if (error == null) { + errorView.setVisibility(GONE); + } + + errorView.setText(error); + errorView.setVisibility(VISIBLE); + } + + public void setCurrentUploadState(UploadState state) { + + currentUploadState = state; + progressBar.setVisibility(GONE); + + switch (state) { + case PENDING: + stateView.setVisibility(VISIBLE); + stateView.setImageResource(R.drawable.ic_info_outline); + ImageViewCompat.setImageTintList(stateView, ColorStateList.valueOf(context.getColor(R.color.status_amber))); + break; + + case LOADING: + stateView.setVisibility(GONE); + progressBar.setVisibility(VISIBLE); + break; + + case DONE: + stateView.setVisibility(VISIBLE); + stateView.setImageResource(R.drawable.ic_check); + ImageViewCompat.setImageTintList(stateView, ColorStateList.valueOf(context.getColor(R.color.status_green))); + break; + + case ERROR: + stateView.setVisibility(VISIBLE); + stateView.setImageResource(R.drawable.ic_error_outline); + ImageViewCompat.setImageTintList(stateView, ColorStateList.valueOf(context.getColor(R.color.status_red))); + break; + } + } + + public UploadState getCurrentUploadState() { + return currentUploadState; + } + +} diff --git a/app/src/main/java/lu/circl/mispbump/fragments/HomeFragment.java b/app/src/main/java/lu/circl/mispbump/fragments/HomeFragment.java index a49bcc8..25c15b8 100644 --- a/app/src/main/java/lu/circl/mispbump/fragments/HomeFragment.java +++ b/app/src/main/java/lu/circl/mispbump/fragments/HomeFragment.java @@ -1,8 +1,6 @@ package lu.circl.mispbump.fragments; -import android.support.v4.app.Fragment; +import androidx.fragment.app.Fragment; public class HomeFragment extends Fragment { - - } diff --git a/app/src/main/java/lu/circl/mispbump/fragments/SyncOptionsFragment.java b/app/src/main/java/lu/circl/mispbump/fragments/SyncOptionsFragment.java index 1446e92..9e9b7a5 100644 --- a/app/src/main/java/lu/circl/mispbump/fragments/SyncOptionsFragment.java +++ b/app/src/main/java/lu/circl/mispbump/fragments/SyncOptionsFragment.java @@ -1,9 +1,10 @@ package lu.circl.mispbump.fragments; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -13,37 +14,17 @@ import lu.circl.mispbump.R; public class SyncOptionsFragment extends Fragment { - private Switch share, push, pull, cache; - private OptionsReadyCallback readyCallback; - - public interface OptionsReadyCallback { - void ready(boolean share_events, boolean push, boolean pull, boolean caching); - } + public Switch allowSelfSigned, push, pull, cache; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_sync_options, container, false); - share = v.findViewById(R.id.share_events_switch); + allowSelfSigned = v.findViewById(R.id.self_signed_switch); push = v.findViewById(R.id.push_switch); pull = v.findViewById(R.id.pull_switch); cache = v.findViewById(R.id.cache_switch); - FloatingActionButton fab = v.findViewById(R.id.sync_options_fab); - fab.setOnClickListener(fabListener); - return v; } - - private View.OnClickListener fabListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - readyCallback.ready(share.isChecked(), push.isChecked(), pull.isChecked(), cache.isChecked()); - } - }; - - public void setOnOptionsReadyCallback(OptionsReadyCallback callback) { - readyCallback = callback; - } - } \ No newline at end of file diff --git a/app/src/main/java/lu/circl/mispbump/interfaces/IOnItemClickListener.java b/app/src/main/java/lu/circl/mispbump/interfaces/IOnItemClickListener.java new file mode 100644 index 0000000..2a76fdf --- /dev/null +++ b/app/src/main/java/lu/circl/mispbump/interfaces/IOnItemClickListener.java @@ -0,0 +1,5 @@ +package lu.circl.mispbump.interfaces; + +public interface IOnItemClickListener { + void onItemClick(T clickedObject); +} diff --git a/app/src/main/java/lu/circl/mispbump/models/SyncInformation.java b/app/src/main/java/lu/circl/mispbump/models/SyncInformation.java index abb3501..a0f7161 100644 --- a/app/src/main/java/lu/circl/mispbump/models/SyncInformation.java +++ b/app/src/main/java/lu/circl/mispbump/models/SyncInformation.java @@ -9,9 +9,21 @@ import lu.circl.mispbump.restful_client.Organisation; public class SyncInformation { public Organisation organisation; + public String syncUserEmail; public String syncUserPassword; public String syncUserAuthkey; public String baseUrl; public SyncInformation() {} + + @Override + public String toString() { + return "SyncInformation{" + + "organisation=" + organisation + + ", syncUserEmail='" + syncUserEmail + '\'' + + ", syncUserPassword='" + syncUserPassword + '\'' + + ", syncUserAuthkey='" + syncUserAuthkey + '\'' + + ", baseUrl='" + baseUrl + '\'' + + '}'; + } } diff --git a/app/src/main/java/lu/circl/mispbump/models/UploadInformation.java b/app/src/main/java/lu/circl/mispbump/models/UploadInformation.java index 7b3222c..60aca90 100644 --- a/app/src/main/java/lu/circl/mispbump/models/UploadInformation.java +++ b/app/src/main/java/lu/circl/mispbump/models/UploadInformation.java @@ -1,11 +1,15 @@ package lu.circl.mispbump.models; +import androidx.annotation.NonNull; + +import java.io.Serializable; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; +import java.util.UUID; -public class UploadInformation { +public class UploadInformation implements Serializable { public enum SyncStatus { COMPLETE, @@ -13,8 +17,12 @@ public class UploadInformation { PENDING } + private UUID id; + private SyncStatus currentSyncStatus = SyncStatus.PENDING; + private boolean allowSelfSigned, pull, push, cached; + private SyncInformation local; private SyncInformation remote; @@ -29,12 +37,11 @@ public class UploadInformation { } public UploadInformation(SyncInformation local, SyncInformation remote) { + id = UUID.randomUUID(); date = Calendar.getInstance().getTime(); this.local = local; this.remote = remote; - - date = Calendar.getInstance().getTime(); } // getter and setter @@ -60,6 +67,16 @@ public class UploadInformation { return remote; } + public UUID getId() { + return id; + } + public void setId(UUID id) { + this.id = id; + } + + public void setDate() { + setDate(Calendar.getInstance().getTime()); + } public void setDate(Date date) { this.date = date; } @@ -71,4 +88,42 @@ public class UploadInformation { return df.format(date); } + public boolean isAllowSelfSigned() { + return allowSelfSigned; + } + public void setAllowSelfSigned(boolean allowSelfSigned) { + this.allowSelfSigned = allowSelfSigned; + } + + public boolean isPull() { + return pull; + } + public void setPull(boolean pull) { + this.pull = pull; + } + + public boolean isPush() { + return push; + } + public void setPush(boolean push) { + this.push = push; + } + + public boolean isCached() { + return cached; + } + public void setCached(boolean cached) { + this.cached = cached; + } + + @NonNull + @Override + public String toString() { + return "UploadInformation{" + + "currentSyncStatus=" + currentSyncStatus + + ", local=" + local.toString() + + ", remote=" + remote.toString() + + ", date=" + date + + '}'; + } } diff --git a/app/src/main/java/lu/circl/mispbump/restful_client/MispRestClient.java b/app/src/main/java/lu/circl/mispbump/restful_client/MispRestClient.java index 9c89081..2dc850b 100644 --- a/app/src/main/java/lu/circl/mispbump/restful_client/MispRestClient.java +++ b/app/src/main/java/lu/circl/mispbump/restful_client/MispRestClient.java @@ -4,20 +4,19 @@ import android.annotation.SuppressLint; import android.content.Context; import android.util.Log; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; - import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; +import java.net.NoRouteToHostException; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.util.Iterator; import java.util.List; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; @@ -27,7 +26,6 @@ import lu.circl.mispbump.auxiliary.PreferenceManager; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; -import okhttp3.ResponseBody; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Call; import retrofit2.Callback; @@ -45,23 +43,34 @@ public class MispRestClient { public interface AvailableCallback { void available(); - void unavailable(); + + void unavailable(String error); } public interface UserCallback { void success(User user); + void failure(String error); } public interface OrganisationCallback { void success(Organisation organisation); + + void failure(String error); + } + + public interface OrganisationsCallback { + void success(Organisation[] organisations); void failure(String error); } public interface ServerCallback { void success(List servers); + void success(MispServer server); + void success(Server server); + void failure(String error); } @@ -71,6 +80,7 @@ public class MispRestClient { /** * Initializes the rest client to communicate with a MISP instance. + * * @param context needed to access the preferences for loading credentials */ public MispRestClient(Context context) { @@ -95,13 +105,15 @@ public class MispRestClient { /** * NOTE: for development only! - * Accepts all certificates so self signed certs are also accepted. - * @return OkHttpClient which accepts all certificates + *

+ * Accepts all certificates including self signed. + * + * @return {@link OkHttpClient} which accepts all certificates */ private OkHttpClient getUnsafeOkHttpClient() { try { // Create a trust manager that does not validate certificate chains - final TrustManager[] trustAllCerts = new TrustManager[] { + final TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @SuppressLint("TrustAllX509TrustManager") @Override @@ -163,6 +175,7 @@ public class MispRestClient { /** * Check via pyMispRoute if server is available + * * @param callback {@link AvailableCallback} */ public void isAvailable(final AvailableCallback callback) { @@ -176,7 +189,7 @@ public class MispRestClient { return; } - callback.unavailable(); + callback.unavailable(extractError(response)); } else { callback.available(); } @@ -184,7 +197,7 @@ public class MispRestClient { @Override public void onFailure(Call call, Throwable t) { - callback.unavailable(); + callback.unavailable(extractError(t)); } }); } @@ -193,6 +206,7 @@ public class MispRestClient { /** * Fetches information about the user that is associated with saved auth key. + * * @param callback {@link UserCallback} wrapper to return user directly */ public void getMyUser(final UserCallback callback) { @@ -201,7 +215,7 @@ public class MispRestClient { call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { - if(!response.isSuccessful()) { + if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { if (response.body() != null) { @@ -222,7 +236,8 @@ public class MispRestClient { /** * Get an user with specific ID. - * @param userId user identifier + * + * @param userId user identifier * @param callback {@link UserCallback} wrapper to return user directly */ public void getUser(int userId, final UserCallback callback) { @@ -231,7 +246,7 @@ public class MispRestClient { call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { - if(!response.isSuccessful()) { + if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { if (response.body() != null) { @@ -252,7 +267,8 @@ public class MispRestClient { /** * Add a given user to the MISP instance referenced by url in preferences. - * @param user user to add + * + * @param user user to add * @param callback {@link UserCallback} wrapper to return the created user directly */ public void addUser(User user, final UserCallback callback) { @@ -261,9 +277,10 @@ public class MispRestClient { call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { - if(!response.isSuccessful()) { + if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { + assert response.body() != null; callback.success(response.body().user); } } @@ -280,7 +297,8 @@ public class MispRestClient { /** * Get an organisation by a given organisation id. - * @param orgId organisation identifier + * + * @param orgId organisation identifier * @param callback {@link OrganisationCallback} wrapper to return a organisation directly */ public void getOrganisation(int orgId, final OrganisationCallback callback) { @@ -289,7 +307,7 @@ public class MispRestClient { call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { - if(!response.isSuccessful()) { + if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { if (response.body() != null) { @@ -307,10 +325,52 @@ public class MispRestClient { }); } + public Organisation[] getAllOrganisations() throws IOException { + Call> call = mispRestService.getAllOrganisations(); + Response> response = call.execute(); + + List mispOrganisations = response.body(); + Organisation[] organisations = new Organisation[mispOrganisations.size()]; + + for (int i = 0; i < mispOrganisations.size(); i++) { + organisations[i] = mispOrganisations.get(i).organisation; + } + + return organisations; + +// call.enqueue(new Callback>() { +// @Override +// public void onResponse(Call> call, Response> response) { +// if (!response.isSuccessful()) { +// // TODO handle +// return; +// } +// +// List mispOrganisations = response.body(); +// +// assert mispOrganisations != null; +// +// Organisation[] organisations = new Organisation[mispOrganisations.size()]; +// +// for (int i = 0; i < mispOrganisations.size(); i++) { +// organisations[i] = mispOrganisations.get(i).organisation; +// } +// +// callback.success(organisations); +// } +// +// @Override +// public void onFailure(Call> call, Throwable t) { +// callback.failure(extractError(t)); +// } +// }); + } + /** * Add a given organisation to the MISP instance referenced by url in preferences. + * * @param organisation organisation to add - * @param callback {@link OrganisationCallback} wrapper to return the created organisation directly + * @param callback {@link OrganisationCallback} wrapper to return the created organisation directly */ public void addOrganisation(Organisation organisation, final OrganisationCallback callback) { Call call = mispRestService.addOrganisation(organisation); @@ -318,9 +378,10 @@ public class MispRestClient { call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { - if(!response.isSuccessful()) { + if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { + assert response.body() != null; callback.success(response.body().organisation); } } @@ -336,6 +397,7 @@ public class MispRestClient { /** * Get all servers on MISP instance. + * * @param callback {@link OrganisationCallback} wrapper to return a list of servers directly */ public void getServers(final ServerCallback callback) { @@ -344,7 +406,7 @@ public class MispRestClient { call.enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { - if(!response.isSuccessful()) { + if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { callback.success(response.body()); @@ -360,29 +422,10 @@ public class MispRestClient { /** * Add a server to the MISP instance - * @param server the server to create + * + * @param server the server to create * @param callback {@link ServerCallback} wrapper to return the created server directly */ -// public void addServer(MispServer server, final ServerCallback callback) { -// Call call = mispRestService.addServer(server); -// -// call.enqueue(new Callback() { -// @Override -// public void onResponse(Call call, Response response) { -// if(!response.isSuccessful()) { -// callback.failure(extractError(response)); -// } else { -// callback.success(response.body()); -// } -// } -// -// @Override -// public void onFailure(Call call, Throwable t) { -// callback.failure(t.getMessage()); -// } -// }); -// } - public void addServer(Server server, final ServerCallback callback) { Call call = mispRestService.addServer(server); @@ -403,6 +446,14 @@ public class MispRestClient { }); } + // error parsing + + /** + * Converts error {@link Response}s to human readable info. + * @param response erroneous response + * @param type of response + * @return human readable String that describes the error + */ private String extractError(Response response) { switch (response.code()) { // bad request (malformed) @@ -416,9 +467,15 @@ public class MispRestClient { // forbidden case 403: try { + assert response.errorBody() != null; JSONObject jsonError = new JSONObject(response.errorBody().string()); String name = jsonError.getString("name") + "\n"; + + if (name.startsWith("Authentication failed")) { + return "Authentication failed"; + } + String reasons = ""; JSONObject errorReasons = jsonError.getJSONObject("errors"); @@ -437,11 +494,33 @@ public class MispRestClient { return "Could not parse (403) error"; - // not found + // not found case 404: return "Not found"; } return "Unknown error"; } + + /** + * Converts a {@link Throwable} to a human readable error message. + * @param t throwable + * @return human readable String that describes the error. + */ + private String extractError(Throwable t) { + + if (t.getCause() instanceof CertificateException) { + return "Trust anchor for certification path not found.\nSelf signed certificates are not supported."; + } + + if (t instanceof SSLHandshakeException) { + return "SSL Handshake Error"; + } + + if (t instanceof NoRouteToHostException) { + return "Server is not available (no route to host)"; + } + + return t.getMessage(); + } } \ No newline at end of file diff --git a/app/src/main/java/lu/circl/mispbump/restful_client/MispRestService.java b/app/src/main/java/lu/circl/mispbump/restful_client/MispRestService.java index 92daf1a..4eb6430 100644 --- a/app/src/main/java/lu/circl/mispbump/restful_client/MispRestService.java +++ b/app/src/main/java/lu/circl/mispbump/restful_client/MispRestService.java @@ -34,6 +34,9 @@ public interface MispRestService { @GET("organisations/view/{value}") Call getOrganisation(@Path("value") int orgId); + @GET("organisations") + Call> getAllOrganisations(); + @POST("admin/organisations/add") Call addOrganisation(@Body Organisation organisation); diff --git a/app/src/main/java/lu/circl/mispbump/restful_client/Organisation.java b/app/src/main/java/lu/circl/mispbump/restful_client/Organisation.java index dcf0539..f24bd59 100644 --- a/app/src/main/java/lu/circl/mispbump/restful_client/Organisation.java +++ b/app/src/main/java/lu/circl/mispbump/restful_client/Organisation.java @@ -5,18 +5,6 @@ package lu.circl.mispbump.restful_client; */ public class Organisation { - public Organisation() { - } - - public Organisation(String name) { - this.name = name; - } - - public Organisation(String name, String description) { - this.name = name; - this.description = description; - } - public Integer id; public String name; public String date_created; @@ -32,7 +20,19 @@ public class Organisation { public String created_by; public Integer user_count; - public Organisation syncOrganisation() { + public Organisation() { + } + + public Organisation(String name) { + this.name = name; + } + + public Organisation(String name, String description) { + this.name = name; + this.description = description; + } + + public Organisation toSyncOrganisation() { Organisation organisation = new Organisation(); organisation.local = true; organisation.name = name; diff --git a/app/src/main/java/lu/circl/mispbump/security/DiffieHellman.java b/app/src/main/java/lu/circl/mispbump/security/DiffieHellman.java index 6a0ebb7..407b057 100644 --- a/app/src/main/java/lu/circl/mispbump/security/DiffieHellman.java +++ b/app/src/main/java/lu/circl/mispbump/security/DiffieHellman.java @@ -32,6 +32,7 @@ public class DiffieHellman { private byte[] sharedSecret; private IvParameterSpec ivParameterSpec; + private DiffieHellman() { initialize(); } @@ -68,25 +69,6 @@ public class DiffieHellman { } } - /** - * Generates a shared secret and derives an initialisation vector from it. - * @param publickey public key of the sync partner - */ - public void setForeignPublicKey(PublicKey publickey) { - try { - keyAgreement.doPhase(publickey, true); - - byte[] tmpSharedSecret = keyAgreement.generateSecret(); - sharedSecret = Arrays.copyOfRange(tmpSharedSecret, 0, 32); - - byte[] inputVector = Arrays.copyOfRange(sharedSecret, 32, 48); - ivParameterSpec = new IvParameterSpec(inputVector); - - } catch (InvalidKeyException e) { - e.printStackTrace(); - } - } - /** * Encrypts data. * @param data data to encrypt @@ -129,6 +111,27 @@ public class DiffieHellman { return data; } + /** + * Generates a shared secret and derives an initialisation vector from it. + * @param pk public key of the sync partner + */ + public void setForeignPublicKey(PublicKey pk) { + try { + keyAgreement.doPhase(pk, true); + + byte[] tmpSharedSecret = keyAgreement.generateSecret(); + sharedSecret = Arrays.copyOfRange(tmpSharedSecret, 0, 32); + + byte[] inputVector = Arrays.copyOfRange(sharedSecret, 32, 48); + ivParameterSpec = new IvParameterSpec(inputVector); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } + } + + /** + * @return this devices public key + */ public PublicKey getPublicKey() { return publickey; } diff --git a/app/src/main/java/lu/circl/mispbump/security/KeyStoreWrapper.java b/app/src/main/java/lu/circl/mispbump/security/KeyStoreWrapper.java index 7e044e1..0576074 100644 --- a/app/src/main/java/lu/circl/mispbump/security/KeyStoreWrapper.java +++ b/app/src/main/java/lu/circl/mispbump/security/KeyStoreWrapper.java @@ -35,6 +35,7 @@ public class KeyStoreWrapper { public static final String USER_ORGANISATION_INFO_ALIAS = "ALIAS_USER_ORGANISATION_INFO"; public static final String AUTOMATION_ALIAS = "ALIAS_AUTOMATION_KEY"; public static final String SERVER_URL_ALIAS = "ALIAS_SERVER_URL"; + public static final String UPLOAD_INFORMATION_ALIAS = "ALIAS_UPLOAD_INFORMATION"; private static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding"; @@ -174,7 +175,6 @@ public class KeyStoreWrapper { cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] byteData = data.getBytes(StandardCharsets.UTF_8); -// byte[] byteData = Base64.decode(data, Base64.DEFAULT); byte[] combined = getCombinedArray(cipher.getIV(), cipher.doFinal(byteData)); return Base64.encodeToString(combined, Base64.NO_WRAP); } @@ -262,15 +262,12 @@ public class KeyStoreWrapper { } public class IvAndData { - - public IvAndData() {} - - public IvAndData(byte[] iv, byte[] data) { + IvAndData(byte[] iv, byte[] data) { this.iv = iv; this.data = data; } - public byte[] iv; - public byte[] data; + byte[] iv; + byte[] data; } } \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..6fb52a3 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..020a1be --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/slide_in_left.xml b/app/src/main/res/animator/slide_in_left.xml new file mode 100644 index 0000000..ddc7c45 --- /dev/null +++ b/app/src/main/res/animator/slide_in_left.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/slide_in_right.xml b/app/src/main/res/animator/slide_in_right.xml new file mode 100644 index 0000000..206720e --- /dev/null +++ b/app/src/main/res/animator/slide_in_right.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/slide_out_left.xml b/app/src/main/res/animator/slide_out_left.xml new file mode 100644 index 0000000..e3e2d1d --- /dev/null +++ b/app/src/main/res/animator/slide_out_left.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/slide_out_right.xml b/app/src/main/res/animator/slide_out_right.xml new file mode 100644 index 0000000..10d8add --- /dev/null +++ b/app/src/main/res/animator/slide_out_right.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..f8ad49c --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml index 25fb386..42d364a 100644 --- a/app/src/main/res/drawable/ic_arrow_forward.xml +++ b/app/src/main/res/drawable/ic_arrow_forward.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml index 17aca2a..d04a04c 100644 --- a/app/src/main/res/drawable/ic_check.xml +++ b/app/src/main/res/drawable/ic_check.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_check_outline.xml b/app/src/main/res/drawable/ic_check_outline.xml new file mode 100644 index 0000000..9e25fdc --- /dev/null +++ b/app/src/main/res/drawable/ic_check_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml index 0c8775c..cbf794b 100644 --- a/app/src/main/res/drawable/ic_close.xml +++ b/app/src/main/res/drawable/ic_close.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_cloud_upload.xml b/app/src/main/res/drawable/ic_cloud_upload.xml new file mode 100644 index 0000000..55dbbae --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_upload.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_forever.xml b/app/src/main/res/drawable/ic_delete_forever.xml index 3047430..4469a5e 100644 --- a/app/src/main/res/drawable/ic_delete_forever.xml +++ b/app/src/main/res/drawable/ic_delete_forever.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_help_outline.xml b/app/src/main/res/drawable/ic_help_outline.xml new file mode 100644 index 0000000..fc62825 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_help_outline_black_24dp.xml b/app/src/main/res/drawable/ic_info_outline.xml similarity index 50% rename from app/src/main/res/drawable/ic_help_outline_black_24dp.xml rename to app/src/main/res/drawable/ic_info_outline.xml index dbd0032..af0d4d0 100644 --- a/app/src/main/res/drawable/ic_help_outline_black_24dp.xml +++ b/app/src/main/res/drawable/ic_info_outline.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_key.xml b/app/src/main/res/drawable/ic_key.xml new file mode 100644 index 0000000..ae7aa7b --- /dev/null +++ b/app/src/main/res/drawable/ic_key.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml new file mode 100644 index 0000000..bc01039 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_open_in_browser.xml b/app/src/main/res/drawable/ic_open_in_browser.xml new file mode 100644 index 0000000..3fb9799 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in_browser.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml index d7366bd..f0aedfe 100644 --- a/app/src/main/res/drawable/ic_person.xml +++ b/app/src/main/res/drawable/ic_person.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/rounded_rect.xml b/app/src/main/res/drawable/rounded_rect.xml new file mode 100644 index 0000000..8daefbc --- /dev/null +++ b/app/src/main/res/drawable/rounded_rect.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 326f6bd..cb08ed5 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -1,27 +1,25 @@ - - - - + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + android:background="@color/colorPrimary" /> + - - + app:backgroundTint="@color/colorAccent" + android:tint="@color/white" + android:src="@drawable/ic_qrcode_scan"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 57fe8a2..1cb59dc 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -1,78 +1,81 @@ - - + app:layout_constraintTop_toTopOf="parent" + app:popupTheme="@style/ThemeOverlay.AppCompat.Dark" /> - - - + - - - - + -