Skip to content
This repository has been archived by the owner on Sep 1, 2021. It is now read-only.

Commit

Permalink
Option to chose keystore directory, fixes #141
Browse files Browse the repository at this point in the history
Chose between app private directory and sdcard for keystore.
Defaults to app private directory which is always writable.

Things to improve:

  * copy data from and back sdcard on switching
  * change persist toggle back if no permission
  * `get_files_dir()` reuse "upstream", just override service part
  * `getExternalStoragePublicDirectory()` rather than `/sdcard/`,
    refs #145
  * we're now cheating the singleton so we can remove the app params
  * account list is cached (server side), when adding new account we
    should invalidate that cache or it won't get pulled
  * `get_running_app()` could also be improved to reuse the singleton
  • Loading branch information
AndreMiras committed May 30, 2019
1 parent 054061f commit 7edf929
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 204 deletions.
2 changes: 1 addition & 1 deletion docs/Debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ buildozer android adb -- logcat
```
or with filtering:
```
buildozer android adb -- logcat | grep -E '(python |service ):'
buildozer android adb -- logcat 2>&1 | grep -E '(I python|I service|F DEBUG)'
```
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ https://github.com/AndreMiras/garden.layoutmargin/archive/20180517.tar.gz#egg=la
https://github.com/AndreMiras/KivyMD/archive/69f3e88.tar.gz#egg=kivymd
https://github.com/AndreMiras/pyetheroll/archive/884805b.tar.gz#egg=pyetheroll
https://github.com/corpetty/py-etherscan-api/archive/cb91fb3.tar.gz#egg=py-etherscan-api
# Post 1.10.1 release
https://github.com/kivy/kivy/archive/90c86f8.zip#egg=Kivy
isort
kivy==1.10.1
kivyunittest==0.1.8
oscpy==0.3.0
plyer==1.3.1
Expand Down
5 changes: 5 additions & 0 deletions src/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Change Log


## [Unreleased]

- Save keystore to private user dir, refs #141


## [v2019.0426]

- Support F-Droid auto updates, refs #132
Expand Down
12 changes: 12 additions & 0 deletions src/ethereum_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@

class AccountUtils:

singleton = None

def __init__(self, keystore_dir):
self.keystore_dir = keystore_dir
self._accounts = None
os.makedirs(keystore_dir, exist_ok=True)

@classmethod
def get_or_create(cls, keystore_dir):
"""
Gets or creates the AccountUtils object so it loads lazily.
"""
if cls.singleton is None or \
cls.singleton.keystore_dir != keystore_dir:
cls.singleton = cls(keystore_dir=keystore_dir)
return cls.singleton

def get_account_list(self):
"""
Returns the Account list.
Expand Down
33 changes: 6 additions & 27 deletions src/etheroll/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from requests.exceptions import ConnectionError

from etheroll.constants import API_KEY_PATH
from etheroll.settings import SettingsScreen
from etheroll.settings import Settings
from etheroll.settings_screen import SettingsScreen
from etheroll.switchaccount import SwitchAccountScreen
from etheroll.ui_utils import Dialog, load_kv_from_py
from etheroll.utils import run_in_thread
Expand All @@ -35,7 +36,6 @@ def __init__(self, **kwargs):
Clock.schedule_once(self._after_init)
self._account_passwords = {}
self._pyetheroll = None
self._account_utils = None

def _after_init(self, dt):
"""
Expand Down Expand Up @@ -75,7 +75,7 @@ def pyetheroll(self):
Also recreates the object if the chain_id changed.
"""
from pyetheroll.etheroll import Etheroll
chain_id = SettingsScreen.get_stored_network()
chain_id = Settings.get_stored_network()
if self._pyetheroll is None or self._pyetheroll.chain_id != chain_id:
self._pyetheroll = Etheroll(API_KEY_PATH, chain_id)
return self._pyetheroll
Expand All @@ -86,14 +86,8 @@ def account_utils(self):
Gets or creates the AccountUtils object so it loads lazily.
"""
from ethereum_utils import AccountUtils
from etheroll.store import Store
if self._account_utils is None:
keystore_dir = Store.get_keystore_path()
# would try to create the keystore directory if doesn't exist,
# hence we need to make sure we have permissions
if self.check_request_write_permission():
self._account_utils = AccountUtils(keystore_dir=keystore_dir)
return self._account_utils
keystore_dir = Settings.get_keystore_path()
return AccountUtils.get_or_create(keystore_dir)

def preload_account_utils(self, dt):
"""
Expand Down Expand Up @@ -341,7 +335,7 @@ def roll(self):
roll_input = roll_screen.get_roll_input()
bet_size = roll_input['bet_size']
chances = roll_input['chances']
gas_price = SettingsScreen.get_stored_gas_price()
gas_price = Settings.get_stored_gas_price()
account = self.current_account
if account is None:
self.on_account_none()
Expand Down Expand Up @@ -416,21 +410,6 @@ def open_address_options(self):
lambda x: self.copy_address_clipboard(), icon='content-copy')
bottom_sheet.open()

@staticmethod
def check_request_write_permission():
"""
Android runtime storage permission check.
"""
if platform != "android":
return True
from android.permissions import (
Permission, request_permission, check_permission)
permission = Permission.WRITE_EXTERNAL_STORAGE
had_permission = check_permission(permission)
if not had_permission:
request_permission(permission)
return had_permission

@staticmethod
def on_permission_error(exception):
title = "Permission denied"
Expand Down
6 changes: 3 additions & 3 deletions src/etheroll/importkeystore.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from kivy.properties import StringProperty
from kivy.uix.boxlayout import BoxLayout

from etheroll.store import Store
from etheroll.settings import Settings
from etheroll.ui_utils import load_kv_from_py

load_kv_from_py(__file__)
Expand All @@ -12,11 +12,11 @@ class ImportKeystore(BoxLayout):
keystore_path = StringProperty()

def __init__(self, **kwargs):
super(ImportKeystore, self).__init__(**kwargs)
super().__init__(**kwargs)
Clock.schedule_once(self._after_init)

def _after_init(self, dt):
"""
Sets keystore_path.
"""
self.keystore_path = Store.get_keystore_path()
self.keystore_path = Settings.get_keystore_path()
140 changes: 72 additions & 68 deletions src/etheroll/settings.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,24 @@
import os

from kivy.app import App
from kivy.utils import platform
from pyetheroll.constants import DEFAULT_GAS_PRICE_GWEI, ChainID

from etheroll.constants import KEYSTORE_DIR_SUFFIX
from etheroll.store import Store
from etheroll.ui_utils import SubScreen, load_kv_from_py

load_kv_from_py(__file__)


class SettingsScreen(SubScreen):
class Settings:
"""
Screen for configuring network, gas price...
"""

def __init__(self, **kwargs):
super(SettingsScreen, self).__init__(**kwargs)

@staticmethod
def get_store():
"""
Wrappers around get_store for handling permissions on Android.
"""
controller = App.get_running_app().root
# would also work for read permission
controller.check_request_write_permission()
try:
store = Store.get_store()
except (PermissionError, OSError):
# PermissionError -> e.g. Android runtime permission
# OSError -> e.g. directory doesn't exist
# fails silently, setting will simply not be stored on disk
store = {}
return store

def store_network(self):
"""
Saves selected network to the store.
"""
store = self.get_store()
network = self.get_ui_network()
store.put('network', value=network.name)

def store_gas_price(self):
"""
Saves gas price value to the store.
"""
store = self.get_store()
gas_price = self.get_ui_gas_price()
store.put('gas_price', value=gas_price)

def store_settings(self):
"""
Stores settings to json store.
"""
self.store_gas_price()
self.store_network()

def get_ui_network(self):
"""
Retrieves network values from UI.
"""
if self.is_ui_mainnet():
network = ChainID.MAINNET
else:
network = ChainID.ROPSTEN
return network

def is_ui_mainnet(self):
return self.ids.mainnet_checkbox_id.active

def is_ui_testnet(self):
return self.ids.testnet_checkbox_id.active

@classmethod
def get_stored_network(cls):
"""
Retrieves last stored network value, defaults to Mainnet.
"""
store = cls.get_store()
store = Store.get_store()
try:
network_dict = store['network']
except KeyError:
Expand All @@ -96,19 +38,81 @@ def is_stored_testnet(cls):
network = cls.get_stored_network()
return network == ChainID.ROPSTEN

def get_ui_gas_price(self):
return self.ids.gas_price_slider_id.value

@classmethod
def get_stored_gas_price(cls):
"""
Retrieves stored gas price value, defaults to DEFAULT_GAS_PRICE_GWEI.
"""
store = cls.get_store()
store = Store.get_store()
try:
gas_price_dict = store['gas_price']
except KeyError:
gas_price_dict = {}
gas_price = gas_price_dict.get(
'value', DEFAULT_GAS_PRICE_GWEI)
return gas_price

@classmethod
def is_persistent_keystore(cls):
"""
Retrieves the settings value regarding the keystore persistency.
Defaults to False.
"""
store = Store.get_store()
try:
persist_keystore_dict = store['persist_keystore']
except KeyError:
persist_keystore_dict = {}
persist_keystore = persist_keystore_dict.get(
'value', False)
return persist_keystore

@staticmethod
def get_files_dir():
"""
Alternative App._get_user_data_dir() implementation for Android
that also works when within a service activity.
"""
from jnius import autoclass, cast
PythonActivity = autoclass('org.kivy.android.PythonActivity')
activity = PythonActivity.mActivity
if activity is None:
# assume we're running from the background service
PythonService = autoclass('org.kivy.android.PythonService')
activity = PythonService.mService
context = cast('android.content.Context', activity)
file_p = cast('java.io.File', context.getFilesDir())
data_dir = file_p.getAbsolutePath()
return data_dir

@classmethod
def _get_android_keystore_prefix(cls, app=None):
"""
Returns the Android keystore path prefix.
The location differs based on the persistency user settings.
"""
if app is None:
app = App.get_running_app()
if cls.is_persistent_keystore():
# TODO: hardcoded path, refs:
# https://github.com/AndreMiras/EtherollApp/issues/145
KEYSTORE_DIR_PREFIX = os.path.join('/sdcard', app.name)
else:
KEYSTORE_DIR_PREFIX = cls.get_files_dir()
return KEYSTORE_DIR_PREFIX

@classmethod
def get_keystore_path(cls, app=None):
"""
Returns the keystore directory path.
This can be overriden by the `KEYSTORE_PATH` environment variable.
"""
keystore_path = os.environ.get('KEYSTORE_PATH')
if keystore_path is not None:
return keystore_path
KEYSTORE_DIR_PREFIX = os.path.expanduser("~")
if platform == "android":
KEYSTORE_DIR_PREFIX = cls._get_android_keystore_prefix(app)
keystore_path = os.path.join(
KEYSTORE_DIR_PREFIX, KEYSTORE_DIR_SUFFIX)
return keystore_path
28 changes: 25 additions & 3 deletions src/etheroll/settings.kv → src/etheroll/settings_screen.kv
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#:import Settings etheroll.settings.Settings
#:import MDCheckbox kivymd.selectioncontrols.MDCheckbox
#:import MDSlider kivymd.slider.MDSlider

Expand All @@ -22,7 +23,7 @@
size_hint: None, None
size: dp(48), dp(48)
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
active: root.is_stored_mainnet()
active: Settings.is_stored_mainnet()
MDLabel:
text: 'Mainnet'
theme_text_color: 'Primary'
Expand All @@ -34,7 +35,7 @@
size_hint: None, None
size: dp(48), dp(48)
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
active: root.is_stored_testnet()
active: Settings.is_stored_testnet()
MDLabel:
text: 'Testnet'
theme_text_color: 'Primary'
Expand All @@ -57,6 +58,27 @@
MDSlider:
id: gas_price_slider_id
range: 0, 50
value: root.get_stored_gas_price()
value: Settings.get_stored_gas_price()
step: 1
PushUp:
BoxLayout:
orientation: 'vertical'
MDLabel:
text: 'Persist keystore'
font_style: 'Title'
theme_text_color: 'Primary'
BoxLayout:
orientation: 'horizontal'
MDSwitch:
id: persist_keystore_switch_id
size_hint: None, None
size: dp(36), dp(48)
pos_hint: {'center_x': 0.75, 'center_y': 0.5}
active: Settings.is_persistent_keystore()
on_release: root.check_request_write_permission()
MDLabel:
halign: 'right'
text: 'Keeps accounts even if the app is uninstalled'
theme_text_color: 'Primary'

PushUp:
Loading

0 comments on commit 7edf929

Please sign in to comment.