Merge pull request #206 from loki-project/onion-requests

Onion Requests 2.0
This commit is contained in:
Niels Andriesse 2020-05-29 14:01:48 +10:00 committed by GitHub
commit f3ef282bfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 628 additions and 49 deletions

View File

@ -117,6 +117,9 @@
android:name="org.thoughtcrime.securesms.loki.activities.SettingsActivity"
android:screenOrientation="portrait"
android:theme="@style/Session.DarkTheme.NoActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.PathActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.QRCodeActivity"
android:screenOrientation="portrait" />

Binary file not shown.

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/accent" />
</shape>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="496dp"
android:height="496dp"
android:viewportWidth="496"
android:viewportHeight="496">
<path
android:pathData="M248,0C111.043,0 0,111.083 0,248C0,384.997 111.043,496 248,496C384.957,496 496,384.997 496,248C496,111.083 384.957,0 248,0ZM248.5,467C127.744,467 30,369.297 30,248.5C30,127.784 127.748,30 248.5,30C369.211,30 467,127.747 467,248.5C467,369.254 369.297,467 248.5,467ZM270.703,285.663L270.703,292C270.703,298.627 268.33,304 261.703,304L238.056,304C231.429,304 226.056,298.627 226.056,292L226.056,283.341C226.056,247.596 250.224,244.566 270.703,233.084C288.264,223.239 314.235,222.608 314.235,185.76C314.235,164.861 286.202,147.355 253.674,147.355C230.485,147.355 204.281,169.53 189.233,188.522C185.176,193.642 177.773,194.593 172.567,190.646L155.743,185.548C150.636,181.676 149.492,174.482 153.099,169.185C176.726,134.491 206.82,104 253.674,104C302.745,104 355.124,142.304 355.124,192.8C355.124,267.221 270.703,260.884 270.703,285.663ZM282,373C282,388.991 268.991,402 253,402C237.009,402 224,388.991 224,373C224,357.009 237.009,344 253,344C268.991,344 282,357.009 282,373Z"
android:strokeWidth="1"
android:fillColor="@color/text"
android:fillType="nonZero"
android:strokeColor="@color/text"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/paths_building" />
</shape>

View File

@ -40,6 +40,22 @@
android:layout_centerVertical="true"
android:layout_marginLeft="64dp" />
<RelativeLayout
android:id="@+id/pathStatusViewContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentRight="true"
android:layout_centerVertical="true" >
<org.thoughtcrime.securesms.loki.views.PathStatusView
android:layout_width="@dimen/path_status_view_size"
android:layout_height="@dimen/path_status_view_size"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_marginRight="8dp" />
</RelativeLayout>
</RelativeLayout>
</android.support.v7.widget.Toolbar>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/default_session_background"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/large_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:textSize="@dimen/small_font_size"
android:textColor="@color/text"
android:alpha="0.6"
android:textAlignment="center"
android:text="@string/activity_path_explanation" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="@dimen/large_spacing">
<LinearLayout
android:id="@+id/pathRowsContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_centerInParent="true" />
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:SpinKit_Color="@color/text" />
</RelativeLayout>
<Button
style="@style/MediumProminentOutlineButton"
android:id="@+id/rebuildPathButton"
android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height"
android:layout_marginBottom="@dimen/medium_spacing"
android:text="@string/activity_path_rebuild_path_button_title" />
</LinearLayout>

10
res/menu/menu_path.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/learnMoreButton"
android:icon="@drawable/ic_question_mark"
app:showAsAction="always" />
</menu>

View File

@ -5,6 +5,7 @@
<dimen name="medium_button_height">44dp</dimen>
<dimen name="tab_bar_height">48dp</dimen>
<dimen name="setting_button_height">72dp</dimen>
<dimen name="path_row_height">72dp</dimen>
<!-- Session -->
<dimen name="album_total_width">300dp</dimen>

View File

