diff --git a/.idea/misc.xml b/.idea/misc.xml index 635999df..53a3fb16 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -24,7 +24,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 46cd7508..7831fccd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,10 +32,13 @@ dependencies { implementation 'com.android.support:recyclerview-v7:27.1.1' implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support:design:27.1.1' + implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.takisoft.fix:preference-v7:27.1.1.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'me.dm7.barcodescanner:zxing:1.9' implementation 'com.android.support:cardview-v7:27.1.1' + implementation 'com.github.esafirm.android-image-picker:imagepicker:1.13.0' + implementation 'com.github.avito-tech:krop:3e65e12' implementation 'com.android.support:support-v4:27.1.1' implementation 'com.mattprecious.swirl:swirl:1.0.0' implementation 'com.madgag.spongycastle:core:1.58.0.0' diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java index c1725899..ef58dd5f 100644 --- a/app/src/main/java/me/impy/aegis/db/Database.java +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -6,6 +6,7 @@ import org.json.JSONObject; import java.util.List; +import me.impy.aegis.encoding.Base64Exception; import me.impy.aegis.otp.OtpInfoException; public class Database { @@ -43,7 +44,7 @@ public class Database { entry.deserialize(array.getJSONObject(i)); addEntry(entry); } - } catch (OtpInfoException | JSONException e) { + } catch (Base64Exception | OtpInfoException | JSONException e) { throw new DatabaseException(e); } } diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java index 624f02b2..1027ac7c 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java @@ -6,6 +6,8 @@ import org.json.JSONObject; import java.io.Serializable; import java.util.UUID; +import me.impy.aegis.encoding.Base64; +import me.impy.aegis.encoding.Base64Exception; import me.impy.aegis.otp.OtpInfo; import me.impy.aegis.otp.OtpInfoException; @@ -13,8 +15,8 @@ public class DatabaseEntry implements Serializable { private UUID _uuid; private String _name = ""; private String _issuer = ""; - private String _icon = ""; private OtpInfo _info; + private byte[] _icon; public DatabaseEntry(OtpInfo info) { _info = info; @@ -36,6 +38,7 @@ public class DatabaseEntry implements Serializable { obj.put("name", _name); obj.put("issuer", _issuer); obj.put("info", _info.toJson()); + obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon)); } catch (JSONException e) { throw new RuntimeException(e); } @@ -43,7 +46,7 @@ public class DatabaseEntry implements Serializable { return obj; } - public void deserialize(JSONObject obj) throws JSONException, OtpInfoException { + public void deserialize(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception { // if there is no uuid, generate a new one if (!obj.has("uuid")) { _uuid = UUID.randomUUID(); @@ -53,6 +56,11 @@ public class DatabaseEntry implements Serializable { _name = obj.getString("name"); _issuer = obj.getString("issuer"); _info = OtpInfo.parseJson(obj.getString("type"), obj.getJSONObject("info")); + + String icon = obj.optString("icon", null); + if (icon != null) { + _icon = Base64.decode(icon); + } } public UUID getUUID() { @@ -67,7 +75,7 @@ public class DatabaseEntry implements Serializable { return _issuer; } - public String getIcon() { + public byte[] getIcon() { return _icon; } @@ -83,11 +91,11 @@ public class DatabaseEntry implements Serializable { _issuer = issuer; } - public void setIcon(String icon) { - _icon = icon; - } - public void setInfo(OtpInfo info) { _info = info; } + + public void setIcon(byte[] icon) { + _icon = icon; + } } diff --git a/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java b/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java index bcfc8497..1adea7a7 100644 --- a/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java +++ b/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java @@ -1,5 +1,7 @@ package me.impy.aegis.helpers; +import android.view.View; + import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.util.ColorGenerator; @@ -8,7 +10,7 @@ public class TextDrawableHelper { } - public static TextDrawable generate(String text, String fallback) { + public static TextDrawable generate(String text, String fallback, View view) { if (text == null || text.isEmpty()) { if (fallback == null || fallback.isEmpty()) { return null; @@ -18,6 +20,8 @@ public class TextDrawableHelper { ColorGenerator generator = ColorGenerator.MATERIAL; int color = generator.getColor(text); - return TextDrawable.builder().buildRound(text.substring(0, 1).toUpperCase(), color); + return TextDrawable.builder().beginConfig() + .width(view.getWidth()) + .height(view.getHeight()).endConfig().buildRect(text.substring(0, 1).toUpperCase(), color); } } diff --git a/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java b/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java index a694659b..cba5c419 100644 --- a/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java @@ -1,6 +1,12 @@ package me.impy.aegis.ui; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.ArrayRes; import android.support.v7.app.ActionBar; @@ -22,7 +28,14 @@ import android.widget.Spinner; import android.widget.TableRow; import com.amulyakhare.textdrawable.TextDrawable; +import com.avito.android.krop.KropView; +import com.esafirm.imagepicker.features.ImagePicker; +import com.esafirm.imagepicker.features.ReturnMode; +import com.esafirm.imagepicker.model.Image; +import java.io.ByteArrayOutputStream; + +import de.hdodenhof.circleimageview.CircleImageView; import me.impy.aegis.R; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.encoding.Base32; @@ -40,8 +53,9 @@ public class EditEntryActivity extends AegisActivity { private boolean _isNew = false; private boolean _edited = false; private DatabaseEntry _entry; - - private ImageView _iconView; + private boolean _hasCustomImage = false; + private CircleImageView _iconView; + private ImageView _saveImageButton; private EditText _textName; private EditText _textIssuer; @@ -57,6 +71,8 @@ public class EditEntryActivity extends AegisActivity { private Spinner _spinnerDigits; private SpinnerItemSelectedListener _selectedListener = new SpinnerItemSelectedListener(); + private KropView _kropView; + private RelativeLayout _advancedSettingsHeader; private RelativeLayout _advancedSettings; @@ -79,6 +95,8 @@ public class EditEntryActivity extends AegisActivity { // set up fields _iconView = findViewById(R.id.profile_drawable); + _kropView = findViewById(R.id.krop_view); + _saveImageButton = findViewById(R.id.iv_saveImage); _textName = findViewById(R.id.text_name); _textIssuer = findViewById(R.id.text_issuer); _textPeriod = findViewById(R.id.text_period); @@ -97,8 +115,15 @@ public class EditEntryActivity extends AegisActivity { // fill the fields with values if possible if (_entry != null) { - TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName()); - _iconView.setImageDrawable(drawable); + if (_entry.getIcon() != null) { + byte[] imageBytes = _entry.getIcon(); + Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); + _iconView.setImageBitmap(image); + _hasCustomImage = true; + } else { + TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _iconView); + _iconView.setImageDrawable(drawable); + } _textName.setText(_entry.getName()); _textIssuer.setText(_entry.getIssuer()); @@ -173,6 +198,24 @@ public class EditEntryActivity extends AegisActivity { } }); + ImagePicker imagePicker = ImagePicker.create(this) + .returnMode(ReturnMode.ALL) // set whether pick and / or camera action should return immediate result or not. + .folderMode(true) // folder mode (false by default) + .toolbarFolderTitle("Folder") // folder selection title + .toolbarImageTitle("Tap to select") // image selection title + .toolbarArrowColor(Color.BLACK) // Toolbar 'up' arrow color + .single() // single mode + .showCamera(false) // show camera or not (true by default) + .imageDirectory("Camera"); + + // Open ImagePicker when clicking on the icon + _iconView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + imagePicker.start(); // start image picker activity with request code + } + }); + _advancedSettingsHeader.setOnClickListener(v -> { openAdvancedSettings(); }); @@ -265,6 +308,10 @@ public class EditEntryActivity extends AegisActivity { finish(true); }); break; + case R.id.action_default_image: + TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _iconView); + _iconView.setImageDrawable(drawable); + _hasCustomImage = false; default: return super.onOptionsItemSelected(item); } @@ -278,6 +325,10 @@ public class EditEntryActivity extends AegisActivity { if (_isNew) { menu.findItem(R.id.action_delete).setVisible(false); } + if (!_hasCustomImage) { + menu.findItem(R.id.action_default_image).setVisible(false); + } + return true; } @@ -289,6 +340,29 @@ public class EditEntryActivity extends AegisActivity { finish(); } + @Override + protected void onActivityResult(int requestCode, final int resultCode, Intent data) { + if (ImagePicker.shouldHandle(requestCode, resultCode, data)) { + // or get a single image only + Image image = ImagePicker.getFirstImageOrNull(data); + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + Bitmap bitmap = BitmapFactory.decodeFile(image.getPath(),bmOptions); + _kropView.setBitmap(bitmap); + _kropView.setVisibility(View.VISIBLE); + + _saveImageButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + _iconView.setImageBitmap(_kropView.getCroppedBitmap()); + _kropView.setVisibility(View.GONE); + _hasCustomImage = true; + } + }); + } + + super.onActivityResult(requestCode, resultCode, data); + } + private boolean onSave() { if (_textSecret.length() == 0) { onError("Secret is a required field."); @@ -359,6 +433,15 @@ public class EditEntryActivity extends AegisActivity { entry.setIssuer(_textIssuer.getText().toString()); entry.setName(_textName.getText().toString()); + if (_hasCustomImage) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + drawableToBitmap(_iconView.getDrawable()).compress(Bitmap.CompressFormat.JPEG, 100, stream); + byte[] bitmapdata = stream.toByteArray(); + entry.setIcon(bitmapdata); + } else { + entry.setIcon(null); + } + _entry = entry; finish(false); return true; @@ -404,8 +487,10 @@ public class EditEntryActivity extends AegisActivity { @Override public void afterTextChanged(Editable s) { - TextDrawable drawable = TextDrawableHelper.generate(_textIssuer.getText().toString(), _textName.getText().toString()); - _iconView.setImageDrawable(drawable); + if (!_hasCustomImage) { + TextDrawable drawable = TextDrawableHelper.generate(_textIssuer.getText().toString(), _textName.getText().toString(), _iconView); + _iconView.setImageDrawable(drawable); + } } }; @@ -441,4 +526,25 @@ public class EditEntryActivity extends AegisActivity { } return -1; } + + public static Bitmap drawableToBitmap(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + final int width = !drawable.getBounds().isEmpty() ? drawable + .getBounds().width() : drawable.getIntrinsicWidth(); + + final int height = !drawable.getBounds().isEmpty() ? drawable + .getBounds().height() : drawable.getIntrinsicHeight(); + + final Bitmap bitmap = Bitmap.createBitmap(width <= 0 ? 1 : width, + height <= 0 ? 1 : height, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } } diff --git a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java index 16259e03..1f140fc4 100644 --- a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java @@ -1,6 +1,5 @@ package me.impy.aegis.ui; -import android.Manifest; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -57,7 +56,9 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); } + setWizardMode(true); showSkipButton(false); + pager.setPagingEnabled(false); //showPagerIndicator(false); setGoBackLock(true); @@ -68,19 +69,6 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { homeSliderPage.setBgColor(getResources().getColor(R.color.colorPrimary)); addSlide(AppIntroFragment.newInstance(homeSliderPage)); - SliderPage permSliderPage = new SliderPage(); - permSliderPage.setTitle("Permissions"); - permSliderPage.setDescription("Aegis needs permission to use your camera in order to scan QR codes. " + - "It also needs access to external storage to able to export the database."); - permSliderPage.setImageDrawable(R.drawable.intro_scanner); - permSliderPage.setBgColor(getResources().getColor(R.color.colorAccent)); - addSlide(AppIntroFragment.newInstance(permSliderPage)); - askForPermissions(new String[]{ - Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - }, 2); - _authenticationSlide = new CustomAuthenticationSlide(); _authenticationSlide.setBgColor(getResources().getColor(R.color.colorHeaderSuccess)); addSlide(_authenticationSlide); @@ -121,7 +109,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { // skip to the last slide if no encryption will be used if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) { // TODO: no magic indices - getPager().setCurrentItem(5); + getPager().setCurrentItem(4); } } } diff --git a/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticationSlide.java b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticationSlide.java index f3b8144d..b46f7421 100644 --- a/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticationSlide.java +++ b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticationSlide.java @@ -8,8 +8,10 @@ import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ArrayAdapter; import android.widget.RadioButton; import android.widget.RadioGroup; +import android.widget.Spinner; import android.widget.TextView; import com.github.paolorotolo.appintro.ISlidePolicy; @@ -23,6 +25,7 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, public static final int CRYPT_TYPE_PASS = 2; public static final int CRYPT_TYPE_FINGER = 3; + private Spinner _authenticationSpinner; private RadioGroup _buttonGroup; private int _bgColor; @@ -38,8 +41,8 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, if (manager != null) { RadioButton button = view.findViewById(R.id.rb_fingerprint); TextView text = view.findViewById(R.id.text_rb_fingerprint); - button.setVisibility(View.VISIBLE); - text.setVisibility(View.VISIBLE); + button.setEnabled(false); + text.setEnabled(false); } view.findViewById(R.id.main).setBackgroundColor(_bgColor); diff --git a/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java b/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java index 38c05e0e..5025d04c 100644 --- a/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java +++ b/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java @@ -1,6 +1,10 @@ package me.impy.aegis.ui.views; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.ImageView; @@ -72,8 +76,14 @@ public class EntryHolder extends RecyclerView.ViewHolder { _profileIssuer.setText(" - " + entry.getIssuer()); } - TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName()); - _profileDrawable.setImageDrawable(drawable); + if (entry.getIcon() != null) { + byte[] imageBytes = entry.getIcon(); + Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); + _profileDrawable.setImageBitmap(image); + } else { + TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), _profileDrawable); + _profileDrawable.setImageDrawable(drawable); + } refreshCode(); } diff --git a/app/src/main/res/color/disabled_textview_colors.xml b/app/src/main/res/color/disabled_textview_colors.xml new file mode 100644 index 00000000..adbd5b7e --- /dev/null +++ b/app/src/main/res/color/disabled_textview_colors.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check_black_24dp.xml b/app/src/main/res/drawable/ic_check_black_24dp.xml new file mode 100644 index 00000000..3c728c59 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_id_card.xml b/app/src/main/res/drawable/ic_id_card.xml new file mode 100644 index 00000000..724573e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_id_card.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_edit_entry.xml b/app/src/main/res/layout/activity_edit_entry.xml index 54211460..5b8d4734 100644 --- a/app/src/main/res/layout/activity_edit_entry.xml +++ b/app/src/main/res/layout/activity_edit_entry.xml @@ -16,18 +16,36 @@ android:layout_height="250dp" android:background="@color/colorPrimary"> - - - + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + /> + + + + + diff --git a/app/src/main/res/layout/card_entry.xml b/app/src/main/res/layout/card_entry.xml index 36de09c1..0330461f 100644 --- a/app/src/main/res/layout/card_entry.xml +++ b/app/src/main/res/layout/card_entry.xml @@ -21,12 +21,14 @@ android:layout_height="match_parent" android:paddingLeft="16dp"> - + + android:textSize="24sp" + android:layout_marginTop="30dp" + android:textStyle="bold" /> + + + android:layout_alignParentBottom="true" + android:layout_marginTop="12dp" + android:orientation="horizontal"> + + android:text="@string/authentication_method_none" + android:textSize="16sp" /> + android:layout_marginTop="-5dp" + android:text="@string/authentication_method_none_description" + android:textColor="@color/secondary_text_inverted" /> + android:checked="true" + android:text="@string/authentication_method_password" + android:textSize="16sp" /> + android:text="@string/authentication_method_password_description" + android:textColor="@color/secondary_text_inverted" /> + android:text="@string/authentication_method_fingerprint" + android:textSize="16sp" + /> + + android:layout_marginTop="-5dp" + + android:text="@string/authentication_method_fingerprint_description" + android:textColor="@color/disabled_textview_colors" + /> + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8332990e..352bb667 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Settings Import Delete + Set default image Discard Save IntroActivity @@ -34,7 +35,7 @@ The database will be stored in plain text Password The database will be encrypted with a key derived from a password - Password & Fingerprint + Fingerprint In addition to a password, fingerprints registered on this device can be used to decrypt the database Password Enter your password @@ -45,7 +46,7 @@ None Password - Password & Fingerprint + Fingerprint