/******************************************************************************* * * * Copyright (C) 2017 by Max Lv * * Copyright (C) 2017 by Mygod Studio * * * * 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 . * * * *******************************************************************************/ 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.Looper import android.support.annotation.RequiresApi import android.support.v4.os.UserManagerCompat import android.support.v7.app.AppCompatDelegate import android.util.Log import android.widget.Toast import com.evernote.android.job.JobConstants import com.evernote.android.job.JobManager import com.evernote.android.job.JobManagerCreateException 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 class App : Application() { companion object { lateinit var app: App private const val TAG = "ShadowsocksApplication" } 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) } private val exceptionParser by lazy { StandardExceptionParser(this, null) } val info: PackageInfo by lazy { getPackageInfo(packageName) } fun getPackageInfo(packageName: String) = 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("${exceptionParser.getDescription(thread.name, t)} - ${t.message}") .setFatal(false) .build()) t.printStackTrace() } override fun onCreate() { super.onCreate() app = this if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR") AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 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") } try { JobManager.create(deviceContext).addJobCreator(AclSyncJob) } catch (e: JobManagerCreateException) { Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show() app.track(e) } // handle data restored if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update() TcpFastOpen.enabledAsync(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) } updateNotificationChannels() } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateNotificationChannels() } private fun updateNotificationChannels() { 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 } } fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit): BroadcastReceiver { val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED) filter.addAction(Intent.ACTION_PACKAGE_REMOVED) filter.addDataScheme("package") val result = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return callback() if (onetime) app.unregisterReceiver(this) } } app.registerReceiver(result, filter) return result } }