@ -28,6 +28,7 @@
<color name="new_conversation_button_collapsed_background">#1F1F1F</color>
<color name="pn_option_background">#1B1B1B</color>
<color name="pn_option_border">#212121</color>
<color name="paths_building">#FFCE3A</color>
<!-- Session -->
<!-- Loki -->

View File

@ -33,6 +33,10 @@
<dimen name="dialog_corner_radius">8dp</dimen>
<dimen name="dialog_button_corner_radius">4dp</dimen>
<dimen name="pn_option_corner_radius">8dp</dimen>
<dimen name="path_status_view_size">8dp</dimen>
<dimen name="path_row_height">56dp</dimen>
<dimen name="path_row_dot_size">8dp</dimen>
<dimen name="path_row_expanded_dot_size">16dp</dimen>
<!-- Distances -->
<dimen name="small_spacing">8dp</dimen>

View File

@ -1699,7 +1699,7 @@
<string name="fragment_enter_session_id_edit_text_hint">Enter your Session ID</string>
<string name="activity_display_name_title_2">Pick your display name</string>
<string name="activity_display_name_explanation">This will be your name when you use Session.</string>
<string name="activity_display_name_explanation">This will be your name when you use Session. It can be your real name, an alias, or anything else you like.</string>
<string name="activity_display_name_edit_text_hint">Enter a display name</string>
<string name="activity_display_name_display_name_missing_error">Please pick a display name</string>
<string name="activity_display_name_display_name_invalid_error">Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters</string>
@ -1708,9 +1708,9 @@
<string name="activity_pn_mode_title">Push Notifications</string>
<string name="activity_pn_mode_explanation">There are two ways Session can handle push notifications. Make sure to read the descriptions carefully before you choose.</string>
<string name="activity_pn_mode_fcm_option_title">Firebase Cloud Messaging</string>
<string name="activity_pn_mode_fcm_option_explanation">Session will use the Firebase Cloud Messaging service to receive push notifications. You\ll be notified of new messages reliably and immediately. Using FCM means that this device will communicate directly with Google\s servers to retrieve push notifications, which will expose your IP address to Google. Your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.</string>
<string name="activity_pn_mode_fcm_option_explanation">Session will use the Firebase Cloud Messaging service to receive push notifications. You\'ll be notified of new messages reliably and immediately. Using FCM means that your IP address and device token will be exposed to Google. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.</string>
<string name="activity_pn_mode_background_polling_option_title">Background Polling</string>
<string name="activity_pn_mode_background_polling_option_explanation">Session will occasionally check for new messages in the background. This guarantees full privacy protection, but message notifications may be significantly delayed.</string>
<string name="activity_pn_mode_background_polling_option_explanation">Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed.</string>
<string name="activity_pn_mode_recommended_option_tag">Recommended</string>
<string name="activity_pn_mode_no_option_picked_dialog_title">Please Pick an Option</string>
@ -1724,9 +1724,9 @@
<string name="sheet_pn_mode_title">Push Notifications</string>
<string name="sheet_pn_mode_explanation">Session now features two ways to handle push notifications. Make sure to read the descriptions carefully before you choose.</string>
<string name="sheet_pn_mode_fcm_option_title">Firebase Cloud Messaging</string>
<string name="sheet_pn_mode_fcm_option_explanation">Session will use the Firebase Cloud Messaging service to receive push notifications. You\ll be notified of new messages reliably and immediately. Using FCM means that this device will communicate directly with Google\s servers to retrieve push notifications, which will expose your IP address to Google. Your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.</string>
<string name="sheet_pn_mode_fcm_option_explanation">Session will use the Firebase Cloud Messaging service to receive push notifications. You\'ll be notified of new messages reliably and immediately. Using FCM means that your IP address and device token will be exposed to Google. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.</string>
<string name="sheet_pn_mode_background_polling_option_title">Background Polling</string>
<string name="sheet_pn_mode_background_polling_option_explanation">Session will occasionally check for new messages in the background. This guarantees full privacy protection, but message notifications may be significantly delayed.</string>
<string name="sheet_pn_mode_background_polling_option_explanation">Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed.</string>
<string name="sheet_pn_mode_recommended_option_tag">Recommended</string>
<string name="sheet_pn_mode_no_option_picked_dialog_title">Please Pick an Option</string>
<string name="sheet_pn_mode_confirm_button_title">Confirm</string>
@ -1734,13 +1734,21 @@
<string name="activity_seed_title">Your Recovery Phrase</string>
<string name="activity_seed_title_2">Meet your recovery phrase</string>
<string name="activity_seed_explanation">Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don\t give it to anyone. To restore your Session ID, launch Session and tap Continue your Session.</string>
<string name="activity_seed_explanation">Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don\t give it to anyone.</string>
<string name="activity_seed_reveal_button_title">Hold to reveal</string>
<string name="view_seed_reminder_subtitle_1">Secure your account by saving your recovery phrase</string>
<string name="view_seed_reminder_subtitle_2">Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID.</string>
<string name="view_seed_reminder_subtitle_3">Make sure to store your recovery phrase in a safe place</string>
<string name="activity_path_title">Path</string>
<string name="activity_path_explanation">Session hides your IP by bouncing your messages through several Service Nodes in Session\'s decentralized network. These are the Service Nodes currently being used by your device:</string>
<string name="activity_path_device_row_title">You</string>
<string name="activity_path_guard_node_row_title">Guard Node</string>
<string name="activity_path_service_node_row_title">Service Node</string>
<string name="activity_path_destination_row_title">Destination</string>
<string name="activity_path_rebuild_path_button_title">Rebuild Path</string>
<string name="activity_create_private_chat_title">New Session</string>
<string name="activity_create_private_chat_enter_session_id_tab_title">Enter Session ID</string>
<string name="activity_create_private_chat_scan_qr_code_tab_title">Scan QR Code</string>
@ -1796,7 +1804,7 @@
<string name="preferences_notifications_strategy_category_title">Notification Strategy</string>
<string name="preferences_notifications_use_fcm_option_title">Use FCM</string>
<string name="preferences_notifications_use_fcm_option_explanation">Using Firebase Cloud Messaging allows for more reliable push notifications, but exposes your IP to Google.</string>
<string name="preferences_notifications_use_fcm_option_explanation">Using Firebase Cloud Messaging allows for more reliable push notifications, but exposes your IP and device token to Google and Loki.</string>
<string name="dialog_link_device_slave_mode_title_1">Waiting for Authorization</string>
<string name="dialog_link_device_slave_mode_title_2">Device Link Authorized</string>
@ -1820,7 +1828,7 @@
<string name="dialog_seed_explanation">This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.</string>
<string name="dialog_clear_all_data_title">Clear All Data</string>
<string name="dialog_clear_all_data_explanation">This will permanently delete your Session ID, including all messages, sessions, and contacts.</string>
<string name="dialog_clear_all_data_explanation">This will permanently delete your messages, sessions, and contacts.</string>
<string name="activity_qr_code_title">QR Code</string>
<string name="activity_qr_code_view_my_qr_code_tab_title">View My QR Code</string>

