Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ android {
applicationId 'ca.psiphon.conduit'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 76
versionName "2.0.5"
versionCode 77
versionName "2.0.6"
}
signingConfigs {
if (hasCustomKeystoreProperties) {
Expand Down
6 changes: 3 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"expo": {
"name": "conduit",
"slug": "conduit",
"version": "2.0.5",
"version": "2.0.6",
"orientation": "portrait",
"scheme": "ca.psiphon.conduit",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
"bundleIdentifier": "ca.psiphon.conduit",
"buildNumber": "27",
"buildNumber": "28",
"deploymentTarget": "15.1",
"icon": "./assets/images/conduit-launcher.png",
"infoPlist": {
Expand All @@ -19,7 +19,7 @@
},
"android": {
"package": "ca.psiphon.conduit",
"versionCode": 76,
"versionCode": 77,
"permissions": [
"android.permission.POST_NOTIFICATIONS",
"com.android.vending.BILLING"
Expand Down
4 changes: 2 additions & 2 deletions ios/conduit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.0.5</string>
<string>2.0.6</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand All @@ -33,7 +33,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>27</string>
<string>28</string>
<key>ExpoLocalization_supportsRTL</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application>
<service
Expand Down Expand Up @@ -29,5 +30,10 @@
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>

<provider
android:name="expo.modules.psiphontunnelcore.LoggingContentProvider"
android:authorities="${applicationId}.log"
android:exported="false" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -17,90 +17,161 @@
*/
package expo.modules.psiphontunnelcore

import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.util.Log
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.Future
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

object AppLogStore {
private const val APP_LOG_FILE_NAME = "app.log"
private const val APP_LOG_ARCHIVE_FILE_NAME = "app.log.1"
private const val MAX_LOG_FILE_BYTES = Constants.QUARTER_MB.toLong()
private val lock = Any()
private val timestampFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private const val TAG = "AppLogStore"
private const val AUTHORITY_SUFFIX = ".log"
private const val PATH_INSERT_LOGS = "insert"
private const val LEGACY_APP_LOG_DIRECTORY = "app_logs"
private const val LEGACY_APP_LOG_FILE_NAME = "app.log"
private const val LEGACY_APP_LOG_ARCHIVE_FILE_NAME = "app.log.1"
private const val MAX_RETRIES = 3
private const val DEFAULT_FLUSH_TIMEOUT_MS = 2_000L
private val retryDelaysMs = longArrayOf(100L, 500L, 1_000L)
private val retryableLogLevels = setOf(Log.ERROR, Log.WARN, Log.INFO)
private val executorService = Executors.newSingleThreadExecutor()

fun info(context: Context, tag: String, message: String) {
Log.i(tag, message)
append(context, "Info", tag, message)
log(context, tag, message, Log.INFO, null)
}

fun warn(context: Context, tag: String, message: String) {
Log.w(tag, message)
append(context, "Warning", tag, message)
log(context, tag, message, Log.WARN, null)
}

fun warn(context: Context, tag: String, message: String, error: Throwable) {
log(context, tag, message, Log.WARN, error)
}

fun error(context: Context, tag: String, message: String) {
Log.e(tag, message)
append(context, "Error", tag, message)
log(context, tag, message, Log.ERROR, null)
}

fun error(context: Context, tag: String, message: String, error: Throwable) {
log(context, tag, message, Log.ERROR, error)
}

fun allLogFiles(context: Context): List<File> {
val logDir = logDirectory(context)
return listOf(
File(logDir, APP_LOG_ARCHIVE_FILE_NAME),
File(logDir, APP_LOG_FILE_NAME),
).filter { it.exists() }
val dataRoot = Utils.dataRootDirectory(context)
val providerLogs = dataRoot.listFiles { _, name ->
name.startsWith(LoggingContentProvider.LOG_FILE_NAME) && !name.endsWith(".lck")
}?.toList().orEmpty()

val legacyLogDir = File(dataRoot, LEGACY_APP_LOG_DIRECTORY)
val legacyLogs = listOf(
File(legacyLogDir, LEGACY_APP_LOG_ARCHIVE_FILE_NAME),
File(legacyLogDir, LEGACY_APP_LOG_FILE_NAME),
)

return (providerLogs + legacyLogs).filter { it.exists() && it.isFile }
}

private fun append(context: Context, level: String, tag: String, message: String) {
synchronized(lock) {
val logDir = logDirectory(context)
rotateIfNeeded(logDir)
val payload = JSONObject()
.put("tag", tag)
.put("message", message)
.put("level", level)
.put("timestamp", rfc3339Timestamp(System.currentTimeMillis()))
.toString() + "\n"

FileOutputStream(File(logDir, APP_LOG_FILE_NAME), true).use { output ->
output.write(payload.toByteArray(StandardCharsets.UTF_8))
}
fun flush(timeoutMs: Long = DEFAULT_FLUSH_TIMEOUT_MS): Boolean {
val barrier: Future<*> = executorService.submit {}
return try {
barrier.get(timeoutMs, TimeUnit.MILLISECONDS)
true
} catch (error: TimeoutException) {
barrier.cancel(false)
Log.w(TAG, String.format(Locale.US, "Timed out flushing logs after %dms", timeoutMs))
false
} catch (error: InterruptedException) {
Thread.currentThread().interrupt()
barrier.cancel(false)
Log.w(TAG, "Interrupted while flushing logs", error)
false
} catch (error: Exception) {
Log.w(TAG, "Failed to flush logs", error)
false
}
}

private fun rotateIfNeeded(logDir: File) {
val current = File(logDir, APP_LOG_FILE_NAME)
if (!current.exists() || current.length() < MAX_LOG_FILE_BYTES) {
private fun log(context: Context, tag: String, message: String, level: Int, error: Throwable?) {
logcat(level, tag, message, error)

if (level == Log.DEBUG || level == Log.VERBOSE) {
return
}

val archived = File(logDir, APP_LOG_ARCHIVE_FILE_NAME)
if (archived.exists()) {
archived.delete()
val appContext = context.applicationContext
val uri = Uri.parse("content://${appContext.packageName}$AUTHORITY_SUFFIX/$PATH_INSERT_LOGS")
val values = ContentValues().apply {
put("tag", tag)
put("message", messageForFile(message, error))
put("level", level)
put("timestamp", System.currentTimeMillis())
}

executorService.execute {
insertWithRetry(appContext, uri, values, level, 0)
}
}

private fun logcat(level: Int, tag: String, message: String, error: Throwable?) {
when (level) {
Log.ERROR -> if (error == null) Log.e(tag, message) else Log.e(tag, message, error)
Log.WARN -> if (error == null) Log.w(tag, message) else Log.w(tag, message, error)
Log.INFO -> if (error == null) Log.i(tag, message) else Log.i(tag, message, error)
Log.DEBUG -> if (error == null) Log.d(tag, message) else Log.d(tag, message, error)
Log.VERBOSE -> if (error == null) Log.v(tag, message) else Log.v(tag, message, error)
else -> Log.println(level, tag, messageForFile(message, error))
}
current.renameTo(archived)
}

private fun logDirectory(context: Context): File {
val directory = File(Utils.dataRootDirectory(context), "app_logs")
if (!directory.exists()) {
directory.mkdirs()
private fun messageForFile(message: String, error: Throwable?): String {
return if (error == null) {
message
} else {
String.format(Locale.US, "%s: %s", message, error)
}
return directory
}

private fun rfc3339Timestamp(timeMillis: Long): String {
synchronized(timestampFormatter) {
return timestampFormatter.format(Date(timeMillis))
private fun insertWithRetry(
context: Context,
uri: Uri,
values: ContentValues,
level: Int,
attempt: Int,
) {
var currentAttempt = attempt
while (true) {
try {
val result = context.contentResolver.insert(uri, values)
if (result != null) {
return
}
throw IllegalStateException("Insert returned null result")
} catch (error: Exception) {
Log.e(TAG, String.format(Locale.US, "Insert failed (attempt %d): %s", currentAttempt + 1, error.message))
if (currentAttempt >= MAX_RETRIES || !retryableLogLevels.contains(level)) {
if (level >= Log.ERROR) {
Log.e(values.getAsString("tag"), values.getAsString("message"))
}
return
}

val delay = retryDelaysMs.getOrElse(currentAttempt) { retryDelaysMs.last() }
currentAttempt += 1
try {
Thread.sleep(delay)
} catch (interrupted: InterruptedException) {
Thread.currentThread().interrupt()
if (level >= Log.ERROR) {
Log.e(values.getAsString("tag"), values.getAsString("message"))
}
return
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import ca.psiphon.conduit.nativemodule.IConduitClientCallback
import ca.psiphon.conduit.nativemodule.IConduitService

Expand All @@ -38,9 +37,21 @@ class ConduitServiceInteractor(private val context: Context) {
private var conduitService: IConduitService? = null
private var callback: ((String, Bundle) -> Unit)? = null

private fun logInfo(message: String) {
AppLogStore.info(context, TAG, message)
}

private fun logWarn(message: String) {
AppLogStore.warn(context, TAG, message)
}

private fun logError(message: String, error: Throwable) {
AppLogStore.error(context, TAG, message, error)
}

private val clientCallback = object : IConduitClientCallback.Stub() {
override fun onProxyStateUpdated(proxyStateBundle: Bundle) {
Log.i(TAG, "Received proxy state callback: ${proxyStateBundle.getString("status")}")
logInfo("Received proxy state callback: ${proxyStateBundle.getString("status")}")
callback?.invoke("proxyState", proxyStateBundle)
}

Expand All @@ -55,13 +66,13 @@ class ConduitServiceInteractor(private val context: Context) {

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.i(TAG, "Connected to InproxyForegroundService")
logInfo("Connected to InproxyForegroundService")
conduitService = IConduitService.Stub.asInterface(service)
registerClientIfReady("service connected")
}

override fun onServiceDisconnected(name: ComponentName?) {
Log.i(TAG, "Disconnected from InproxyForegroundService")
logInfo("Disconnected from InproxyForegroundService")
conduitService = null
isServiceBound = false

Expand All @@ -76,18 +87,18 @@ class ConduitServiceInteractor(private val context: Context) {
fun onStart(callback: (String, Bundle) -> Unit) {
this.callback = callback
isStopped = false
Log.i(TAG, "Interactor start")
logInfo("Interactor start")
bindService()
registerClientIfReady("observer start")
}

fun onStop() {
isStopped = true
Log.i(TAG, "Interactor stop")
logInfo("Interactor stop")
try {
conduitService?.unregisterClient(clientCallback)
} catch (error: RemoteException) {
Log.e(TAG, "Failed to unregister inproxy client", error)
logError("Failed to unregister inproxy client", error)
}

if (isServiceBound) {
Expand Down Expand Up @@ -124,23 +135,23 @@ class ConduitServiceInteractor(private val context: Context) {
}
try {
service.registerClient(clientCallback)
Log.i(TAG, "Registered inproxy client callback: $reason")
logInfo("Registered inproxy client callback: $reason")
emitPendingProxyError()
} catch (error: RemoteException) {
Log.e(TAG, "Failed to register inproxy client: $reason", error)
logError("Failed to register inproxy client: $reason", error)
}
}

private fun bindService() {
if (isServiceBound) {
return
}
Log.i(TAG, "Binding to InproxyForegroundService")
logInfo("Binding to InproxyForegroundService")
val intent = Intent(context, InproxyForegroundService::class.java)
val bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
isServiceBound = bound
if (!bound) {
Log.w(TAG, "bindService returned false")
logWarn("bindService returned false")
}
}

Expand Down
Loading
Loading