/* * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.fdroid.fdroid; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManager.RunningAppProcessInfo; import android.app.Application; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.StrictMode; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiskCache; import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; import org.acra.ACRA; import org.acra.ReportingInteractionMode; import org.acra.annotation.ReportsCrashes; import org.apache.commons.net.util.SubnetUtils; import org.fdroid.fdroid.Preferences.ChangeListener; import org.fdroid.fdroid.Preferences.Theme; import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.InstalledAppProviderService; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.net.IconDownloader; import org.fdroid.fdroid.net.WifiStateChangeService; import java.net.URL; import java.net.URLStreamHandler; import java.net.URLStreamHandlerFactory; import java.security.Security; import java.util.List; import java.util.Locale; import info.guardianproject.netcipher.NetCipher; import info.guardianproject.netcipher.proxy.OrbotHelper; import sun.net.www.protocol.bluetooth.Handler; @ReportsCrashes(mailTo = "reports@f-droid.org", mode = ReportingInteractionMode.DIALOG, reportDialogClass = org.fdroid.fdroid.acra.CrashReportActivity.class, reportSenderFactoryClasses = org.fdroid.fdroid.acra.CrashReportSenderFactory.class ) public class FDroidApp extends Application { private static final String TAG = "FDroidApp"; public static final String SYSTEM_DIR_NAME = Environment.getRootDirectory().getAbsolutePath(); private static Locale locale; // for the local repo on this device, all static since there is only one public static volatile int port; public static volatile String ipAddressString; public static volatile SubnetUtils.SubnetInfo subnetInfo; public static volatile String ssid; public static volatile String bssid; public static volatile Repo repo = new Repo(); // Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle. private static final org.spongycastle.jce.provider.BouncyCastleProvider SPONGYCASTLE_PROVIDER; @SuppressWarnings("unused") BluetoothAdapter bluetoothAdapter; static { SPONGYCASTLE_PROVIDER = new org.spongycastle.jce.provider.BouncyCastleProvider(); enableSpongyCastle(); } private static Theme curTheme = Theme.light; public void reloadTheme() { curTheme = Preferences.get().getTheme(); } public void applyTheme(Activity activity) { activity.setTheme(getCurThemeResId()); } public static int getCurThemeResId() { switch (curTheme) { case light: return R.style.AppThemeLight; case dark: return R.style.AppThemeDark; case night: return R.style.AppThemeNight; default: return R.style.AppThemeLight; } } public void applyDialogTheme(Activity activity) { activity.setTheme(getCurDialogThemeResId()); } private static int getCurDialogThemeResId() { switch (curTheme) { case light: return R.style.MinWithDialogBaseThemeLight; case dark: return R.style.MinWithDialogBaseThemeDark; case night: return R.style.MinWithDialogBaseThemeDark; default: return R.style.MinWithDialogBaseThemeLight; } } public static void enableSpongyCastle() { Security.addProvider(SPONGYCASTLE_PROVIDER); } public static void enableSpongyCastleOnLollipop() { if (Build.VERSION.SDK_INT == 21) { Security.addProvider(SPONGYCASTLE_PROVIDER); } } public static void disableSpongyCastleOnLollipop() { if (Build.VERSION.SDK_INT == 21) { Security.removeProvider(SPONGYCASTLE_PROVIDER.getName()); } } /** * Initialize the settings needed to run a local swap repo. This should * only ever be called in {@link org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread}, * after the single init call in {@link FDroidApp#onCreate()}. */ public static void initWifiSettings() { port = 8888; ipAddressString = null; subnetInfo = new SubnetUtils("0.0.0.0/32").getInfo(); ssid = ""; bssid = ""; repo = new Repo(); } public void updateLanguage() { Context ctx = getBaseContext(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); String lang = prefs.getString(Preferences.PREF_LANGUAGE, ""); locale = Utils.getLocaleFromAndroidLangTag(lang); applyLanguage(); } private void applyLanguage() { Context ctx = getBaseContext(); Configuration cfg = new Configuration(); cfg.locale = locale == null ? Locale.getDefault() : locale; ctx.getResources().updateConfiguration(cfg, null); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); applyLanguage(); } @Override public void onCreate() { super.onCreate(); if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectAll() .penaltyLog() .build()); } updateLanguage(); ACRA.init(this); if (isAcraProcess()) { return; } PRNGFixes.apply(); Preferences.setup(this); curTheme = Preferences.get().getTheme(); Preferences.get().configureProxy(); InstalledAppProviderService.compareToPackageManager(this); // If the user changes the preference to do with filtering rooted apps, // it is easier to just notify a change in the app provider, // so that the newly updated list will correctly filter relevant apps. Preferences.get().registerAppsRequiringRootChangeListener(new Preferences.ChangeListener() { @Override public void onPreferenceChange() { getContentResolver().notifyChange(AppProvider.getContentUri(), null); } }); // If the user changes the preference to do with filtering anti-feature apps, // it is easier to just notify a change in the app provider, // so that the newly updated list will correctly filter relevant apps. Preferences.get().registerAppsRequiringAntiFeaturesChangeListener(new Preferences.ChangeListener() { @Override public void onPreferenceChange() { getContentResolver().notifyChange(AppProvider.getContentUri(), null); } }); // This is added so that the bluetooth:// scheme we use for URLs the BluetoothDownloader // understands is not treated as invalid by the java.net.URL class. The actual Handler does // nothing, but its presence is enough. URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() { @Override public URLStreamHandler createURLStreamHandler(String protocol) { return TextUtils.equals(protocol, "bluetooth") ? new Handler() : null; } }); final Context context = this; Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() { @Override public void onPreferenceChange() { AppProvider.Helper.calcSuggestedApks(context); } }); CleanCacheService.schedule(this); UpdateService.schedule(getApplicationContext()); bluetoothAdapter = getBluetoothAdapter(); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()) .imageDownloader(new IconDownloader(getApplicationContext())) .diskCache(new LimitedAgeDiskCache( Utils.getIconsCacheDir(this), null, new FileNameGenerator() { @Override public String generate(String imageUri) { return imageUri.substring( imageUri.lastIndexOf('/') + 1); } }, // 30 days in secs: 30*24*60*60 = 2592000 2592000) ) .threadPoolSize(4) .threadPriority(Thread.NORM_PRIORITY - 2) // Default is NORM_PRIORITY - 1 .build(); ImageLoader.getInstance().init(config); FDroidApp.initWifiSettings(); startService(new Intent(this, WifiStateChangeService.class)); // if the HTTPS pref changes, then update all affected things Preferences.get().registerLocalRepoHttpsListeners(new ChangeListener() { @Override public void onPreferenceChange() { startService(new Intent(FDroidApp.this, WifiStateChangeService.class)); } }); configureTor(Preferences.get().isTorEnabled()); if (Preferences.get().isKeepingInstallHistory()) { InstallHistoryService.register(this); } String packageName = getString(R.string.install_history_reader_packageName); String unset = getString(R.string.install_history_reader_packageName_UNSET); if (!TextUtils.equals(packageName, unset)) { int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; if (Build.VERSION.SDK_INT >= 19) { modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; } grantUriPermission(packageName, InstallHistoryService.LOG_URI, modeFlags); } } /** * Asks if the current process is "org.fdroid.fdroid:acra". * * This is helpful for bailing out of the {@link FDroidApp#onCreate} method early, preventing * problems that arise from executing the code twice. This happens due to the `android:process` * statement in AndroidManifest.xml causes another process to be created to run * {@link org.fdroid.fdroid.acra.CrashReportActivity}. This was causing lots of things to be * started/run twice including {@link CleanCacheService} and {@link WifiStateChangeService}. * * Note that it is not perfect, because some devices seem to not provide a list of running app * processes when asked. In such situations, F-Droid may regress to the behaviour where some * services may run twice and thus cause weirdness or slowness. However that is probably better * for end users than experiencing a deterministic crash every time F-Droid is started. */ private boolean isAcraProcess() { ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE); List<RunningAppProcessInfo> processes = manager.getRunningAppProcesses(); if (processes == null) { return false; } int pid = android.os.Process.myPid(); for (RunningAppProcessInfo processInfo : processes) { if (processInfo.pid == pid && "org.fdroid.fdroid:acra".equals(processInfo.processName)) { return true; } } return false; } @TargetApi(18) private BluetoothAdapter getBluetoothAdapter() { // to use the new, recommended way of getting the adapter // http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html if (Build.VERSION.SDK_INT < 18) { return BluetoothAdapter.getDefaultAdapter(); } return ((BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter(); } public void sendViaBluetooth(Activity activity, int resultCode, String packageName) { if (resultCode == Activity.RESULT_CANCELED) { return; } String bluetoothPackageName = null; String className = null; boolean found = false; Intent sendBt = null; try { PackageManager pm = getPackageManager(); ApplicationInfo appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); sendBt = new Intent(Intent.ACTION_SEND); // The APK type is blocked by stock Android, so use zip // sendBt.setType("application/vnd.android.package-archive"); sendBt.setType("application/zip"); sendBt.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + appInfo.publicSourceDir)); // not all devices have the same Bluetooth Activities, so // let's find it for (ResolveInfo info : pm.queryIntentActivities(sendBt, 0)) { bluetoothPackageName = info.activityInfo.packageName; if ("com.android.bluetooth".equals(bluetoothPackageName) || "com.mediatek.bluetooth".equals(bluetoothPackageName)) { className = info.activityInfo.name; found = true; break; } } } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Could not get application info to send via bluetooth", e); found = false; } if (sendBt != null) { if (found) { sendBt.setClassName(bluetoothPackageName, className); activity.startActivity(sendBt); } else { Toast.makeText(this, R.string.bluetooth_activity_not_found, Toast.LENGTH_SHORT).show(); activity.startActivity(Intent.createChooser(sendBt, getString(R.string.choose_bt_send))); } } } private static boolean useTor; /** * Set the proxy settings based on whether Tor should be enabled or not. */ private static void configureTor(boolean enabled) { useTor = enabled; if (useTor) { NetCipher.useTor(); } else { NetCipher.clearProxy(); } } public static void checkStartTor(Context context) { if (useTor) { OrbotHelper.requestStartTor(context); } } public static boolean isUsingTor() { return useTor; } }