View File

@ -181,6 +181,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
LokiSessionResetImplementation sessionResetImpl = new LokiSessionResetImplementation(this);
if (userPublicKey != null) {
LokiSwarmAPI.Companion.configureIfNeeded(apiDB);
LokiAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
FriendRequestProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);

View File

@ -82,8 +82,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV6 = 27;
private static final int lokiV7 = 28;
private static final int lokiV8 = 29;
private static final int lokiV9 = 30;
private static final int DATABASE_VERSION = lokiV8; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV9; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -131,7 +132,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
db.execSQL(StickerDatabase.CREATE_TABLE);
db.execSQL(LokiAPIDatabase.getCreateSwarmCacheTableCommand());
db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand());
db.execSQL(LokiAPIDatabase.getCreatePathCacheCommand());
db.execSQL(LokiAPIDatabase.getCreateSwarmCacheCommand());
db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTableCommand());
db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTableCommand());
db.execSQL(LokiAPIDatabase.getCreateGroupChatAuthTokenTableCommand());
@ -582,6 +585,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateSessionRequestTimestampTableCommand());
}
if (oldVersion < lokiV9) {
db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand());
db.execSQL(LokiAPIDatabase.getCreatePathCacheCommand());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -101,6 +101,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
profileButton.hexEncodedPublicKey = hexEncodedPublicKey
profileButton.update()
profileButton.setOnClickListener { openSettings() }
pathStatusViewContainer.setOnClickListener { showPath() }
// Set up seed reminder view
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
@ -262,6 +263,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
show(intent)
}
private fun showPath() {
val intent = Intent(this, PathActivity::class.java)
show(intent)
}
override fun createNewPrivateChat() {
val intent = Intent(this, CreatePrivateChatActivity::class.java)
show(intent)

View File

@ -0,0 +1,261 @@
package org.thoughtcrime.securesms.loki.activities
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.v4.content.LocalBroadcastManager
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_path.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.animateSizeChange
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.whispersystems.signalservice.loki.api.onionrequests.OnionRequestAPI
import org.whispersystems.signalservice.loki.api.onionrequests.Snode
class PathActivity : PassphraseRequiredActionBarActivity() {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_path)
supportActionBar!!.title = resources.getString(R.string.activity_path_title)
rebuildPathButton.setOnClickListener { rebuildPath() }
update(false)
registerObservers()
}
private fun registerObservers() {
val buildingPathsReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handleBuildingPathsEvent()
}
}
broadcastReceivers.add(buildingPathsReceiver)
LocalBroadcastManager.getInstance(this).registerReceiver(buildingPathsReceiver, IntentFilter("buildingPaths"))
val pathsBuiltReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handlePathsBuiltEvent()
}
}
broadcastReceivers.add(pathsBuiltReceiver)
LocalBroadcastManager.getInstance(this).registerReceiver(pathsBuiltReceiver, IntentFilter("pathsBuilt"))
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_path, menu)
return true
}
override fun onDestroy() {
for (receiver in broadcastReceivers) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
}
super.onDestroy()
}
// endregion
// region Updating
private fun handleBuildingPathsEvent() { update(false) }
private fun handlePathsBuiltEvent() { update(false) }
private fun update(isAnimated: Boolean) {
pathRowsContainer.removeAllViews()
if (OnionRequestAPI.paths.count() >= OnionRequestAPI.pathCount) {
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
val pathRows = path.mapIndexed { index, snode ->
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))
getPathRow(snode, LineView.Location.Middle, index.toLong() * 1000 + 2000, dotAnimationRepeatInterval, isGuardSnode)
}
val youRow = getPathRow("You", null, LineView.Location.Top, 1000, dotAnimationRepeatInterval)
val destinationRow = getPathRow("Destination", null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
for (row in rows) {
pathRowsContainer.addView(row)
}
if (isAnimated) {
spinner.fadeOut()
} else {
spinner.alpha = 0.0f
}
} else {
if (isAnimated) {
spinner.fadeIn()
} else {
spinner.alpha = 1.0f
}
}
}
// endregion
// region General
private fun getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Long, dotAnimationRepeatInterval: Long): LinearLayout {
val mainContainer = LinearLayout(this)
mainContainer.orientation = LinearLayout.HORIZONTAL
mainContainer.gravity = Gravity.CENTER_VERTICAL
val mainContainerLayoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
mainContainer.layoutParams = mainContainerLayoutParams
val lineView = LineView(this, location, dotAnimationStartDelay, dotAnimationRepeatInterval)
val lineViewLayoutParams = LinearLayout.LayoutParams(resources.getDimensionPixelSize(R.dimen.path_row_expanded_dot_size), resources.getDimensionPixelSize(R.dimen.path_row_height))
lineView.layoutParams = lineViewLayoutParams
mainContainer.addView(lineView)
val titleTextView = TextView(this)
titleTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.medium_font_size))
titleTextView.text = title
val titleContainer = LinearLayout(this)
titleContainer.orientation = LinearLayout.VERTICAL
titleContainer.addView(titleTextView)
val titleContainerLayoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
titleContainerLayoutParams.marginStart = resources.getDimensionPixelSize(R.dimen.large_spacing)
titleContainer.layoutParams = titleContainerLayoutParams
mainContainer.addView(titleContainer)
if (subtitle != null) {
val subtitleTextView = TextView(this)
subtitleTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
subtitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.small_font_size))
subtitleTextView.text = subtitle
titleContainer.addView(subtitleTextView)
}
return mainContainer
}
private fun getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Long, dotAnimationRepeatInterval: Long, isGuardSnode: Boolean): LinearLayout {
val title = if (isGuardSnode) resources.getString(R.string.activity_path_guard_node_row_title) else resources.getString(R.string.activity_path_service_node_row_title)
val subtitle = snode.toString().removePrefix("https://").substringBefore(":")
return getPathRow(title, subtitle, location, dotAnimationStartDelay, dotAnimationRepeatInterval)
}
// endregion
// region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
when(id) {
R.id.learnMoreButton -> learnMore()
else -> { /* Do nothing */ }
}
return super.onOptionsItemSelected(item)
}
private fun learnMore() {
try {
val url = "https://getsession.org/faq/#onion-routing"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
}
}
private fun rebuildPath() {
DatabaseFactory.getLokiAPIDatabase(this).clearPaths()
OnionRequestAPI.guardSnodes = setOf()
OnionRequestAPI.paths = listOf()
OnionRequestAPI.buildPaths()
}
// endregion
// region Line View
private class LineView : RelativeLayout {
private lateinit var location: Location
private var dotAnimationStartDelay: Long = 0
private var dotAnimationRepeatInterval: Long = 0
private val dotView by lazy {
val result = View(context)
result.setBackgroundResource(R.drawable.accent_dot)
result
}
enum class Location {
Top, Middle, Bottom
}
constructor(context: Context, location: Location, dotAnimationStartDelay: Long, dotAnimationRepeatInterval: Long) : super(context) {
this.location = location
this.dotAnimationStartDelay = dotAnimationStartDelay
this.dotAnimationRepeatInterval = dotAnimationRepeatInterval
setUpViewHierarchy()
}
constructor(context: Context) : super(context) {
throw Exception("Use LineView(context:location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
throw Exception("Use LineView(context:location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
throw Exception("Use LineView(context:location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
throw Exception("Use LineView(context:location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
}
private fun setUpViewHierarchy() {
val lineView = View(context)
lineView.setBackgroundColor(resources.getColorWithID(R.color.text, context.theme))
val lineViewHeight = when (location) {
Location.Top, Location.Bottom -> resources.getDimensionPixelSize(R.dimen.path_row_height) / 2
Location.Middle -> resources.getDimensionPixelSize(R.dimen.path_row_height)
}
val lineViewLayoutParams = LayoutParams(1, lineViewHeight)
when (location) {
Location.Top -> lineViewLayoutParams.addRule(ALIGN_PARENT_BOTTOM)
Location.Middle, Location.Bottom -> lineViewLayoutParams.addRule(ALIGN_PARENT_TOP)
}
lineViewLayoutParams.addRule(CENTER_HORIZONTAL)
lineView.layoutParams = lineViewLayoutParams
addView(lineView)
val dotViewSize = resources.getDimensionPixelSize(R.dimen.path_row_dot_size)
val dotViewLayoutParams = LayoutParams(dotViewSize, dotViewSize)
dotViewLayoutParams.addRule(CENTER_IN_PARENT)
dotView.layoutParams = dotViewLayoutParams
addView(dotView)
Handler().postDelayed({
performAnimation()
}, dotAnimationStartDelay)
}
private fun performAnimation() {
expand()
Handler().postDelayed({
collapse()
Handler().postDelayed({
performAnimation()
}, dotAnimationRepeatInterval)
}, 1000)
}
private fun expand() {
dotView.animateSizeChange(R.dimen.path_row_dot_size, R.dimen.path_row_expanded_dot_size)
}
private fun collapse() {
dotView.animateSizeChange(R.dimen.path_row_expanded_dot_size, R.dimen.path_row_dot_size)
}
}
// endregion
}

View File

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.loki.activities
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
@ -31,6 +29,8 @@ import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog
import org.thoughtcrime.securesms.loki.dialogs.SeedDialog
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mms.GlideApp
@ -147,7 +147,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
private fun updateProfile(isUpdatingProfilePicture: Boolean) {
showLoader()
loader.fadeIn()
val promises = mutableListOf<Promise<*, Exception>>()
val displayName = displayNameToBeUploaded
if (displayName != null) {
@ -187,24 +187,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
profilePictureView.update()
}
profilePictureToBeUploaded = null
hideLoader()
loader.fadeOut()
}
}
private fun showLoader() {
loader.visibility = View.VISIBLE
loader.animate().setDuration(150).alpha(1.0f).start()
}
private fun hideLoader() {
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
loader.visibility = View.GONE
}
})
}
// endregion
// region Interaction

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues
import android.content.Context
import android.util.Log
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
@ -16,11 +17,21 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
private val userPublicKey get() = TextSecurePreferences.getLocalNumber(context)
companion object {
// Snode pool cache
private val snodePoolCache = "loki_snode_pool_cache"
private val dummyKey = "dummy_key"
private val snodePoolKey = "snode_pool_key"
@JvmStatic val createSnodePoolCacheCommand = "CREATE TABLE $snodePoolCache ($dummyKey TEXT PRIMARY KEY, $snodePoolKey TEXT);"
// Path cache
private val pathCache = "loki_path_cache"
private val indexPath = "index_path"
private val snode = "snode"
@JvmStatic val createPathCacheCommand = "CREATE TABLE $pathCache ($indexPath TEXT PRIMARY KEY, $snode TEXT);"
// Swarm cache
private val swarmCache = "loki_api_swarm_cache"
private val hexEncodedPublicKey = "hex_encoded_public_key"
private val swarm = "swarm"
@JvmStatic val createSwarmCacheTableCommand = "CREATE TABLE $swarmCache ($hexEncodedPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
@JvmStatic val createSwarmCacheCommand = "CREATE TABLE $swarmCache ($hexEncodedPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
// Last message hash value cache
private val lastMessageHashValueCache = "loki_api_last_message_hash_value_cache"
private val target = "target"
@ -66,6 +77,90 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
@JvmStatic val createSessionRequestTimestampTableCommand = "CREATE TABLE $sessionRequestTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);"
}
override fun getSnodePool(): Set<LokiAPITarget> {
val database = databaseHelper.readableDatabase
return database.get(snodePoolCache, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor ->
val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePoolKey))
snodePoolAsString.split(", ").mapNotNull { snodeAsString ->
val components = snodeAsString.split("-")
val address = components[0]
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
LokiAPITarget(address, port, LokiAPITarget.KeySet(ed25519Key, x25519Key))
}
}?.toSet() ?: setOf()
}
override fun setSnodePool(newValue: Set<LokiAPITarget>) {
val database = databaseHelper.writableDatabase
val snodePoolAsString = newValue.joinToString(", ") { snode ->
var string = "${snode.address}-${snode.port}"
val keySet = snode.publicKeySet
if (keySet != null) {
string += "-${keySet.ed25519Key}-${keySet.x25519Key}"
}
string
}
val row = wrap(mapOf(Companion.dummyKey to "dummy_key", snodePoolKey to snodePoolAsString))
database.insertOrUpdate(snodePoolCache, row, "${Companion.dummyKey} = ?", wrap("dummy_key"))
}
override fun getPaths(): List<List<LokiAPITarget>> {
val database = databaseHelper.readableDatabase
fun get(indexPath: String): LokiAPITarget? {
return database.get(pathCache, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor ->
val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode))
val components = snodeAsString.split("-")
val address = components[0]
val port = components.getOrNull(1)?.toIntOrNull()
val ed25519Key = components.getOrNull(2)
val x25519Key = components.getOrNull(3)
if (port != null && ed25519Key != null && x25519Key != null) {
LokiAPITarget(address, port, LokiAPITarget.KeySet(ed25519Key, x25519Key))
} else {
null
}
}
}
val path0Snode0 = get("0-0") ?: return listOf(); val path0Snode1 = get("0-1") ?: return listOf()
val path0Snode2 = get("0-2") ?: return listOf(); val path1Snode0 = get("1-0") ?: return listOf()
val path1Snode1 = get("1-1") ?: return listOf(); val path1Snode2 = get("1-2") ?: return listOf()
return listOf( listOf( path0Snode0, path0Snode1, path0Snode2 ), listOf( path1Snode0, path1Snode1, path1Snode2 ) )
}
fun clearPaths() {
val database = databaseHelper.writableDatabase
fun delete(indexPath: String) {
database.delete(pathCache, "${Companion.indexPath} = ?", wrap(indexPath))
}
delete("0-0"); delete("0-1")
delete("0-2"); delete("1-0")
delete("1-1"); delete("1-2")
}
override fun setPaths(newValue: List<List<LokiAPITarget>>) {
// FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this.
if (newValue.count() != 2) { return }
val path0 = newValue[0]
val path1 = newValue[1]
if (path0.count() != 3 || path1.count() != 3) { return }
Log.d("Loki", "Persisting onion request paths to database.")
val database = databaseHelper.writableDatabase
fun set(indexPath: String ,snode: LokiAPITarget) {
var snodeAsString = "${snode.address}-${snode.port}"
val keySet = snode.publicKeySet
if (keySet != null) {
snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}"
}
val row = wrap(mapOf(Companion.indexPath to indexPath, Companion.snode to snodeAsString))
database.insertOrUpdate(pathCache, row, "${Companion.indexPath} = ?", wrap(indexPath))
}
set("0-0", path0[0]); set("0-1", path0[1])
set("0-2", path0[2]); set("1-0", path1[0])
set("1-1", path1[1]); set("1-2", path1[2])
}
override fun getSwarmCache(hexEncodedPublicKey: String): Set<LokiAPITarget>? {
val database = databaseHelper.readableDatabase
return database.get(swarmCache, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) { cursor ->
@ -75,7 +170,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val address = components[0]
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
val x25519Key = components.getOrNull(3)?: return@mapNotNull null
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
LokiAPITarget(address, port, LokiAPITarget.KeySet(ed25519Key, x25519Key))
}
}?.toSet()

