From bafb078d9cdec28c16553de479e413229e7bcad2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 9 Jul 2016 18:34:42 +0800 Subject: [PATCH] AnalyticsService: send anonymous usage information to GA This library receive usage informations from system processes and system applications and send these informations anonymously to Google Analytics servers. --- Service/Android.mk | 24 ++ Service/AndroidManifest.xml | 48 +++ Service/proguard.flags | 3 + Service/res/values/analytics.xml | 24 ++ Service/res/values/strings.xml | 19 + .../android_x86/analytics/AnalyticsService.java | 357 +++++++++++++++++ .../org/android_x86/analytics/BatteryState.java | 132 +++++++ .../analytics/BootCompletedReceiver.java | 80 ++++ Service/src/org/android_x86/analytics/Fields.java | 76 ++++ .../analytics/ImmortalIntentService.java | 139 +++++++ .../src/org/android_x86/analytics/LogHelper.java | 169 ++++++++ .../src/org/android_x86/analytics/PowerStats.java | 114 ++++++ Service/src/org/android_x86/analytics/Util.java | 439 +++++++++++++++++++++ 13 files changed, 1624 insertions(+) create mode 100644 Service/Android.mk create mode 100644 Service/AndroidManifest.xml create mode 100644 Service/proguard.flags create mode 100644 Service/res/values/analytics.xml create mode 100644 Service/res/values/strings.xml create mode 100644 Service/src/org/android_x86/analytics/AnalyticsService.java create mode 100644 Service/src/org/android_x86/analytics/BatteryState.java create mode 100644 Service/src/org/android_x86/analytics/BootCompletedReceiver.java create mode 100644 Service/src/org/android_x86/analytics/Fields.java create mode 100644 Service/src/org/android_x86/analytics/ImmortalIntentService.java create mode 100644 Service/src/org/android_x86/analytics/LogHelper.java create mode 100644 Service/src/org/android_x86/analytics/PowerStats.java create mode 100644 Service/src/org/android_x86/analytics/Util.java diff --git a/Service/Android.mk b/Service/Android.mk new file mode 100644 index 0000000..3247789 --- /dev/null +++ b/Service/Android.mk @@ -0,0 +1,24 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional +LOCAL_STATIC_JAVA_LIBRARIES := \ + analytics-utils \ + googleanalytics \ + org.apache.http.legacy \ + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-proto-files-under, protos) + +LOCAL_PROTOC_OPTIMIZE_TYPE := lite +LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ + +LOCAL_PACKAGE_NAME := AnalyticsService +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true + +LOCAL_PROGUARD_FLAG_FILES := proguard.flags + +include $(BUILD_PACKAGE) diff --git a/Service/AndroidManifest.xml b/Service/AndroidManifest.xml new file mode 100644 index 0000000..c921229 --- /dev/null +++ b/Service/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Service/proguard.flags b/Service/proguard.flags new file mode 100644 index 0000000..1013e0d --- /dev/null +++ b/Service/proguard.flags @@ -0,0 +1,3 @@ + +-dontwarn org.apache.http.** +-dontwarn android.net.http.** diff --git a/Service/res/values/analytics.xml b/Service/res/values/analytics.xml new file mode 100644 index 0000000..9c5695d --- /dev/null +++ b/Service/res/values/analytics.xml @@ -0,0 +1,24 @@ + + + + + UA-10249025-9 + + false + + + false + diff --git a/Service/res/values/strings.xml b/Service/res/values/strings.xml new file mode 100644 index 0000000..3a2870f --- /dev/null +++ b/Service/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + AnalyticsService + AnalyticsService Settings + diff --git a/Service/src/org/android_x86/analytics/AnalyticsService.java b/Service/src/org/android_x86/analytics/AnalyticsService.java new file mode 100644 index 0000000..66b6fd3 --- /dev/null +++ b/Service/src/org/android_x86/analytics/AnalyticsService.java @@ -0,0 +1,357 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.os.BadParcelableException; +import android.os.Bundle; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.util.Log; +import org.android_x86.analytics.AnalyticsHelper; +import org.android_x86.analytics.GeneralLogs; +import org.android_x86.analytics.ImmortalIntentService; + +import java.util.HashMap; + +public class AnalyticsService extends ImmortalIntentService { + private static final String TAG = "AnalyticsService"; + private static final boolean LOG = false; + + public static final String sSharedPreferencesKey = "org.android_x86.analytics.prefs"; + + private static final int MS_IN_SECOND = 1000; + // ga event + private static final String EVENT_CATEGORY_POWER = "power"; + + private static final String EVENT_BOOT_COMPLETED = "boot_completed"; + private static final String EVENT_SHUTDOWN = "shutdown"; + private static final String EVENT_SCREEN_ON = "screen_on"; + private static final String EVENT_SCREEN_OFF = "screen_off"; + // SharedPreferences_KEY + private static final String SHARED_PREFS_KEY_SCREEN_CHANGE_TIME = "screen_change_time"; + private static final String SHARED_PREFS_KEY_LATEST_SEND_TIME = "latest_send_time"; + // System property for usage_statistics + private static final String PROPERTY_USAGE_STATISTICS = "persist.sys.usage_statistics"; + + private boolean mEnable; + private SharedPreferences mSharedPrefs; + private BroadcastReceiver mReceiver; + + private final HashMap mStaticEventHandlers = + new HashMap(); + + private LogHelper mLogHelper; + + public AnalyticsService() { + super("AnalyticsService"); + initEventHandlers(); + } + + @Override + public void onCreate() { + super.onCreate(); + if (LOG) { + Log.d(TAG, "AnalyticsService onCreate"); + } + mEnable = SystemProperties.getBoolean(PROPERTY_USAGE_STATISTICS, true); + + mSharedPrefs = getSharedPreferences(sSharedPreferencesKey, + Context.MODE_PRIVATE); + + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (LOG) { + Log.d(TAG, "Receive Intent: " + Util.toString(intent)); + } + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + AnalyticsHelper.screenOff(getBaseContext()); + PowerStats.onScreenOff(context); + } else if (Intent.ACTION_SCREEN_ON.equals(action)) { + AnalyticsHelper.screenOn(getBaseContext()); + PowerStats.onScreenOn(context); + } else if (Intent.ACTION_SHUTDOWN.equals(action)) { + AnalyticsHelper.onShutdown(getBaseContext()); + PowerStats.onScreenOff(context); + } else if (BootCompletedReceiver.ACTION_BOOT_COMPLETED.equals(action)) { + AnalyticsHelper.onBootCompleted(getBaseContext()); + PowerStats.onScreenOn(context); + } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) { + PowerStats.onPowerConnected(context); + } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { + PowerStats.onPowerDisconnected(context); + } + } + }; + + // Register for Intent broadcasts for... + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SHUTDOWN); + filter.addAction(BootCompletedReceiver.ACTION_BOOT_COMPLETED); + filter.addAction(BootCompletedReceiver.ACTION_SEND_LOGS); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + getBaseContext().registerReceiver(mReceiver, filter); + + mLogHelper = new LogHelper(this); + } + + @Override + protected void onHandleIntent(Intent intent) { + String action = intent.getAction(); + if (!mEnable){ + if (LOG) { + Log.d(TAG, "USAGE STATISTICS not enable"); + } + return; + } + EventHandler eventHandler = mStaticEventHandlers.get(action); + if (eventHandler != null){ + eventHandler.onEvent(intent); + } else if (!Intent.ACTION_BOOT_COMPLETED.equals(action)){ + Log.w(TAG, "unknow action :" + action); + } + if (LOG) { + Log.d(TAG, "Handle Intent: " + Util.toString(intent)); + } + } + + @Override + public void onDestroy() { + if (LOG){ + Log.d(TAG, "onDestroy"); + } + super.onDestroy(); + } + + private void onHitScreen(Intent data) { + String componentName; + try { + componentName = data.getStringExtra(AnalyticsHelper.EXTRA_COMPONENT_NAME); + } catch (BadParcelableException e) { + Log.w(TAG, "ignore BadParcelableException", e); + return; + } + if (componentName == null) { + Log.w(TAG, "onHitScreen, cannot get data"); + return; + } + if (LOG) { + Log.v(TAG, "hitScreen:" + componentName); + } + ComponentName component = ComponentName.unflattenFromString(componentName); + if (component == null) { + Log.e(TAG, "onHitScreen, invalid ComponentName: " + componentName); + return; + } + + mLogHelper.newAppViewBuilder() + .setActivityDimensions(component) + .send(); + } + + private static long getCurrentTimeInSeconds() { + return System.currentTimeMillis() / MS_IN_SECOND; + } + + private void saveScreenChangeTime(long nowSeconds) { + mSharedPrefs.edit() + .putLong(SHARED_PREFS_KEY_SCREEN_CHANGE_TIME, nowSeconds) + .commit(); + } + + private void removeScreenChangeTime() { + mSharedPrefs.edit() + .remove(SHARED_PREFS_KEY_SCREEN_CHANGE_TIME) + .commit(); + } + + private Long getDurationAndSaveScreenChangeTime() { + long nowSeconds = getCurrentTimeInSeconds(); + long latestChangeTime = mSharedPrefs.getLong(SHARED_PREFS_KEY_SCREEN_CHANGE_TIME, -1); + + saveScreenChangeTime(nowSeconds); + if (latestChangeTime == -1) { + return null; + } + return nowSeconds - latestChangeTime; + } + + private void onBootCompleted(Intent data) { + long bootTime = SystemClock.elapsedRealtime() / MS_IN_SECOND; + mLogHelper.newEventBuilder(EVENT_CATEGORY_POWER, EVENT_BOOT_COMPLETED, null, bootTime) + .send(); + } + + private void onShutdown(Intent data) { + long powerOnIncludeSleep; + long powerOnNotSleep; + try { + powerOnIncludeSleep = data. + getLongExtra(AnalyticsHelper.EXTRA_TIME_INCLUDE_SLEEP, -1); + powerOnNotSleep = data. + getLongExtra(AnalyticsHelper.EXTRA_TIME_NOT_COUNTING_SLEEP, -1); + } catch (BadParcelableException e) { + Log.w(TAG, "ignore BadParcelableException", e); + return; + } + if (powerOnIncludeSleep == -1 || powerOnNotSleep == -1) { + Log.w(TAG, "onShutdown, cannot get data"); + return; + } + mLogHelper.newEventBuilder( + EVENT_CATEGORY_POWER, EVENT_SHUTDOWN, null, powerOnIncludeSleep) + .setPower(powerOnNotSleep) + .send(); + } + + private void onScreenOn(Intent data) { + Long screenOffDuration = getDurationAndSaveScreenChangeTime(); + + mLogHelper.newEventBuilder( + EVENT_CATEGORY_POWER, EVENT_SCREEN_ON, null, screenOffDuration) + .send(); + } + + private void onScreenOff(Intent data) { + Long screenOnDuration = getDurationAndSaveScreenChangeTime(); + mLogHelper.newEventBuilder( + EVENT_CATEGORY_POWER, EVENT_SCREEN_OFF, null, screenOnDuration) + .send(); + } + + private static final String MAIN_THREAD = "main"; + + private void onException(Intent data) { + String exceptionDescription; + String threadName; + String packageName; + try { + exceptionDescription = data.getStringExtra(AnalyticsHelper.EXTRA_EXCEPTION); + threadName = data.getStringExtra(AnalyticsHelper.EXTRA_THREAD_NAME); + packageName = data.getStringExtra(AnalyticsHelper.EXTRA_PACKAGE_NAME); + } catch (BadParcelableException e) { + Log.w(TAG, "ignore BadParcelableException", e); + return; + } + if (exceptionDescription == null) { + Log.e(TAG, "onException, cannot get data"); + return; + } + + if (threadName != null && + !threadName.isEmpty() && + !threadName.equals(MAIN_THREAD)) { + exceptionDescription = "Thread: " + threadName + " " + exceptionDescription; + } + + mLogHelper.newExceptionBuilder(exceptionDescription) + .setPackageDimensions(packageName) + .send(); + } + + private void onCustomEvent(Intent data) { + String event_category; + String event_action; + String event_label; + Long event_value; + String packageName; + boolean hasSampling; + try { + event_category = data.getStringExtra(AnalyticsHelper.EXTRA_EVENT_CATEGORY); + event_action = data.getStringExtra(AnalyticsHelper.EXTRA_EVENT_ACTION); + event_label = data.getStringExtra(AnalyticsHelper.EXTRA_EVENT_LABEL); + event_value = (Long) data.getSerializableExtra(AnalyticsHelper.EXTRA_EVENT_VALUE); + packageName = data.getStringExtra(AnalyticsHelper.EXTRA_PACKAGE_NAME); + hasSampling = data.getBooleanExtra(AnalyticsHelper.EXTRA_HAS_SAMPLING, true); + } catch (BadParcelableException e) { + Log.w(TAG, "ignore BadParcelableException", e); + return; + } + mLogHelper.newEventBuilder( + event_category, event_action, event_label, event_value) + .setPackageDimensions(packageName) + .send(); + } + + private void initEventHandlers() { + mStaticEventHandlers.put(AnalyticsHelper.ACTION_HIT_SCREEN, new EventHandler() { + @Override + void onEvent(Intent intent) { + onHitScreen(intent); + } + }); + mStaticEventHandlers.put(AnalyticsHelper.ACTION_BOOT_COMPLETED, new EventHandler() { + @Override + void onEvent(Intent intent) { + // save boot completed time + saveScreenChangeTime(getCurrentTimeInSeconds()); + + onBootCompleted(intent); + } + }); + mStaticEventHandlers.put(AnalyticsHelper.ACTION_SHUTDOWN, new EventHandler() { + @Override + void onEvent(Intent intent) { + onShutdown(intent); + + // Note: add one extra screen off event to calculate correct screen on duration + // time + onScreenOff(null); + removeScreenChangeTime(); + } + }); + mStaticEventHandlers.put(AnalyticsHelper.ACTION_SCREEN_ON, new EventHandler() { + @Override + void onEvent(Intent intent) { + onScreenOn(intent); + } + }); + mStaticEventHandlers.put(AnalyticsHelper.ACTION_SCREEN_OFF, new EventHandler() { + @Override + void onEvent(Intent intent) { + onScreenOff(intent); + } + }); + mStaticEventHandlers.put(AnalyticsHelper.ACTION_EXCEPTION, new EventHandler() { + @Override + void onEvent(Intent intent) { + onException(intent); + } + }); + mStaticEventHandlers.put(AnalyticsHelper.ACTION_CUSTOM_EVENT, new EventHandler() { + @Override + void onEvent(Intent intent) { + onCustomEvent(intent); + } + }); + } + static abstract class EventHandler { + abstract void onEvent(Intent intent); + } +} diff --git a/Service/src/org/android_x86/analytics/BatteryState.java b/Service/src/org/android_x86/analytics/BatteryState.java new file mode 100644 index 0000000..26b59d1 --- /dev/null +++ b/Service/src/org/android_x86/analytics/BatteryState.java @@ -0,0 +1,132 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; + +public class BatteryState { + + private final Intent mIntent; + + public BatteryState(Intent batteryChangedIntent) { + mIntent = batteryChangedIntent; + } + + /** + * Gets BatteryState or null. + */ + public static BatteryState of(Context context) { + Intent intent = context.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (intent == null) { + return null; + } + return new BatteryState(intent); + } + + /** + * Gets BatteryManager.EXTRA_STATUS, return BatteryManager.BATTERY_STATUS_UNKNOWN + * if failed to get. + */ + public int getStatus() { + return mIntent.getIntExtra(BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN); + } + + /** + * Gets BatteryManager.EXTRA_PLUGGED, return 0 if failed to get. + */ + public int getPlugged() { + return mIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + } + + /** + * Gets BatteryManager.EXTRA_LEVEL, return -1 if failed to get. + */ + public int getLevel() { + return mIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + } + + /** + * Gets BatteryManager.EXTRA_SCALE, return -1 if failed to get. + */ + public int getScale() { + return mIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + } + + /** + * Gets battery percentage or -1 if failed. + */ + public float getLevelPercentage() { + int level = getLevel(); + int scale = getScale(); + if (level < 0 || scale <= 0) { + return -1; + } + return (100.0f * level) / scale; + } + + /** + * Whether battery is charging + */ + public boolean isCharging() { + return isCharging(getStatus(), getPlugged()); + } + + /** + * Whether battery is charging + * @param status corresponds to BatteryManager.EXTRA_STATUS + * @param plugged corresponds to BatteryManager.EXTRA_PLUGGED + */ + public static boolean isCharging(int status, int plugged) { + // fix bug: it's also charging when status is + // "BATTERY_STATUS_DISCHARGING, BATTERY_PLUGGED_*" after boot machine with plugged + return status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL || + plugged != 0; + } + + private static final int MIN_BATTERY_PERCENTAGE = 5; + + /** + * Whether power is sufficient to do some heavy tasks + */ + public boolean isPowerSufficient() { + return isPowerSufficient(MIN_BATTERY_PERCENTAGE); + } + + /** + * Whether power is sufficient to do some heavy tasks + */ + private boolean isPowerSufficient(int minBatteryPercentage) { + return isCharging() || getLevelPercentage() > minBatteryPercentage; + } + + /** + * Whether power is sufficient to do some heavy tasks + */ + public static boolean isPowerSufficient(Context context) { + BatteryState state = of(context); + if (state == null) { + return false; + } + return state.isPowerSufficient(); + } + +} diff --git a/Service/src/org/android_x86/analytics/BootCompletedReceiver.java b/Service/src/org/android_x86/analytics/BootCompletedReceiver.java new file mode 100644 index 0000000..495738e --- /dev/null +++ b/Service/src/org/android_x86/analytics/BootCompletedReceiver.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Random; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import org.android_x86.analytics.AnalyticsHelper; + +public class BootCompletedReceiver extends BroadcastReceiver { + private static final String TAG = "BootCompletedReceiver"; + + public static final String ACTION_BOOT_COMPLETED = "org.android_x86.boot_completed"; + public static final String ACTION_SEND_LOGS = "org.android_x86.send_logs"; + + @Override + public void onReceive(Context context, Intent data) { + String action = data.getAction(); + Intent startIntent = new Intent(action); + startIntent.setComponent( + new ComponentName( + AnalyticsHelper.TARGET_PACKAGE_NAME, + AnalyticsHelper.TARGET_CLASS_NAME)); + if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) { + Log.w(TAG, "unknow action:" + action); + return; + } + context.startService(startIntent); + context.sendBroadcast(new Intent(ACTION_BOOT_COMPLETED)); + + // Set alarm to send logs periodically + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(ACTION_SEND_LOGS); + am.setInexactRepeating(AlarmManager.RTC, getStartTime(), AlarmManager.INTERVAL_HALF_HOUR, + PendingIntent.getBroadcast(context, 0, intent, 0)); + } + + // begin and end hour that we enable sending logs + private static final int BEGIN_HOUR = 0; + private static final int END_HOUR = 24; + private static final int DELAY_SECONDS = 10; + + /** + * Gets start time in [BEGIN_HOUR, END_HOUR) + */ + private static long getStartTime() { + GregorianCalendar calendar = new GregorianCalendar(); + int currentHour = calendar.get(Calendar.HOUR_OF_DAY); + if (currentHour >= BEGIN_HOUR || currentHour < END_HOUR) { + // start after DELAY_SECONDS if boot in [BEGIN_HOUR, END_HOUR) + calendar.add(Calendar.SECOND, DELAY_SECONDS); + } else { + // choose time in [BEGIN_HOUR, END_HOUR) randomly + int startHour = BEGIN_HOUR + new Random().nextInt(END_HOUR + 24 - BEGIN_HOUR); + calendar.add(Calendar.HOUR_OF_DAY, startHour - currentHour); + } + return calendar.getTimeInMillis(); + } +} diff --git a/Service/src/org/android_x86/analytics/Fields.java b/Service/src/org/android_x86/analytics/Fields.java new file mode 100644 index 0000000..95fb97f --- /dev/null +++ b/Service/src/org/android_x86/analytics/Fields.java @@ -0,0 +1,76 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +class Fields { + + enum Metric { + METRIC_POWER_ON_NOT_INCLUDE_SLEEP(1); + + private final int value; + Metric(int value) { this.value = value; } + public int getValue() { return value; } + } + + enum FieldEnum { + // Google analytics fields, see http://goo.gl/M6dK2U + // Common fields + APP_ID(1), + APP_VERSION(2), + APP_NAME(3), + SCREEN_NAME(4); + + private final int value; + FieldEnum(int value) { this.value = value; } + public int getValue() { return value; } + } + + // Custom dimension + enum Dimension { + DIMENSION_BUILD_TYPE(1), + DIMENSION_BUILD_FLAVOR(2), + DIMENSION_DEVICE(3), + DIMENSION_MODEL(4), + DIMENSION_BUILD_VERSION(5), + DIMENSION_POWER_TYPE(6), + DIMENSION_INPUT_TYPE(7), + DIMENSION_DISPLAY_TYPE(8), + DIMENSION_TAG(9), + DIMENSION_NETWORK_TYPE(10), + DIMENSION_RESERVED(11), + DIMENSION_RESOLUTION(12), + DIMENSION_DENSITY(13); + + private final int value; + Dimension(int value) { this.value = value; } + public int getValue() { return value; } + } + + enum Type { + // Use int_key, see FieldEnum + DEFAULT(0), + // Use int_key, correspond to google analytics' custom dimension + CUSTOM_DIMENSION(1), + // Use int_key, correspond to google analytics' custom metric + CUSTOM_METRIC(2), + // Use keys + CUSTOM_GENERAL_LOG(3); + + private final int value; + Type(int value) { this.value = value; } + public int getValue() { return value; } + } +} diff --git a/Service/src/org/android_x86/analytics/ImmortalIntentService.java b/Service/src/org/android_x86/analytics/ImmortalIntentService.java new file mode 100644 index 0000000..ba7ec74 --- /dev/null +++ b/Service/src/org/android_x86/analytics/ImmortalIntentService.java @@ -0,0 +1,139 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; + +/** + * @see android.app.IntentService + * not stopSelf(msg.arg1) + */ +public abstract class ImmortalIntentService extends Service { + private volatile Looper mServiceLooper; + private volatile ServiceHandler mServiceHandler; + private String mName; + private boolean mRedelivery; + + private final class ServiceHandler extends Handler { + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + onHandleIntent((Intent)msg.obj); + } + } + + /** + * Creates an IntentService. Invoked by your subclass's constructor. + * + * @param name Used to name the worker thread, important only for debugging. + */ + public ImmortalIntentService(String name) { + super(); + mName = name; + } + + /** + * Sets intent redelivery preferences. Usually called from the constructor + * with your preferred semantics. + * + *

If enabled is true, + * {@link #onStartCommand(Intent, int, int)} will return + * {@link Service#START_REDELIVER_INTENT}, so if this process dies before + * {@link #onHandleIntent(Intent)} returns, the process will be restarted + * and the intent redelivered. If multiple Intents have been sent, only + * the most recent one is guaranteed to be redelivered. + * + *

If enabled is false (the default), + * {@link #onStartCommand(Intent, int, int)} will return + * {@link Service#START_NOT_STICKY}, and if the process dies, the Intent + * dies along with it. + */ + public void setIntentRedelivery(boolean enabled) { + mRedelivery = enabled; + } + + @Override + public void onCreate() { + // TODO: It would be nice to have an option to hold a partial wakelock + // during processing, and to have a static startService(Context, Intent) + // method that would launch the service & hand off a wakelock. + + super.onCreate(); + HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); + thread.start(); + + mServiceLooper = thread.getLooper(); + mServiceHandler = new ServiceHandler(mServiceLooper); + } + + @Override + public void onStart(Intent intent, int startId) { + Message msg = mServiceHandler.obtainMessage(); + msg.arg1 = startId; + msg.obj = intent; + mServiceHandler.sendMessage(msg); + } + + /** + * You should not override this method for your IntentService. Instead, + * override {@link #onHandleIntent}, which the system calls when the IntentService + * receives a start request. + * @see android.app.Service#onStartCommand + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + onStart(intent, startId); + return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; + } + + @Override + public void onDestroy() { + mServiceLooper.quit(); + } + + /** + * Unless you provide binding for your service, you don't need to implement this + * method, because the default implementation returns null. + * @see android.app.Service#onBind + */ + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * This method is invoked on the worker thread with a request to process. + * Only one Intent is processed at a time, but the processing happens on a + * worker thread that runs independently from other application logic. + * So, if this code takes a long time, it will hold up other requests to + * the same IntentService, but it will not hold up anything else. + * When all requests have been handled, the IntentService stops itself, + * so you should not call {@link #stopSelf}. + * + * @param intent The value passed to {@link + * android.content.Context#startService(Intent)}. + */ + protected abstract void onHandleIntent(Intent intent); +} diff --git a/Service/src/org/android_x86/analytics/LogHelper.java b/Service/src/org/android_x86/analytics/LogHelper.java new file mode 100644 index 0000000..6ba51d6 --- /dev/null +++ b/Service/src/org/android_x86/analytics/LogHelper.java @@ -0,0 +1,169 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; + +import com.google.analytics.tracking.android.EasyTracker; +import com.google.analytics.tracking.android.Fields; +import com.google.analytics.tracking.android.MapBuilder; +import com.google.analytics.tracking.android.Tracker; + +import org.android_x86.analytics.AnalyticsHelper; +import org.android_x86.analytics.GeneralLogs; +import org.android_x86.analytics.Fields.Type; +import org.android_x86.analytics.Fields.FieldEnum; +import org.android_x86.analytics.Fields.Metric; +import org.android_x86.analytics.Fields.Dimension; + +public class LogHelper { + private static final String TAG = "LogHelper"; + private static final boolean DEBUG = AnalyticsHelper.DEBUG; + + private static final double SAMPLING_RATE = 0; //TODO + + private final Context mContext; + private final Tracker mTracker; + + /** + * Log to Google analytics + */ + public LogHelper(Context context) { + mContext = context; + mTracker = EasyTracker.getInstance(context); + } + + public LogBuilder newAppViewBuilder() { + return new LogBuilder(MapBuilder.createAppView()); + } + + public LogBuilder newEventBuilder( + String category, String action, String label, Long value) { + MapBuilder mapBuilder = MapBuilder.createEvent(category, action, label, value); + LogBuilder builder = new LogBuilder(mapBuilder); + return builder; + } + + public LogBuilder newExceptionBuilder(String exceptionDescription) { + return new LogBuilder(MapBuilder.createException(exceptionDescription, true)); + } + + public class LogBuilder { + private final MapBuilder mBuilder; + + // do not check if field redundant! + + private LogBuilder(MapBuilder builder) { + mBuilder = builder; + } + + /** + * Sets common field + */ + private void setCommonField(FieldEnum key, String value) { + if (mBuilder == null) return; + switch (key) { + case APP_ID: + mBuilder.set(Fields.APP_ID, value); + break; + case APP_VERSION: + mBuilder.set(Fields.APP_VERSION, value); + break; + case APP_NAME: + mBuilder.set(Fields.APP_NAME, value); + break; + case SCREEN_NAME: + mBuilder.set(Fields.SCREEN_NAME, value); + break; + default: + throw new UnsupportedOperationException("Invalid field: " + key); + } + } + + /** + * Sets Google Analytics field without check + */ + private LogBuilder set(Type type, T key, Object value) { + if (mBuilder == null) return this; + switch (type) { + case DEFAULT: + setCommonField((FieldEnum)key, value.toString()); + break; + case CUSTOM_DIMENSION: + mBuilder.set(Fields.customDimension(((Dimension)key).getValue()), value.toString()); + break; + case CUSTOM_METRIC: + mBuilder.set(Fields.customMetric(((Metric)key).getValue()), value.toString()); + break; + default: + throw new UnsupportedOperationException("Invalid Type: " + type + ", key: " + + key + ", value=" + value); + } + return this; + } + + public LogBuilder setPower(long powerOnNotSleep) { + set(Type.CUSTOM_METRIC, Metric.METRIC_POWER_ON_NOT_INCLUDE_SLEEP, powerOnNotSleep); + return this; + } + + public LogBuilder setPackageDimensions(String packageName) { + // removed a lots of informations! + set(Type.DEFAULT, FieldEnum.APP_NAME, packageName); + return this; + } + + public LogBuilder setActivityDimensions(ComponentName component) { + // removed a lots of informations! + set(Type.DEFAULT, FieldEnum.APP_NAME, component.getPackageName()); + set(Type.DEFAULT, FieldEnum.SCREEN_NAME, component.getClassName()); + return this; + } + + public void send() { + // removed a lots of informations! + + if (mBuilder != null) { + // Add common fields to MapBuilder + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_BUILD_TYPE, Build.TYPE); + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_DEVICE, Build.DEVICE); + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_MODEL, Build.MODEL); + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_BUILD_VERSION, Util.BuildUtil.getBuildVersion()); + + DisplayMetrics metrics = Util.getDefaultDisplayMetrics(mContext); + float rate = Util.getDefaultDisplayRefreshRate(mContext); + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_RESOLUTION, + metrics.widthPixels + " * " + metrics.heightPixels + " " + + Integer.toString((int) rate) + "Hz"); + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_DENSITY, metrics.densityDpi); + + Map map = mBuilder.build(); + if (DEBUG) { + Log.d(TAG, "Google Analytics log entry: " + map); + } + mTracker.send(map); + } + } + } +} diff --git a/Service/src/org/android_x86/analytics/PowerStats.java b/Service/src/org/android_x86/analytics/PowerStats.java new file mode 100644 index 0000000..86c5ec3 --- /dev/null +++ b/Service/src/org/android_x86/analytics/PowerStats.java @@ -0,0 +1,114 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +import android.content.Context; +import android.os.SystemClock; + +public class PowerStats { + private static final long MIN_STATS_INTERVAL_MILLIS = 15 * 60 * 1000; + private static final long TEN_HOUR_MILLIS = 10 * 60 * 60 * 1000; + + private static final String EVENT_CATEGORY_POWER_USAGE = "power_usage"; + private static final String ACTION_DISCHARGE_SCREEN_ON = "discharge_screen_on"; + private static final String ACTION_DISCHARGE_SCREEN_OFF = "discharge_screen_off"; + + private final boolean mIsScreenOn; + private final boolean mIsCharging; + private final long mTime; + private final float mPercentage; + + private static PowerStats mPowerStats; + + private PowerStats(boolean isScreenOn, boolean isCharging, long time, float percentage) { + mIsScreenOn = isScreenOn; + mIsCharging = isCharging; + mTime = time; + mPercentage = percentage; + } + + @Override + public String toString() { + return "isScreenOn: " + mIsScreenOn + + " isCharging: " + mIsCharging + + " time: " + mTime + + " percentage: " + mPercentage; + } + + public static void onScreenOn(Context context) { + onIntent(context, true, null); + } + + public static void onScreenOff(Context context) { + onIntent(context, false, null); + } + + public static void onPowerConnected(Context context) { + onIntent(context, null, true); + } + + public static void onPowerDisconnected(Context context) { + onIntent(context, null, false); + } + + private static void onIntent(Context context, Boolean isScreenOn, Boolean isCharging) { + BatteryState state = BatteryState.of(context); + if (state == null) { + return; + } + float percentage = state.getLevelPercentage(); + if (percentage < 0) { + return; + } + new PowerStats( + isScreenOn != null ? isScreenOn : Util.isScreenOn(context), + isCharging != null ? isCharging : state.isCharging(), + SystemClock.elapsedRealtime(), + percentage) + .handle(context); + } + + private void handle(Context context) { + PowerStats previous; + synchronized(PowerStats.class) { + previous = mPowerStats; + // only save battery percentage when screen on/off or charging status change. + if (previous != null + && (mIsScreenOn == previous.mIsScreenOn) + && (mIsCharging == previous.mIsCharging)) { + return; + } + mPowerStats = this; + } + if (previous == null) { + return; + } + long interval = mTime - previous.mTime; + float percentageReduce = previous.mPercentage - mPercentage; + if (interval > MIN_STATS_INTERVAL_MILLIS + && percentageReduce >= 0 + && !previous.mIsCharging) { + long value = (long) (percentageReduce * TEN_HOUR_MILLIS / interval); + AnalyticsHelper.newSystemCoreEvent(context, + EVENT_CATEGORY_POWER_USAGE, + previous.mIsScreenOn ? ACTION_DISCHARGE_SCREEN_ON + : ACTION_DISCHARGE_SCREEN_OFF) + .setLabel(Long.toString(value)) + .setValue(value) + .sendWithSampling(); + } + } +} diff --git a/Service/src/org/android_x86/analytics/Util.java b/Service/src/org/android_x86/analytics/Util.java new file mode 100644 index 0000000..e8d6f54 --- /dev/null +++ b/Service/src/org/android_x86/analytics/Util.java @@ -0,0 +1,439 @@ +/* + * Copyright 2016 Jide Technology Ltd. + * + * 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 org.android_x86.analytics; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Formatter; +import java.util.Random; +import java.util.Collections; +import java.util.ArrayList; +import java.util.List; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.HttpURLConnection; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.CoreProtocolPNames; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.os.PowerManager; +import android.os.Bundle; +import android.os.Build; +import android.content.Context; +import android.content.Intent; +import android.util.DisplayMetrics; +import android.view.WindowManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.provider.Settings.Secure; + +public class Util { + private Util() { + } + + public static DisplayMetrics getDefaultDisplayMetrics(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = new DisplayMetrics(); + wm.getDefaultDisplay().getRealMetrics(metrics); + return metrics; + } + + public static float getDefaultDisplayRefreshRate(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + return wm.getDefaultDisplay().getRefreshRate(); + } + + /* --- DebugUtil --- */ + + /** + * Gets Intent's debug string with extras. + */ + public static String toString(Intent intent) { + StringBuilder sb = new StringBuilder(); + sb.append(intent.getAction()); + Bundle extras = intent.getExtras(); + if (extras != null && !extras.isEmpty()) { + sb.append(" " + extras); + } + return sb.toString(); + } + + /* --- PowerUtil --- */ + + public static boolean isScreenOn(Context context) { + return ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).isScreenOn(); + } + + /* --- BuildUtil --- */ + + public static class BuildUtil { + + public static final String UNKNOWN = "unknown"; + + public static final String NIGHTLY = "nightly"; + public static final String BETA = "beta"; + public static final String STABLE = "stable"; + + /** + * Gets locale string. + */ + public static String getLocale(Context context) { + return context.getResources().getConfiguration().locale.toString(); + } + + /** + * Gets build flavor + */ + public static String getFlavor() { + String version = Build.VERSION.INCREMENTAL; + if (!version.isEmpty()) { + switch (version.charAt(0)) { + case 'N': + return NIGHTLY; + case 'B': + return BETA; + case 'S': + return STABLE; + } + } + return UNKNOWN; + } + + /** + * Gets ANDROID_ID or empty string if not present. + */ + public static String getAndroidID(Context context) { + String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); + return androidId == null ? "" : androidId; + } + + /** + * Gets full build version. + */ + public static String getBuildVersion() { + if (Build.VERSION.INCREMENTAL.startsWith(Build.VERSION.RELEASE)) { + return Build.VERSION.INCREMENTAL; + } + return Build.VERSION.RELEASE + '-' + Build.VERSION.INCREMENTAL; + } + } + + /* --- IOUtil --- */ + + public static class IOUtil { + + public static final String UTF8 = "UTF-8"; + + private static final int BUFFER_SIZE = 16 * 1024; + + /** + * Reads input stream to an output stream and close input stream (not close output stream). + * @param in + * @param out + * @throws IOException + */ + public static void toOutputStream(InputStream in, OutputStream out) throws IOException { + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int size; + while ((size = in.read(buffer)) != -1) { + out.write(buffer, 0, size); + } + } finally { + in.close(); + } + } + + /** + * Writes byte array to output stream and close output stream. + * @throws IOException + */ + public static void toAndCloseOutputStream(byte[] data, OutputStream out) throws IOException { + try { + out.write(data); + } finally { + out.close(); + } + } + + /** + * Writes a string to output stream and close output stream. + * @throws IOException + */ + public static void toAndCloseOutputStream(String s, OutputStream out) throws IOException { + toAndCloseOutputStream(s.getBytes(UTF8), out); + } + + /** + * Reads input stream to a string and close input stream. + * @throws IOException + */ + public static String toString(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + toOutputStream(in, out); + return out.toString(UTF8); + } finally { + out.close(); + } + } + } + + /* --- HttpUtil --- */ + + public static class HttpStatusException extends Exception { + private final int mStatusCode; + + public HttpStatusException(int statusCode) { + super(); + mStatusCode = statusCode; + } + + public HttpStatusException(int statusCode, String message) { + super(message); + mStatusCode = statusCode; + } + + @Override + public String getMessage() { + return "StatusCode: " + mStatusCode + " " + super.getMessage(); + } + + public int getStatusCode() { + return mStatusCode; + } + } + + public static class HttpStatusLineException extends HttpStatusException { + private final StatusLine mStatusLine; + + public HttpStatusLineException(StatusLine statusLine) { + super(statusLine.getStatusCode()); + mStatusLine = statusLine; + } + + public HttpStatusLineException(StatusLine statusLine, String message) { + super(statusLine.getStatusCode(), message); + mStatusLine = statusLine; + } + + @Override + public String getMessage() { + return "StatusLine: " + mStatusLine + " " + super.getMessage(); + } + + public StatusLine getStatusLine() { + return mStatusLine; + } + } + + /** + * HTTP post to given URL. + */ + public static HttpEntity doPost(String url, HttpEntity entity) + throws URISyntaxException, IOException, HttpStatusLineException { + HttpClient client = new DefaultHttpClient(); + client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, "android"); + HttpPost request = new HttpPost(); + request.setURI(new URI(url)); + request.setEntity(entity); + + HttpResponse response = client.execute(request); + StatusLine statusLine = response.getStatusLine(); + if (statusLine.getStatusCode() != HttpStatus.SC_OK) { + throw new HttpStatusLineException(statusLine); + } + return response.getEntity(); + } + + /** + * Gets JSON from URL + */ + public static JSONObject getJsonFromUrl(String url) + throws MalformedURLException, JSONException, IOException { + return new JSONObject(IOUtil.toString(new URL(url).openStream())); + } + + /** + * Posts JSON request and get JSON reply. + */ + public static JSONObject postAndGetJson(String url, String jsonRequest) + throws MalformedURLException, IOException, HttpStatusException, JSONException { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json; charset=utf8"); + conn.setRequestProperty("Accept", "application/json"); + IOUtil.toAndCloseOutputStream(jsonRequest, conn.getOutputStream()); + + int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new HttpStatusException(responseCode); + } + return new JSONObject(IOUtil.toString(conn.getInputStream())); + } + + /** + * Posts JSON request and get JSON reply. + */ + public static JSONObject postAndGetJson(String url, JSONObject jsonRequest) + throws MalformedURLException, IOException, HttpStatusException, JSONException { + return postAndGetJson(url, jsonRequest.toString()); + } + + /* --- NetUtil --- */ + + public static ConnectivityManager getConnectivityManager(Context context) { + return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + /** + * Whether network is good to send cloud server requests + */ + public static boolean isNetworkGood(ConnectivityManager cm) { + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } + + /** + * Whether network is good to send cloud server requests + */ + public static boolean isNetworkGood(Context context) { + return isNetworkGood(getConnectivityManager(context)); + } + + /** + * Gets MAC address or empty string. + */ + public static String getMacAddress(Context context) { + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + String mac = wifiInfo.getMacAddress(); + if (mac != null) { + return mac; + } + } + return ""; + } + + /* --- SecurityUtil --- */ + + public static class SecurityUtil { + + /** + * Converts bytes to hex string. + */ + public static String byteToHex(byte[] bytes) { + Formatter formatter = new Formatter(); + for (byte b : bytes) { + formatter.format("%02x", b); + } + String result = formatter.toString(); + formatter.close(); + return result; + } + + /** + * Use SHA-1 algorithm to hash given string and return a hex string. + */ + public static String sha1(String s) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + return byteToHex(MessageDigest.getInstance("SHA-1").digest(s.getBytes("UTF-8"))); + } + + } + + /* --- SignatureBuilder --- */ + + public static class SignatureBuilder { + private final List mInfos; + + private SignatureBuilder(String... infos) { + mInfos = new ArrayList(); + for (String s : infos) { + mInfos.add(s); + } + } + + /** + * Gets a random SignatureBuilder + */ + public static SignatureBuilder of() { + return new SignatureBuilder( + String.valueOf(System.currentTimeMillis()), + String.valueOf(new Random().nextLong()), + Build.SERIAL); + } + + /** + * Gets informations used to generate signature. + */ + public List getInfos() { + return mInfos; + } + + /** + * Builds signature with secret key and separator. + */ + public String build(String key, String separator) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + List list = new ArrayList(mInfos); + list.add(key); + Collections.sort(list); + + StringBuilder sb = new StringBuilder(); + for (String s : list) { + sb.append(s).append(separator); + } + return SecurityUtil.sha1(sb.toString()); + } + + /** + * Builds signature with secret key and separator, return comma separated string of source + * informations and signature. + */ + public String buildToJoinString(String key, String separator) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + String signature = build(key, separator); + StringBuilder sb = new StringBuilder(); + for (String s : mInfos) { + sb.append(s).append(','); + } + sb.append(signature); + return sb.toString(); + } + } +} -- 2.11.0