// Copyright 2008 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.android.stardroid; import android.app.Application; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; import com.google.android.stardroid.layers.LayerManager; import com.google.android.stardroid.util.Analytics; import com.google.android.stardroid.util.AnalyticsInterface.Slice; import com.google.android.stardroid.util.MiscUtil; import com.google.android.stardroid.util.PreferenceChangeAnalyticsTracker; import com.google.android.stardroid.views.PreferencesButton; import java.util.Calendar; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import javax.inject.Inject; /** * The main Stardroid Application class. * * @author John Taylor */ public class StardroidApplication extends Application { private static final String TAG = MiscUtil.getTag(StardroidApplication.class); private static final String PREVIOUS_APP_VERSION_PREF = "previous_app_version"; private static final String NONE = "Clean install"; private static final String UNKNOWN = "Unknown previous version"; @Inject SharedPreferences preferences; // We keep a reference to this just to start it initializing. @Inject LayerManager layerManager; @Inject static ExecutorService backgroundExecutor; @Inject Analytics analytics; @Inject SensorManager sensorManager; // We need to maintain references to this object to keep it from // getting gc'd. @Inject PreferenceChangeAnalyticsTracker preferenceChangeAnalyticsTracker; private ApplicationComponent component; @Override public void onCreate() { Log.d(TAG, "StardroidApplication: onCreate"); super.onCreate(); component = DaggerApplicationComponent.builder() .applicationModule(new ApplicationModule(this)) .build(); component.inject(this); Log.i(TAG, "OS Version: " + android.os.Build.VERSION.RELEASE + "(" + android.os.Build.VERSION.SDK_INT + ")"); String versionName = getVersionName(); Log.i(TAG, "Sky Map version " + versionName + " build " + getVersion()); // This populates the default values from the preferences XML file. See // {@link DefaultValues} for more details. PreferenceManager.setDefaultValues(this, R.xml.preference_screen, false); setUpAnalytics(versionName); performFeatureCheck(); Log.d(TAG, "StardroidApplication: -onCreate"); } public ApplicationComponent getApplicationComponent() { return component; } private void setUpAnalytics(String versionName) { analytics.setCustomVar(Slice.ANDROID_OS, Integer.toString(Build.VERSION.SDK_INT)); analytics.setCustomVar(Slice.SKYMAP_VERSION, versionName); analytics.setCustomVar(Slice.DEVICE_NAME, android.os.Build.MODEL); analytics.setEnabled(preferences.getBoolean(Analytics.PREF_KEY, true)); // Ugly hack since this isn't injectable PreferencesButton.setAnalytics(analytics); String previousVersion = preferences.getString(PREVIOUS_APP_VERSION_PREF, NONE); boolean newUser = false; if (previousVersion.equals(NONE)) { // It's possible a previous version exists, it's just that it wasn't a recent enough // version to have set PREVIOUS_APP_VERSION_PREF. If so, we should see that the TOS // have been accepted. String oldPreviousVersionKey = "read_tos"; if (preferences.contains(oldPreviousVersionKey)) { previousVersion = UNKNOWN; } else { // Best guess that this is the first every run of a new user. // Could also be someone with a new device. newUser = true; } } analytics.setCustomVar(Slice.NEW_USER, Boolean.toString(newUser)); analytics.trackPageView(Analytics.APPLICATION_CREATE); preferences.edit().putString(PREVIOUS_APP_VERSION_PREF, versionName).commit(); if (!previousVersion.equals(versionName)) { // It's either an upgrade or a new installation Log.d(TAG, "New installation: version " + versionName); analytics.trackEvent(Analytics.INSTALL_CATEGORY, Analytics.INSTALL_EVENT + versionName, Analytics.PREVIOUS_VERSION + previousVersion, 1); } // It will be interesting to see *when* people use Sky Map. analytics.trackEvent( Analytics.GENERAL_CATEGORY, Analytics.START_HOUR, Integer.toString(Calendar.getInstance().get(Calendar.HOUR_OF_DAY)) + 'h', 0); preferences.registerOnSharedPreferenceChangeListener(preferenceChangeAnalyticsTracker); } @Override public void onTerminate() { super.onTerminate(); analytics.setEnabled(false); } /** * Returns the version string for Sky Map. */ public String getVersionName() { // TODO(jontayler): update to use the info created by gradle. PackageManager packageManager = getPackageManager(); try { PackageInfo info = packageManager.getPackageInfo(this.getPackageName(), 0); return info.versionName; } catch (NameNotFoundException e) { Log.e(TAG, "Unable to obtain package info"); return "Unknown"; } } /** * Returns the build number for Sky Map. */ public int getVersion() { PackageManager packageManager = getPackageManager(); try { PackageInfo info = packageManager.getPackageInfo(this.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { Log.e(TAG, "Unable to obtain package info"); return -1; } } /** * Schedules this runnable to run as soon as possible on a background * thread. */ // TODO(johntaylor): the idea, and I'm not sure yet whether it's a good one, // is to centralize the management of background threads so we don't have // them scattered all over the app. We can then control how many threads // are spawned, perhaps having a VIP service for extra important runnables // that we'd prefer not to queue, as well as providing convenience functions // to facilitate callbacks on the UI thread. public static void runInBackground(Runnable runnable) { backgroundExecutor.submit(runnable); } /** * Returns either the name of the sensor or a string version of the sensor type id, depending * on the supported OS level along with some context. */ public static String getSafeNameForSensor(Sensor sensor) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { return "Sensor type: " + sensor.getStringType() + ": " + sensor.getType(); } else { return "Sensor type: " + sensor.getType(); } } /** * Check what features are available to this phone and report back to analytics * so we can judge when to add/drop support. */ private void performFeatureCheck() { if (sensorManager == null) { Log.e(TAG, "No sensor manager"); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_AVAILABILITY, "No Sensor Manager", 0); } // Minimum requirements if (hasDefaultSensor(Sensor.TYPE_ACCELEROMETER)) { if (hasDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)) { Log.i(TAG, "Minimal sensors available"); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_AVAILABILITY, "Minimal Sensors: Yes", 1); } else { Log.e(TAG, "No magnetic field sensor"); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_AVAILABILITY, "No Mag Field Sensor", 0); } } else { if (hasDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)) { Log.e(TAG, "No accelerometer"); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_AVAILABILITY, "No Accel Sensor", 0); } else { Log.e(TAG, "No magnetic field sensor or accelerometer"); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_AVAILABILITY, "No Mag Field/Accel Sensors", 0); } } // Check for a particularly strange combo - it would be weird to have a rotation sensor // but no accelerometer or magnetic field sensor boolean hasRotationSensor = false; if (hasDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)) { if (hasDefaultSensor(Sensor.TYPE_ACCELEROMETER) && hasDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) && hasDefaultSensor(Sensor.TYPE_GYROSCOPE)) { hasRotationSensor = true; analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.ROT_SENSOR_AVAILABILITY, "OK - All Sensors", 1); } else if (hasDefaultSensor(Sensor.TYPE_ACCELEROMETER) && hasDefaultSensor( Sensor.TYPE_MAGNETIC_FIELD)) { // Even though it allegedly has the rotation vector sensor too many gyro-less phones // lie about this, so put these devices on the 'classic' sensor code for now. hasRotationSensor = false; analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.ROT_SENSOR_AVAILABILITY, "Disabled - No gyro", 1); } else { analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.ROT_SENSOR_AVAILABILITY, "Disabled - Missing Mag/Accel", 0); } } else { analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.ROT_SENSOR_AVAILABILITY, "No rotation", 0); } // Enable Gyro if available and user hasn't already disabled it. if (!preferences.contains(ApplicationConstants.SHARED_PREFERENCE_DISABLE_GYRO)) { preferences.edit().putBoolean( ApplicationConstants.SHARED_PREFERENCE_DISABLE_GYRO, !hasRotationSensor).apply(); } // Do we at least have defaults for the main ones? int[] importantSensorTypes = {Sensor.TYPE_ACCELEROMETER, Sensor.TYPE_GYROSCOPE, Sensor.TYPE_MAGNETIC_FIELD, Sensor.TYPE_LIGHT, Sensor.TYPE_ROTATION_VECTOR, Sensor.TYPE_ORIENTATION}; for (int sensorType : importantSensorTypes) { if (hasDefaultSensor(sensorType)) { Log.i(TAG, "No sensor of type " + sensorType); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_TYPE + sensorType, "Sensor Absent", 0); } else { Log.i(TAG, "Sensor present of type " + sensorType); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_TYPE + sensorType, "Sensor Present", 1); } } // Lastly a dump of all the sensors. Log.d(TAG, "All sensors:"); List<Sensor> allSensors = sensorManager.getSensorList(Sensor.TYPE_ALL); Set<String> sensorTypes = new HashSet<>(); for (Sensor sensor : allSensors) { Log.i(TAG, sensor.getName()); sensorTypes.add(getSafeNameForSensor(sensor)); } Log.d(TAG, "All sensors summary:"); for (String sensorType : sensorTypes) { Log.i(TAG, sensorType); analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_NAME, sensorType, 1); } } private boolean hasDefaultSensor(int sensorType) { if (sensorManager == null) { return false; } Sensor sensor = sensorManager.getDefaultSensor(sensorType); if (sensor == null) { return false; } SensorEventListener dummy = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { // Nothing } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // Nothing } }; boolean success = sensorManager.registerListener( dummy, sensor, SensorManager.SENSOR_DELAY_UI); if (!success) { analytics.trackEvent( Analytics.SENSOR_CATEGORY, Analytics.SENSOR_LIAR, getSafeNameForSensor(sensor), 1); } sensorManager.unregisterListener(dummy); return success; } }