View File

@ -1,7 +1,12 @@
package org.thoughtcrime.securesms.loki.utilities
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.graphics.PointF
import android.graphics.Rect
import android.support.annotation.DimenRes
import android.view.View
fun View.contains(point: PointF): Boolean {
@ -13,4 +18,34 @@ val View.hitRect: Rect
val rect = Rect()
getHitRect(rect)
return rect
}
}
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
val layoutParams = this.layoutParams
val startSize = resources.getDimension(startSizeID)
val endSize = resources.getDimension(endSizeID)
val animation = ValueAnimator.ofObject(FloatEvaluator(), startSize, endSize)
animation.duration = animationDuration
animation.addUpdateListener { animator ->
val size = animator.animatedValue as Float
layoutParams.width = size.toInt()
layoutParams.height = size.toInt()
this.layoutParams = layoutParams
}
animation.start()
}
fun View.fadeIn(duration: Long = 150) {
visibility = View.VISIBLE
animate().setDuration(duration).alpha(1.0f).start()
}
fun View.fadeOut(duration: Long = 150) {
animate().setDuration(duration).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
visibility = View.GONE
}
})
}

View File

@ -96,13 +96,13 @@ class NewConversationButtonSetView : RelativeLayout {
fun expand() {
animateImageViewColorChange(R.color.new_conversation_button_collapsed_background, R.color.accent)
animateImageViewSizeChange(R.dimen.new_conversation_button_collapsed_size, R.dimen.new_conversation_button_expanded_size)
imageView.animateSizeChange(R.dimen.new_conversation_button_collapsed_size, R.dimen.new_conversation_button_expanded_size, animationDuration)
animateImageViewPositionChange(collapsedImageViewPosition, expandedImageViewPosition)
}
fun collapse() {
animateImageViewColorChange(R.color.accent, R.color.new_conversation_button_collapsed_background)
animateImageViewSizeChange(R.dimen.new_conversation_button_expanded_size, R.dimen.new_conversation_button_collapsed_size)
imageView.animateSizeChange(R.dimen.new_conversation_button_expanded_size, R.dimen.new_conversation_button_collapsed_size, animationDuration)
animateImageViewPositionChange(expandedImageViewPosition, collapsedImageViewPosition)
}
@ -119,21 +119,6 @@ class NewConversationButtonSetView : RelativeLayout {
animation.start()
}
private fun animateImageViewSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int) {
val layoutParams = imageView.layoutParams
val startSize = resources.getDimension(startSizeID)
val endSize = resources.getDimension(endSizeID)
val animation = ValueAnimator.ofObject(FloatEvaluator(), startSize, endSize)
animation.duration = animationDuration
animation.addUpdateListener { animator ->
val size = animator.animatedValue as Float
layoutParams.width = size.toInt()
layoutParams.height = size.toInt()
imageView.layoutParams = layoutParams
}
animation.start()
}
private fun animateImageViewPositionChange(startPosition: PointF, endPosition: PointF) {
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
animation.duration = animationDuration

View File

@ -0,0 +1,77 @@
package org.thoughtcrime.securesms.loki.views
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.support.v4.content.LocalBroadcastManager
import android.util.AttributeSet
import android.view.View
import network.loki.messenger.R
import org.whispersystems.signalservice.loki.api.onionrequests.OnionRequestAPI
class PathStatusView : View {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
constructor(context: Context) : super(context) {
initialize()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initialize()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initialize()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
initialize()
}
private fun initialize() {
update()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
registerObservers()
}
private fun registerObservers() {
val buildingPathsReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handleBuildingPathsEvent()
}
}
broadcastReceivers.add(buildingPathsReceiver)
LocalBroadcastManager.getInstance(context).registerReceiver(buildingPathsReceiver, IntentFilter("buildingPaths"))
val pathsBuiltReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
handlePathsBuiltEvent()
}
}
broadcastReceivers.add(pathsBuiltReceiver)
LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltReceiver, IntentFilter("pathsBuilt"))
}
override fun onDetachedFromWindow() {
for (receiver in broadcastReceivers) {
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
}
super.onDetachedFromWindow()
}
private fun handleBuildingPathsEvent() { update() }
private fun handlePathsBuiltEvent() { update() }
private fun update() {
if (OnionRequestAPI.paths.count() >= OnionRequestAPI.pathCount) {
setBackgroundResource(R.drawable.accent_dot)
} else {
setBackgroundResource(R.drawable.paths_building_dot)
}
}
}