To do direct boot, important files need to be moved to device storage (i.e. not as safe storage). The only thing sensitive is user credentials (server settings in each profile) and the config file passed to libss-*.so. In order to implement direct boot, at least the active profile has to be moved/copied to device storage. This commit keeps profile information (and temporary data used by ProfileConfigFragment) and Shadowsocks config in credential storage and move everything else to device storage. In order to make direct boot work, active profile is also copied into device storage and Shadowsocks config file is created in device storage during direct boot. Cleanup of those information is also implemented. Fixes #1457. NB: * `android:directBootAware` can't be used on application element. That seems to be used by system apps. * Works fine with multi-user; * Old GSM files are not migrated (see App.kt) to device storage.
236 lines
11 KiB
Kotlin
236 lines
11 KiB
Kotlin
/*******************************************************************************
|
|
* *
|
|
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
|
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* This program is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
* *
|
|
*******************************************************************************/
|
|
|
|
package com.github.shadowsocks
|
|
|
|
import android.app.Application
|
|
import android.app.NotificationChannel
|
|
import android.app.NotificationManager
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.IntentFilter
|
|
import android.content.pm.PackageInfo
|
|
import android.content.pm.PackageManager
|
|
import android.content.res.Configuration
|
|
import android.os.Build
|
|
import android.os.Handler
|
|
import android.os.LocaleList
|
|
import android.os.Looper
|
|
import android.support.annotation.RequiresApi
|
|
import android.support.v4.os.UserManagerCompat
|
|
import android.support.v7.app.AppCompatDelegate
|
|
import android.util.Log
|
|
import com.evernote.android.job.JobConstants
|
|
import com.evernote.android.job.JobManager
|
|
import com.github.shadowsocks.acl.Acl
|
|
import com.github.shadowsocks.acl.AclSyncJob
|
|
import com.github.shadowsocks.bg.BaseService
|
|
import com.github.shadowsocks.database.Profile
|
|
import com.github.shadowsocks.database.ProfileManager
|
|
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
|
|
import com.github.shadowsocks.preference.DataStore
|
|
import com.github.shadowsocks.preference.IconListPreference
|
|
import com.github.shadowsocks.utils.*
|
|
import com.google.android.gms.analytics.GoogleAnalytics
|
|
import com.google.android.gms.analytics.HitBuilders
|
|
import com.google.android.gms.analytics.StandardExceptionParser
|
|
import com.google.android.gms.analytics.Tracker
|
|
import com.google.firebase.FirebaseApp
|
|
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
|
import com.j256.ormlite.logger.LocalLog
|
|
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat
|
|
import java.io.File
|
|
import java.io.IOException
|
|
import java.util.*
|
|
|
|
class App : Application() {
|
|
companion object {
|
|
lateinit var app: App
|
|
private const val TAG = "ShadowsocksApplication"
|
|
|
|
// The ones in Locale doesn't have script included
|
|
private val SIMPLIFIED_CHINESE by lazy {
|
|
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hans-CN") else Locale.SIMPLIFIED_CHINESE
|
|
}
|
|
private val TRADITIONAL_CHINESE by lazy {
|
|
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hant-TW") else Locale.TRADITIONAL_CHINESE
|
|
}
|
|
}
|
|
|
|
val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
val deviceContext: Context by lazy { if (Build.VERSION.SDK_INT < 24) this else DeviceContext(this) }
|
|
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
|
|
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) }
|
|
val info: PackageInfo by lazy { packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) }
|
|
|
|
fun startService() {
|
|
val intent = Intent(this, BaseService.serviceClass.java)
|
|
if (Build.VERSION.SDK_INT >= 26) startForegroundService(intent) else startService(intent)
|
|
}
|
|
fun reloadService() = sendBroadcast(Intent(Action.RELOAD))
|
|
fun stopService() = sendBroadcast(Intent(Action.CLOSE))
|
|
|
|
val currentProfile: Profile? get() =
|
|
if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId)
|
|
|
|
fun switchProfile(id: Int): Profile {
|
|
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
|
|
DataStore.profileId = result.id
|
|
return result
|
|
}
|
|
|
|
// send event
|
|
fun track(category: String, action: String) = tracker.send(HitBuilders.EventBuilder()
|
|
.setCategory(category)
|
|
.setAction(action)
|
|
.setLabel(BuildConfig.VERSION_NAME)
|
|
.build())
|
|
fun track(t: Throwable) = track(Thread.currentThread(), t)
|
|
fun track(thread: Thread, t: Throwable) {
|
|
tracker.send(HitBuilders.ExceptionBuilder()
|
|
.setDescription(StandardExceptionParser(this, null).getDescription(thread.name, t))
|
|
.setFatal(false)
|
|
.build())
|
|
t.printStackTrace()
|
|
}
|
|
|
|
private fun checkChineseLocale(config: Configuration) {
|
|
fun check(locale: Locale): Locale? {
|
|
if (locale.language != "zh") return null
|
|
when (locale.country) { "CN", "TW" -> return null }
|
|
if (Build.VERSION.SDK_INT >= 21) when (locale.script) {
|
|
"Hans" -> return SIMPLIFIED_CHINESE
|
|
"Hant" -> return TRADITIONAL_CHINESE
|
|
else -> Log.w(TAG, "Unknown zh locale script: ${locale.script}. Falling back to trying countries...")
|
|
}
|
|
when (locale.country) {
|
|
"SG" -> return SIMPLIFIED_CHINESE
|
|
"HK", "MO" -> return TRADITIONAL_CHINESE
|
|
}
|
|
Log.w(TAG, "Unknown zh locale: %s. Falling back to zh-Hans-CN..."
|
|
.format(Locale.ENGLISH, if (Build.VERSION.SDK_INT >= 21) locale.toLanguageTag() else locale))
|
|
return SIMPLIFIED_CHINESE
|
|
}
|
|
if (Build.VERSION.SDK_INT >= 24) @RequiresApi(24) {
|
|
val localeList = config.locales
|
|
var changed = false
|
|
val newList = Array<Locale>(localeList.size(), { i ->
|
|
val locale = localeList[i]
|
|
val newLocale = check(locale)
|
|
if (newLocale == null) locale else {
|
|
changed = true
|
|
newLocale
|
|
}
|
|
})
|
|
if (changed) {
|
|
val newConfig = Configuration(config)
|
|
newConfig.locales = LocaleList(*(newList.distinct().toTypedArray()))
|
|
val res = resources
|
|
res.updateConfiguration(newConfig, res.displayMetrics)
|
|
}
|
|
} else {
|
|
@Suppress("DEPRECATION")
|
|
val newLocale = check(config.locale)
|
|
if (newLocale != null) {
|
|
val newConfig = Configuration(config)
|
|
@Suppress("DEPRECATION")
|
|
newConfig.locale = newLocale
|
|
val res = resources
|
|
res.updateConfiguration(newConfig, res.displayMetrics)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
app = this
|
|
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
|
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
|
checkChineseLocale(resources.configuration)
|
|
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
|
|
BottomSheetPreferenceDialogFragment::class.java)
|
|
|
|
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
|
deviceContext.moveDatabaseFrom(this, Key.DB_PUBLIC)
|
|
deviceContext.moveDatabaseFrom(this, JobConstants.DATABASE_NAME)
|
|
deviceContext.moveSharedPreferencesFrom(this, JobConstants.PREF_FILE_NAME)
|
|
val old = Acl.getFile(Acl.CUSTOM_RULES, this)
|
|
if (old.canRead()) {
|
|
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
|
|
old.delete()
|
|
}
|
|
}
|
|
|
|
FirebaseApp.initializeApp(deviceContext)
|
|
remoteConfig.setDefaults(R.xml.default_configs)
|
|
remoteConfig.fetch().addOnCompleteListener {
|
|
if (it.isSuccessful) remoteConfig.activateFetched() else Log.e(TAG, "Failed to fetch config")
|
|
}
|
|
JobManager.create(deviceContext).addJobCreator(AclSyncJob)
|
|
|
|
// handle data restored
|
|
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update()
|
|
TcpFastOpen.enabled(DataStore.publicStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
|
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
|
|
val assetManager = assets
|
|
for (dir in arrayOf("acl", "overture"))
|
|
try {
|
|
for (file in assetManager.list(dir)) assetManager.open(dir + '/' + file).use { input ->
|
|
File(deviceContext.filesDir, file).outputStream().use { output -> input.copyTo(output) }
|
|
}
|
|
} catch (e: IOException) {
|
|
Log.e(TAG, e.message)
|
|
app.track(e)
|
|
}
|
|
DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
|
val nm = getSystemService(NotificationManager::class.java)
|
|
nm.createNotificationChannels(listOf(
|
|
NotificationChannel("service-vpn", getText(R.string.service_vpn),
|
|
NotificationManager.IMPORTANCE_LOW),
|
|
NotificationChannel("service-proxy", getText(R.string.service_proxy),
|
|
NotificationManager.IMPORTANCE_LOW),
|
|
NotificationChannel("service-transproxy", getText(R.string.service_transproxy),
|
|
NotificationManager.IMPORTANCE_LOW)))
|
|
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
|
|
}
|
|
}
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
super.onConfigurationChanged(newConfig)
|
|
checkChineseLocale(newConfig)
|
|
}
|
|
|
|
fun listenForPackageChanges(callback: () -> Unit): BroadcastReceiver {
|
|
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
|
|
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
filter.addDataScheme("package")
|
|
val result = broadcastReceiver { _, intent ->
|
|
if (intent.action != Intent.ACTION_PACKAGE_REMOVED ||
|
|
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) callback()
|
|
}
|
|
app.registerReceiver(result, filter)
|
|
return result
|
|
}
|
|
}
|