packages/services/Analytics
Révision | bafb078d9cdec28c16553de479e413229e7bcad2 (tree) |
---|---|
l'heure | 2016-08-10 18:48:29 |
Auteur | Hugo <hugo@jide...> |
Commiter | Chih-Wei Huang |
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.
@@ -0,0 +1,24 @@ | ||
1 | +LOCAL_PATH := $(call my-dir) | |
2 | + | |
3 | +include $(CLEAR_VARS) | |
4 | + | |
5 | +LOCAL_MODULE_TAGS := optional | |
6 | +LOCAL_STATIC_JAVA_LIBRARIES := \ | |
7 | + analytics-utils \ | |
8 | + googleanalytics \ | |
9 | + org.apache.http.legacy \ | |
10 | + | |
11 | +LOCAL_SRC_FILES := \ | |
12 | + $(call all-java-files-under, src) \ | |
13 | + $(call all-proto-files-under, protos) | |
14 | + | |
15 | +LOCAL_PROTOC_OPTIMIZE_TYPE := lite | |
16 | +LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ | |
17 | + | |
18 | +LOCAL_PACKAGE_NAME := AnalyticsService | |
19 | +LOCAL_CERTIFICATE := platform | |
20 | +LOCAL_PRIVILEGED_MODULE := true | |
21 | + | |
22 | +LOCAL_PROGUARD_FLAG_FILES := proguard.flags | |
23 | + | |
24 | +include $(BUILD_PACKAGE) |
@@ -0,0 +1,48 @@ | ||
1 | +<?xml version="1.0" encoding="utf-8"?> | |
2 | +<!-- Copyright (C) 2016 Jide Technology Ltd. | |
3 | + | |
4 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + you may not use this file except in compliance with the License. | |
6 | + You may obtain a copy of the License at | |
7 | + | |
8 | + http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + | |
10 | + Unless required by applicable law or agreed to in writing, software | |
11 | + distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + See the License for the specific language governing permissions and | |
14 | + limitations under the License. | |
15 | +--> | |
16 | + | |
17 | +<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
18 | + package="org.android_x86.analytics"> | |
19 | + | |
20 | + <uses-permission android:name="android.permission.INTERNET" /> | |
21 | + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |
22 | + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> | |
23 | + <uses-permission android:name="android.permission.BATTERY_STATS" /> | |
24 | + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> | |
25 | + <uses-permission android:name="android.permission.READ_SETTINGS" /> | |
26 | + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> | |
27 | + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> | |
28 | + <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | |
29 | + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> | |
30 | + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> | |
31 | + <application android:label="@string/app_name" | |
32 | + android:persistent="true"> | |
33 | + <service android:name=".AnalyticsService" | |
34 | + android:exported="true" /> | |
35 | + <receiver android:name=".BootCompletedReceiver"> | |
36 | + <intent-filter> | |
37 | + <action android:name="android.intent.action.BOOT_COMPLETED" /> | |
38 | + </intent-filter> | |
39 | + </receiver> | |
40 | + <activity android:name=".UsageStatisticsSettings"> | |
41 | + <intent-filter> | |
42 | + <action android:name="android.intent.action.MAIN" /> | |
43 | + <action android:name="org.android_x86.analytics.SETUP_USAGE_STATISTICS" /> | |
44 | + <category android:name="android.intent.category.DEFAULT" /> | |
45 | + </intent-filter> | |
46 | + </activity> | |
47 | + </application> | |
48 | +</manifest> |
@@ -0,0 +1,3 @@ | ||
1 | + | |
2 | +-dontwarn org.apache.http.** | |
3 | +-dontwarn android.net.http.** |
@@ -0,0 +1,24 @@ | ||
1 | +<?xml version="1.0" encoding="utf-8"?> | |
2 | +<!-- Copyright (C) 2016 Jide Technology Ltd. | |
3 | + | |
4 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + you may not use this file except in compliance with the License. | |
6 | + You may obtain a copy of the License at | |
7 | + | |
8 | + http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + | |
10 | + Unless required by applicable law or agreed to in writing, software | |
11 | + distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + See the License for the specific language governing permissions and | |
14 | + limitations under the License. | |
15 | +--> | |
16 | +<resources> | |
17 | + <!--Replace placeholder ID with your tracking ID--> | |
18 | + <string name="ga_trackingId">UA-10249025-9</string> | |
19 | + <!--Enable automatic activity tracking--> | |
20 | + <bool name="ga_autoActivityTracking">false</bool> | |
21 | + | |
22 | + <!--Enable automatic exception tracking--> | |
23 | + <bool name="ga_reportUncaughtExceptions">false</bool> | |
24 | +</resources> |
@@ -0,0 +1,19 @@ | ||
1 | +<?xml version="1.0" encoding="utf-8"?> | |
2 | +<!-- Copyright (C) 2016 Jide Technology Ltd. | |
3 | + | |
4 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + you may not use this file except in compliance with the License. | |
6 | + You may obtain a copy of the License at | |
7 | + | |
8 | + http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + | |
10 | + Unless required by applicable law or agreed to in writing, software | |
11 | + distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + See the License for the specific language governing permissions and | |
14 | + limitations under the License. | |
15 | +--> | |
16 | +<resources> | |
17 | + <string name="app_name">AnalyticsService</string> | |
18 | + <string name="app_setting_name">AnalyticsService Settings</string> | |
19 | +</resources> |
@@ -0,0 +1,357 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +import android.app.AlarmManager; | |
19 | +import android.content.BroadcastReceiver; | |
20 | +import android.content.ComponentName; | |
21 | +import android.content.Context; | |
22 | +import android.content.Intent; | |
23 | +import android.content.IntentFilter; | |
24 | +import android.content.SharedPreferences; | |
25 | +import android.net.ConnectivityManager; | |
26 | +import android.os.BadParcelableException; | |
27 | +import android.os.Bundle; | |
28 | +import android.os.SystemClock; | |
29 | +import android.os.SystemProperties; | |
30 | +import android.util.Log; | |
31 | +import org.android_x86.analytics.AnalyticsHelper; | |
32 | +import org.android_x86.analytics.GeneralLogs; | |
33 | +import org.android_x86.analytics.ImmortalIntentService; | |
34 | + | |
35 | +import java.util.HashMap; | |
36 | + | |
37 | +public class AnalyticsService extends ImmortalIntentService { | |
38 | + private static final String TAG = "AnalyticsService"; | |
39 | + private static final boolean LOG = false; | |
40 | + | |
41 | + public static final String sSharedPreferencesKey = "org.android_x86.analytics.prefs"; | |
42 | + | |
43 | + private static final int MS_IN_SECOND = 1000; | |
44 | + // ga event | |
45 | + private static final String EVENT_CATEGORY_POWER = "power"; | |
46 | + | |
47 | + private static final String EVENT_BOOT_COMPLETED = "boot_completed"; | |
48 | + private static final String EVENT_SHUTDOWN = "shutdown"; | |
49 | + private static final String EVENT_SCREEN_ON = "screen_on"; | |
50 | + private static final String EVENT_SCREEN_OFF = "screen_off"; | |
51 | + // SharedPreferences_KEY | |
52 | + private static final String SHARED_PREFS_KEY_SCREEN_CHANGE_TIME = "screen_change_time"; | |
53 | + private static final String SHARED_PREFS_KEY_LATEST_SEND_TIME = "latest_send_time"; | |
54 | + // System property for usage_statistics | |
55 | + private static final String PROPERTY_USAGE_STATISTICS = "persist.sys.usage_statistics"; | |
56 | + | |
57 | + private boolean mEnable; | |
58 | + private SharedPreferences mSharedPrefs; | |
59 | + private BroadcastReceiver mReceiver; | |
60 | + | |
61 | + private final HashMap<String, EventHandler> mStaticEventHandlers = | |
62 | + new HashMap<String, EventHandler>(); | |
63 | + | |
64 | + private LogHelper mLogHelper; | |
65 | + | |
66 | + public AnalyticsService() { | |
67 | + super("AnalyticsService"); | |
68 | + initEventHandlers(); | |
69 | + } | |
70 | + | |
71 | + @Override | |
72 | + public void onCreate() { | |
73 | + super.onCreate(); | |
74 | + if (LOG) { | |
75 | + Log.d(TAG, "AnalyticsService onCreate"); | |
76 | + } | |
77 | + mEnable = SystemProperties.getBoolean(PROPERTY_USAGE_STATISTICS, true); | |
78 | + | |
79 | + mSharedPrefs = getSharedPreferences(sSharedPreferencesKey, | |
80 | + Context.MODE_PRIVATE); | |
81 | + | |
82 | + mReceiver = new BroadcastReceiver() { | |
83 | + @Override | |
84 | + public void onReceive(Context context, Intent intent) { | |
85 | + String action = intent.getAction(); | |
86 | + if (LOG) { | |
87 | + Log.d(TAG, "Receive Intent: " + Util.toString(intent)); | |
88 | + } | |
89 | + if (Intent.ACTION_SCREEN_OFF.equals(action)) { | |
90 | + AnalyticsHelper.screenOff(getBaseContext()); | |
91 | + PowerStats.onScreenOff(context); | |
92 | + } else if (Intent.ACTION_SCREEN_ON.equals(action)) { | |
93 | + AnalyticsHelper.screenOn(getBaseContext()); | |
94 | + PowerStats.onScreenOn(context); | |
95 | + } else if (Intent.ACTION_SHUTDOWN.equals(action)) { | |
96 | + AnalyticsHelper.onShutdown(getBaseContext()); | |
97 | + PowerStats.onScreenOff(context); | |
98 | + } else if (BootCompletedReceiver.ACTION_BOOT_COMPLETED.equals(action)) { | |
99 | + AnalyticsHelper.onBootCompleted(getBaseContext()); | |
100 | + PowerStats.onScreenOn(context); | |
101 | + } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) { | |
102 | + PowerStats.onPowerConnected(context); | |
103 | + } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { | |
104 | + PowerStats.onPowerDisconnected(context); | |
105 | + } | |
106 | + } | |
107 | + }; | |
108 | + | |
109 | + // Register for Intent broadcasts for... | |
110 | + IntentFilter filter = new IntentFilter(); | |
111 | + filter.addAction(Intent.ACTION_SCREEN_OFF); | |
112 | + filter.addAction(Intent.ACTION_SCREEN_ON); | |
113 | + filter.addAction(Intent.ACTION_SHUTDOWN); | |
114 | + filter.addAction(BootCompletedReceiver.ACTION_BOOT_COMPLETED); | |
115 | + filter.addAction(BootCompletedReceiver.ACTION_SEND_LOGS); | |
116 | + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); | |
117 | + filter.addAction(Intent.ACTION_POWER_CONNECTED); | |
118 | + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); | |
119 | + getBaseContext().registerReceiver(mReceiver, filter); | |
120 | + | |
121 | + mLogHelper = new LogHelper(this); | |
122 | + } | |
123 | + | |
124 | + @Override | |
125 | + protected void onHandleIntent(Intent intent) { | |
126 | + String action = intent.getAction(); | |
127 | + if (!mEnable){ | |
128 | + if (LOG) { | |
129 | + Log.d(TAG, "USAGE STATISTICS not enable"); | |
130 | + } | |
131 | + return; | |
132 | + } | |
133 | + EventHandler eventHandler = mStaticEventHandlers.get(action); | |
134 | + if (eventHandler != null){ | |
135 | + eventHandler.onEvent(intent); | |
136 | + } else if (!Intent.ACTION_BOOT_COMPLETED.equals(action)){ | |
137 | + Log.w(TAG, "unknow action :" + action); | |
138 | + } | |
139 | + if (LOG) { | |
140 | + Log.d(TAG, "Handle Intent: " + Util.toString(intent)); | |
141 | + } | |
142 | + } | |
143 | + | |
144 | + @Override | |
145 | + public void onDestroy() { | |
146 | + if (LOG){ | |
147 | + Log.d(TAG, "onDestroy"); | |
148 | + } | |
149 | + super.onDestroy(); | |
150 | + } | |
151 | + | |
152 | + private void onHitScreen(Intent data) { | |
153 | + String componentName; | |
154 | + try { | |
155 | + componentName = data.getStringExtra(AnalyticsHelper.EXTRA_COMPONENT_NAME); | |
156 | + } catch (BadParcelableException e) { | |
157 | + Log.w(TAG, "ignore BadParcelableException", e); | |
158 | + return; | |
159 | + } | |
160 | + if (componentName == null) { | |
161 | + Log.w(TAG, "onHitScreen, cannot get data"); | |
162 | + return; | |
163 | + } | |
164 | + if (LOG) { | |
165 | + Log.v(TAG, "hitScreen:" + componentName); | |
166 | + } | |
167 | + ComponentName component = ComponentName.unflattenFromString(componentName); | |
168 | + if (component == null) { | |
169 | + Log.e(TAG, "onHitScreen, invalid ComponentName: " + componentName); | |
170 | + return; | |
171 | + } | |
172 | + | |
173 | + mLogHelper.newAppViewBuilder() | |
174 | + .setActivityDimensions(component) | |
175 | + .send(); | |
176 | + } | |
177 | + | |
178 | + private static long getCurrentTimeInSeconds() { | |
179 | + return System.currentTimeMillis() / MS_IN_SECOND; | |
180 | + } | |
181 | + | |
182 | + private void saveScreenChangeTime(long nowSeconds) { | |
183 | + mSharedPrefs.edit() | |
184 | + .putLong(SHARED_PREFS_KEY_SCREEN_CHANGE_TIME, nowSeconds) | |
185 | + .commit(); | |
186 | + } | |
187 | + | |
188 | + private void removeScreenChangeTime() { | |
189 | + mSharedPrefs.edit() | |
190 | + .remove(SHARED_PREFS_KEY_SCREEN_CHANGE_TIME) | |
191 | + .commit(); | |
192 | + } | |
193 | + | |
194 | + private Long getDurationAndSaveScreenChangeTime() { | |
195 | + long nowSeconds = getCurrentTimeInSeconds(); | |
196 | + long latestChangeTime = mSharedPrefs.getLong(SHARED_PREFS_KEY_SCREEN_CHANGE_TIME, -1); | |
197 | + | |
198 | + saveScreenChangeTime(nowSeconds); | |
199 | + if (latestChangeTime == -1) { | |
200 | + return null; | |
201 | + } | |
202 | + return nowSeconds - latestChangeTime; | |
203 | + } | |
204 | + | |
205 | + private void onBootCompleted(Intent data) { | |
206 | + long bootTime = SystemClock.elapsedRealtime() / MS_IN_SECOND; | |
207 | + mLogHelper.newEventBuilder(EVENT_CATEGORY_POWER, EVENT_BOOT_COMPLETED, null, bootTime) | |
208 | + .send(); | |
209 | + } | |
210 | + | |
211 | + private void onShutdown(Intent data) { | |
212 | + long powerOnIncludeSleep; | |
213 | + long powerOnNotSleep; | |
214 | + try { | |
215 | + powerOnIncludeSleep = data. | |
216 | + getLongExtra(AnalyticsHelper.EXTRA_TIME_INCLUDE_SLEEP, -1); | |
217 | + powerOnNotSleep = data. | |
218 | + getLongExtra(AnalyticsHelper.EXTRA_TIME_NOT_COUNTING_SLEEP, -1); | |
219 | + } catch (BadParcelableException e) { | |
220 | + Log.w(TAG, "ignore BadParcelableException", e); | |
221 | + return; | |
222 | + } | |
223 | + if (powerOnIncludeSleep == -1 || powerOnNotSleep == -1) { | |
224 | + Log.w(TAG, "onShutdown, cannot get data"); | |
225 | + return; | |
226 | + } | |
227 | + mLogHelper.newEventBuilder( | |
228 | + EVENT_CATEGORY_POWER, EVENT_SHUTDOWN, null, powerOnIncludeSleep) | |
229 | + .setPower(powerOnNotSleep) | |
230 | + .send(); | |
231 | + } | |
232 | + | |
233 | + private void onScreenOn(Intent data) { | |
234 | + Long screenOffDuration = getDurationAndSaveScreenChangeTime(); | |
235 | + | |
236 | + mLogHelper.newEventBuilder( | |
237 | + EVENT_CATEGORY_POWER, EVENT_SCREEN_ON, null, screenOffDuration) | |
238 | + .send(); | |
239 | + } | |
240 | + | |
241 | + private void onScreenOff(Intent data) { | |
242 | + Long screenOnDuration = getDurationAndSaveScreenChangeTime(); | |
243 | + mLogHelper.newEventBuilder( | |
244 | + EVENT_CATEGORY_POWER, EVENT_SCREEN_OFF, null, screenOnDuration) | |
245 | + .send(); | |
246 | + } | |
247 | + | |
248 | + private static final String MAIN_THREAD = "main"; | |
249 | + | |
250 | + private void onException(Intent data) { | |
251 | + String exceptionDescription; | |
252 | + String threadName; | |
253 | + String packageName; | |
254 | + try { | |
255 | + exceptionDescription = data.getStringExtra(AnalyticsHelper.EXTRA_EXCEPTION); | |
256 | + threadName = data.getStringExtra(AnalyticsHelper.EXTRA_THREAD_NAME); | |
257 | + packageName = data.getStringExtra(AnalyticsHelper.EXTRA_PACKAGE_NAME); | |
258 | + } catch (BadParcelableException e) { | |
259 | + Log.w(TAG, "ignore BadParcelableException", e); | |
260 | + return; | |
261 | + } | |
262 | + if (exceptionDescription == null) { | |
263 | + Log.e(TAG, "onException, cannot get data"); | |
264 | + return; | |
265 | + } | |
266 | + | |
267 | + if (threadName != null && | |
268 | + !threadName.isEmpty() && | |
269 | + !threadName.equals(MAIN_THREAD)) { | |
270 | + exceptionDescription = "Thread: " + threadName + " " + exceptionDescription; | |
271 | + } | |
272 | + | |
273 | + mLogHelper.newExceptionBuilder(exceptionDescription) | |
274 | + .setPackageDimensions(packageName) | |
275 | + .send(); | |
276 | + } | |
277 | + | |
278 | + private void onCustomEvent(Intent data) { | |
279 | + String event_category; | |
280 | + String event_action; | |
281 | + String event_label; | |
282 | + Long event_value; | |
283 | + String packageName; | |
284 | + boolean hasSampling; | |
285 | + try { | |
286 | + event_category = data.getStringExtra(AnalyticsHelper.EXTRA_EVENT_CATEGORY); | |
287 | + event_action = data.getStringExtra(AnalyticsHelper.EXTRA_EVENT_ACTION); | |
288 | + event_label = data.getStringExtra(AnalyticsHelper.EXTRA_EVENT_LABEL); | |
289 | + event_value = (Long) data.getSerializableExtra(AnalyticsHelper.EXTRA_EVENT_VALUE); | |
290 | + packageName = data.getStringExtra(AnalyticsHelper.EXTRA_PACKAGE_NAME); | |
291 | + hasSampling = data.getBooleanExtra(AnalyticsHelper.EXTRA_HAS_SAMPLING, true); | |
292 | + } catch (BadParcelableException e) { | |
293 | + Log.w(TAG, "ignore BadParcelableException", e); | |
294 | + return; | |
295 | + } | |
296 | + mLogHelper.newEventBuilder( | |
297 | + event_category, event_action, event_label, event_value) | |
298 | + .setPackageDimensions(packageName) | |
299 | + .send(); | |
300 | + } | |
301 | + | |
302 | + private void initEventHandlers() { | |
303 | + mStaticEventHandlers.put(AnalyticsHelper.ACTION_HIT_SCREEN, new EventHandler() { | |
304 | + @Override | |
305 | + void onEvent(Intent intent) { | |
306 | + onHitScreen(intent); | |
307 | + } | |
308 | + }); | |
309 | + mStaticEventHandlers.put(AnalyticsHelper.ACTION_BOOT_COMPLETED, new EventHandler() { | |
310 | + @Override | |
311 | + void onEvent(Intent intent) { | |
312 | + // save boot completed time | |
313 | + saveScreenChangeTime(getCurrentTimeInSeconds()); | |
314 | + | |
315 | + onBootCompleted(intent); | |
316 | + } | |
317 | + }); | |
318 | + mStaticEventHandlers.put(AnalyticsHelper.ACTION_SHUTDOWN, new EventHandler() { | |
319 | + @Override | |
320 | + void onEvent(Intent intent) { | |
321 | + onShutdown(intent); | |
322 | + | |
323 | + // Note: add one extra screen off event to calculate correct screen on duration | |
324 | + // time | |
325 | + onScreenOff(null); | |
326 | + removeScreenChangeTime(); | |
327 | + } | |
328 | + }); | |
329 | + mStaticEventHandlers.put(AnalyticsHelper.ACTION_SCREEN_ON, new EventHandler() { | |
330 | + @Override | |
331 | + void onEvent(Intent intent) { | |
332 | + onScreenOn(intent); | |
333 | + } | |
334 | + }); | |
335 | + mStaticEventHandlers.put(AnalyticsHelper.ACTION_SCREEN_OFF, new EventHandler() { | |
336 | + @Override | |
337 | + void onEvent(Intent intent) { | |
338 | + onScreenOff(intent); | |
339 | + } | |
340 | + }); | |
341 | + mStaticEventHandlers.put(AnalyticsHelper.ACTION_EXCEPTION, new EventHandler() { | |
342 | + @Override | |
343 | + void onEvent(Intent intent) { | |
344 | + onException(intent); | |
345 | + } | |
346 | + }); | |
347 | + mStaticEventHandlers.put(AnalyticsHelper.ACTION_CUSTOM_EVENT, new EventHandler() { | |
348 | + @Override | |
349 | + void onEvent(Intent intent) { | |
350 | + onCustomEvent(intent); | |
351 | + } | |
352 | + }); | |
353 | + } | |
354 | + static abstract class EventHandler { | |
355 | + abstract void onEvent(Intent intent); | |
356 | + } | |
357 | +} |
@@ -0,0 +1,132 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +import android.content.Context; | |
19 | +import android.content.Intent; | |
20 | +import android.content.IntentFilter; | |
21 | +import android.os.BatteryManager; | |
22 | + | |
23 | +public class BatteryState { | |
24 | + | |
25 | + private final Intent mIntent; | |
26 | + | |
27 | + public BatteryState(Intent batteryChangedIntent) { | |
28 | + mIntent = batteryChangedIntent; | |
29 | + } | |
30 | + | |
31 | + /** | |
32 | + * Gets BatteryState or null. | |
33 | + */ | |
34 | + public static BatteryState of(Context context) { | |
35 | + Intent intent = context.registerReceiver(null, | |
36 | + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); | |
37 | + if (intent == null) { | |
38 | + return null; | |
39 | + } | |
40 | + return new BatteryState(intent); | |
41 | + } | |
42 | + | |
43 | + /** | |
44 | + * Gets BatteryManager.EXTRA_STATUS, return BatteryManager.BATTERY_STATUS_UNKNOWN | |
45 | + * if failed to get. | |
46 | + */ | |
47 | + public int getStatus() { | |
48 | + return mIntent.getIntExtra(BatteryManager.EXTRA_STATUS, | |
49 | + BatteryManager.BATTERY_STATUS_UNKNOWN); | |
50 | + } | |
51 | + | |
52 | + /** | |
53 | + * Gets BatteryManager.EXTRA_PLUGGED, return 0 if failed to get. | |
54 | + */ | |
55 | + public int getPlugged() { | |
56 | + return mIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); | |
57 | + } | |
58 | + | |
59 | + /** | |
60 | + * Gets BatteryManager.EXTRA_LEVEL, return -1 if failed to get. | |
61 | + */ | |
62 | + public int getLevel() { | |
63 | + return mIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); | |
64 | + } | |
65 | + | |
66 | + /** | |
67 | + * Gets BatteryManager.EXTRA_SCALE, return -1 if failed to get. | |
68 | + */ | |
69 | + public int getScale() { | |
70 | + return mIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); | |
71 | + } | |
72 | + | |
73 | + /** | |
74 | + * Gets battery percentage or -1 if failed. | |
75 | + */ | |
76 | + public float getLevelPercentage() { | |
77 | + int level = getLevel(); | |
78 | + int scale = getScale(); | |
79 | + if (level < 0 || scale <= 0) { | |
80 | + return -1; | |
81 | + } | |
82 | + return (100.0f * level) / scale; | |
83 | + } | |
84 | + | |
85 | + /** | |
86 | + * Whether battery is charging | |
87 | + */ | |
88 | + public boolean isCharging() { | |
89 | + return isCharging(getStatus(), getPlugged()); | |
90 | + } | |
91 | + | |
92 | + /** | |
93 | + * Whether battery is charging | |
94 | + * @param status corresponds to BatteryManager.EXTRA_STATUS | |
95 | + * @param plugged corresponds to BatteryManager.EXTRA_PLUGGED | |
96 | + */ | |
97 | + public static boolean isCharging(int status, int plugged) { | |
98 | + // fix bug: it's also charging when status is | |
99 | + // "BATTERY_STATUS_DISCHARGING, BATTERY_PLUGGED_*" after boot machine with plugged | |
100 | + return status == BatteryManager.BATTERY_STATUS_CHARGING || | |
101 | + status == BatteryManager.BATTERY_STATUS_FULL || | |
102 | + plugged != 0; | |
103 | + } | |
104 | + | |
105 | + private static final int MIN_BATTERY_PERCENTAGE = 5; | |
106 | + | |
107 | + /** | |
108 | + * Whether power is sufficient to do some heavy tasks | |
109 | + */ | |
110 | + public boolean isPowerSufficient() { | |
111 | + return isPowerSufficient(MIN_BATTERY_PERCENTAGE); | |
112 | + } | |
113 | + | |
114 | + /** | |
115 | + * Whether power is sufficient to do some heavy tasks | |
116 | + */ | |
117 | + private boolean isPowerSufficient(int minBatteryPercentage) { | |
118 | + return isCharging() || getLevelPercentage() > minBatteryPercentage; | |
119 | + } | |
120 | + | |
121 | + /** | |
122 | + * Whether power is sufficient to do some heavy tasks | |
123 | + */ | |
124 | + public static boolean isPowerSufficient(Context context) { | |
125 | + BatteryState state = of(context); | |
126 | + if (state == null) { | |
127 | + return false; | |
128 | + } | |
129 | + return state.isPowerSufficient(); | |
130 | + } | |
131 | + | |
132 | +} |
@@ -0,0 +1,80 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +import java.util.Calendar; | |
19 | +import java.util.GregorianCalendar; | |
20 | +import java.util.Random; | |
21 | + | |
22 | +import android.app.AlarmManager; | |
23 | +import android.app.PendingIntent; | |
24 | +import android.content.BroadcastReceiver; | |
25 | +import android.content.ComponentName; | |
26 | +import android.content.Context; | |
27 | +import android.content.Intent; | |
28 | +import android.util.Log; | |
29 | +import org.android_x86.analytics.AnalyticsHelper; | |
30 | + | |
31 | +public class BootCompletedReceiver extends BroadcastReceiver { | |
32 | + private static final String TAG = "BootCompletedReceiver"; | |
33 | + | |
34 | + public static final String ACTION_BOOT_COMPLETED = "org.android_x86.boot_completed"; | |
35 | + public static final String ACTION_SEND_LOGS = "org.android_x86.send_logs"; | |
36 | + | |
37 | + @Override | |
38 | + public void onReceive(Context context, Intent data) { | |
39 | + String action = data.getAction(); | |
40 | + Intent startIntent = new Intent(action); | |
41 | + startIntent.setComponent( | |
42 | + new ComponentName( | |
43 | + AnalyticsHelper.TARGET_PACKAGE_NAME, | |
44 | + AnalyticsHelper.TARGET_CLASS_NAME)); | |
45 | + if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) { | |
46 | + Log.w(TAG, "unknow action:" + action); | |
47 | + return; | |
48 | + } | |
49 | + context.startService(startIntent); | |
50 | + context.sendBroadcast(new Intent(ACTION_BOOT_COMPLETED)); | |
51 | + | |
52 | + // Set alarm to send logs periodically | |
53 | + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); | |
54 | + Intent intent = new Intent(ACTION_SEND_LOGS); | |
55 | + am.setInexactRepeating(AlarmManager.RTC, getStartTime(), AlarmManager.INTERVAL_HALF_HOUR, | |
56 | + PendingIntent.getBroadcast(context, 0, intent, 0)); | |
57 | + } | |
58 | + | |
59 | + // begin and end hour that we enable sending logs | |
60 | + private static final int BEGIN_HOUR = 0; | |
61 | + private static final int END_HOUR = 24; | |
62 | + private static final int DELAY_SECONDS = 10; | |
63 | + | |
64 | + /** | |
65 | + * Gets start time in [BEGIN_HOUR, END_HOUR) | |
66 | + */ | |
67 | + private static long getStartTime() { | |
68 | + GregorianCalendar calendar = new GregorianCalendar(); | |
69 | + int currentHour = calendar.get(Calendar.HOUR_OF_DAY); | |
70 | + if (currentHour >= BEGIN_HOUR || currentHour < END_HOUR) { | |
71 | + // start after DELAY_SECONDS if boot in [BEGIN_HOUR, END_HOUR) | |
72 | + calendar.add(Calendar.SECOND, DELAY_SECONDS); | |
73 | + } else { | |
74 | + // choose time in [BEGIN_HOUR, END_HOUR) randomly | |
75 | + int startHour = BEGIN_HOUR + new Random().nextInt(END_HOUR + 24 - BEGIN_HOUR); | |
76 | + calendar.add(Calendar.HOUR_OF_DAY, startHour - currentHour); | |
77 | + } | |
78 | + return calendar.getTimeInMillis(); | |
79 | + } | |
80 | +} |
@@ -0,0 +1,76 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +class Fields { | |
19 | + | |
20 | + enum Metric { | |
21 | + METRIC_POWER_ON_NOT_INCLUDE_SLEEP(1); | |
22 | + | |
23 | + private final int value; | |
24 | + Metric(int value) { this.value = value; } | |
25 | + public int getValue() { return value; } | |
26 | + } | |
27 | + | |
28 | + enum FieldEnum { | |
29 | + // Google analytics fields, see http://goo.gl/M6dK2U | |
30 | + // Common fields | |
31 | + APP_ID(1), | |
32 | + APP_VERSION(2), | |
33 | + APP_NAME(3), | |
34 | + SCREEN_NAME(4); | |
35 | + | |
36 | + private final int value; | |
37 | + FieldEnum(int value) { this.value = value; } | |
38 | + public int getValue() { return value; } | |
39 | + } | |
40 | + | |
41 | + // Custom dimension | |
42 | + enum Dimension { | |
43 | + DIMENSION_BUILD_TYPE(1), | |
44 | + DIMENSION_BUILD_FLAVOR(2), | |
45 | + DIMENSION_DEVICE(3), | |
46 | + DIMENSION_MODEL(4), | |
47 | + DIMENSION_BUILD_VERSION(5), | |
48 | + DIMENSION_POWER_TYPE(6), | |
49 | + DIMENSION_INPUT_TYPE(7), | |
50 | + DIMENSION_DISPLAY_TYPE(8), | |
51 | + DIMENSION_TAG(9), | |
52 | + DIMENSION_NETWORK_TYPE(10), | |
53 | + DIMENSION_RESERVED(11), | |
54 | + DIMENSION_RESOLUTION(12), | |
55 | + DIMENSION_DENSITY(13); | |
56 | + | |
57 | + private final int value; | |
58 | + Dimension(int value) { this.value = value; } | |
59 | + public int getValue() { return value; } | |
60 | + } | |
61 | + | |
62 | + enum Type { | |
63 | + // Use int_key, see FieldEnum | |
64 | + DEFAULT(0), | |
65 | + // Use int_key, correspond to google analytics' custom dimension | |
66 | + CUSTOM_DIMENSION(1), | |
67 | + // Use int_key, correspond to google analytics' custom metric | |
68 | + CUSTOM_METRIC(2), | |
69 | + // Use keys | |
70 | + CUSTOM_GENERAL_LOG(3); | |
71 | + | |
72 | + private final int value; | |
73 | + Type(int value) { this.value = value; } | |
74 | + public int getValue() { return value; } | |
75 | + } | |
76 | +} |
@@ -0,0 +1,139 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +import android.app.Service; | |
19 | +import android.content.Intent; | |
20 | +import android.os.Handler; | |
21 | +import android.os.HandlerThread; | |
22 | +import android.os.IBinder; | |
23 | +import android.os.Looper; | |
24 | +import android.os.Message; | |
25 | + | |
26 | +/** | |
27 | + * @see android.app.IntentService | |
28 | + * not stopSelf(msg.arg1) | |
29 | + */ | |
30 | +public abstract class ImmortalIntentService extends Service { | |
31 | + private volatile Looper mServiceLooper; | |
32 | + private volatile ServiceHandler mServiceHandler; | |
33 | + private String mName; | |
34 | + private boolean mRedelivery; | |
35 | + | |
36 | + private final class ServiceHandler extends Handler { | |
37 | + public ServiceHandler(Looper looper) { | |
38 | + super(looper); | |
39 | + } | |
40 | + | |
41 | + @Override | |
42 | + public void handleMessage(Message msg) { | |
43 | + onHandleIntent((Intent)msg.obj); | |
44 | + } | |
45 | + } | |
46 | + | |
47 | + /** | |
48 | + * Creates an IntentService. Invoked by your subclass's constructor. | |
49 | + * | |
50 | + * @param name Used to name the worker thread, important only for debugging. | |
51 | + */ | |
52 | + public ImmortalIntentService(String name) { | |
53 | + super(); | |
54 | + mName = name; | |
55 | + } | |
56 | + | |
57 | + /** | |
58 | + * Sets intent redelivery preferences. Usually called from the constructor | |
59 | + * with your preferred semantics. | |
60 | + * | |
61 | + * <p>If enabled is true, | |
62 | + * {@link #onStartCommand(Intent, int, int)} will return | |
63 | + * {@link Service#START_REDELIVER_INTENT}, so if this process dies before | |
64 | + * {@link #onHandleIntent(Intent)} returns, the process will be restarted | |
65 | + * and the intent redelivered. If multiple Intents have been sent, only | |
66 | + * the most recent one is guaranteed to be redelivered. | |
67 | + * | |
68 | + * <p>If enabled is false (the default), | |
69 | + * {@link #onStartCommand(Intent, int, int)} will return | |
70 | + * {@link Service#START_NOT_STICKY}, and if the process dies, the Intent | |
71 | + * dies along with it. | |
72 | + */ | |
73 | + public void setIntentRedelivery(boolean enabled) { | |
74 | + mRedelivery = enabled; | |
75 | + } | |
76 | + | |
77 | + @Override | |
78 | + public void onCreate() { | |
79 | + // TODO: It would be nice to have an option to hold a partial wakelock | |
80 | + // during processing, and to have a static startService(Context, Intent) | |
81 | + // method that would launch the service & hand off a wakelock. | |
82 | + | |
83 | + super.onCreate(); | |
84 | + HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); | |
85 | + thread.start(); | |
86 | + | |
87 | + mServiceLooper = thread.getLooper(); | |
88 | + mServiceHandler = new ServiceHandler(mServiceLooper); | |
89 | + } | |
90 | + | |
91 | + @Override | |
92 | + public void onStart(Intent intent, int startId) { | |
93 | + Message msg = mServiceHandler.obtainMessage(); | |
94 | + msg.arg1 = startId; | |
95 | + msg.obj = intent; | |
96 | + mServiceHandler.sendMessage(msg); | |
97 | + } | |
98 | + | |
99 | + /** | |
100 | + * You should not override this method for your IntentService. Instead, | |
101 | + * override {@link #onHandleIntent}, which the system calls when the IntentService | |
102 | + * receives a start request. | |
103 | + * @see android.app.Service#onStartCommand | |
104 | + */ | |
105 | + @Override | |
106 | + public int onStartCommand(Intent intent, int flags, int startId) { | |
107 | + onStart(intent, startId); | |
108 | + return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; | |
109 | + } | |
110 | + | |
111 | + @Override | |
112 | + public void onDestroy() { | |
113 | + mServiceLooper.quit(); | |
114 | + } | |
115 | + | |
116 | + /** | |
117 | + * Unless you provide binding for your service, you don't need to implement this | |
118 | + * method, because the default implementation returns null. | |
119 | + * @see android.app.Service#onBind | |
120 | + */ | |
121 | + @Override | |
122 | + public IBinder onBind(Intent intent) { | |
123 | + return null; | |
124 | + } | |
125 | + | |
126 | + /** | |
127 | + * This method is invoked on the worker thread with a request to process. | |
128 | + * Only one Intent is processed at a time, but the processing happens on a | |
129 | + * worker thread that runs independently from other application logic. | |
130 | + * So, if this code takes a long time, it will hold up other requests to | |
131 | + * the same IntentService, but it will not hold up anything else. | |
132 | + * When all requests have been handled, the IntentService stops itself, | |
133 | + * so you should not call {@link #stopSelf}. | |
134 | + * | |
135 | + * @param intent The value passed to {@link | |
136 | + * android.content.Context#startService(Intent)}. | |
137 | + */ | |
138 | + protected abstract void onHandleIntent(Intent intent); | |
139 | +} |
@@ -0,0 +1,169 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +import java.util.HashSet; | |
19 | +import java.util.Map; | |
20 | +import java.util.Set; | |
21 | + | |
22 | +import android.content.ComponentName; | |
23 | +import android.content.Context; | |
24 | +import android.os.Build; | |
25 | +import android.util.DisplayMetrics; | |
26 | +import android.util.Log; | |
27 | + | |
28 | +import com.google.analytics.tracking.android.EasyTracker; | |
29 | +import com.google.analytics.tracking.android.Fields; | |
30 | +import com.google.analytics.tracking.android.MapBuilder; | |
31 | +import com.google.analytics.tracking.android.Tracker; | |
32 | + | |
33 | +import org.android_x86.analytics.AnalyticsHelper; | |
34 | +import org.android_x86.analytics.GeneralLogs; | |
35 | +import org.android_x86.analytics.Fields.Type; | |
36 | +import org.android_x86.analytics.Fields.FieldEnum; | |
37 | +import org.android_x86.analytics.Fields.Metric; | |
38 | +import org.android_x86.analytics.Fields.Dimension; | |
39 | + | |
40 | +public class LogHelper { | |
41 | + private static final String TAG = "LogHelper"; | |
42 | + private static final boolean DEBUG = AnalyticsHelper.DEBUG; | |
43 | + | |
44 | + private static final double SAMPLING_RATE = 0; //TODO | |
45 | + | |
46 | + private final Context mContext; | |
47 | + private final Tracker mTracker; | |
48 | + | |
49 | + /** | |
50 | + * Log to Google analytics | |
51 | + */ | |
52 | + public LogHelper(Context context) { | |
53 | + mContext = context; | |
54 | + mTracker = EasyTracker.getInstance(context); | |
55 | + } | |
56 | + | |
57 | + public LogBuilder newAppViewBuilder() { | |
58 | + return new LogBuilder(MapBuilder.createAppView()); | |
59 | + } | |
60 | + | |
61 | + public LogBuilder newEventBuilder( | |
62 | + String category, String action, String label, Long value) { | |
63 | + MapBuilder mapBuilder = MapBuilder.createEvent(category, action, label, value); | |
64 | + LogBuilder builder = new LogBuilder(mapBuilder); | |
65 | + return builder; | |
66 | + } | |
67 | + | |
68 | + public LogBuilder newExceptionBuilder(String exceptionDescription) { | |
69 | + return new LogBuilder(MapBuilder.createException(exceptionDescription, true)); | |
70 | + } | |
71 | + | |
72 | + public class LogBuilder { | |
73 | + private final MapBuilder mBuilder; | |
74 | + | |
75 | + // do not check if field redundant! | |
76 | + | |
77 | + private LogBuilder(MapBuilder builder) { | |
78 | + mBuilder = builder; | |
79 | + } | |
80 | + | |
81 | + /** | |
82 | + * Sets common field | |
83 | + */ | |
84 | + private void setCommonField(FieldEnum key, String value) { | |
85 | + if (mBuilder == null) return; | |
86 | + switch (key) { | |
87 | + case APP_ID: | |
88 | + mBuilder.set(Fields.APP_ID, value); | |
89 | + break; | |
90 | + case APP_VERSION: | |
91 | + mBuilder.set(Fields.APP_VERSION, value); | |
92 | + break; | |
93 | + case APP_NAME: | |
94 | + mBuilder.set(Fields.APP_NAME, value); | |
95 | + break; | |
96 | + case SCREEN_NAME: | |
97 | + mBuilder.set(Fields.SCREEN_NAME, value); | |
98 | + break; | |
99 | + default: | |
100 | + throw new UnsupportedOperationException("Invalid field: " + key); | |
101 | + } | |
102 | + } | |
103 | + | |
104 | + /** | |
105 | + * Sets Google Analytics field without check | |
106 | + */ | |
107 | + private <T> LogBuilder set(Type type, T key, Object value) { | |
108 | + if (mBuilder == null) return this; | |
109 | + switch (type) { | |
110 | + case DEFAULT: | |
111 | + setCommonField((FieldEnum)key, value.toString()); | |
112 | + break; | |
113 | + case CUSTOM_DIMENSION: | |
114 | + mBuilder.set(Fields.customDimension(((Dimension)key).getValue()), value.toString()); | |
115 | + break; | |
116 | + case CUSTOM_METRIC: | |
117 | + mBuilder.set(Fields.customMetric(((Metric)key).getValue()), value.toString()); | |
118 | + break; | |
119 | + default: | |
120 | + throw new UnsupportedOperationException("Invalid Type: " + type + ", key: " | |
121 | + + key + ", value=" + value); | |
122 | + } | |
123 | + return this; | |
124 | + } | |
125 | + | |
126 | + public LogBuilder setPower(long powerOnNotSleep) { | |
127 | + set(Type.CUSTOM_METRIC, Metric.METRIC_POWER_ON_NOT_INCLUDE_SLEEP, powerOnNotSleep); | |
128 | + return this; | |
129 | + } | |
130 | + | |
131 | + public LogBuilder setPackageDimensions(String packageName) { | |
132 | + // removed a lots of informations! | |
133 | + set(Type.DEFAULT, FieldEnum.APP_NAME, packageName); | |
134 | + return this; | |
135 | + } | |
136 | + | |
137 | + public LogBuilder setActivityDimensions(ComponentName component) { | |
138 | + // removed a lots of informations! | |
139 | + set(Type.DEFAULT, FieldEnum.APP_NAME, component.getPackageName()); | |
140 | + set(Type.DEFAULT, FieldEnum.SCREEN_NAME, component.getClassName()); | |
141 | + return this; | |
142 | + } | |
143 | + | |
144 | + public void send() { | |
145 | + // removed a lots of informations! | |
146 | + | |
147 | + if (mBuilder != null) { | |
148 | + // Add common fields to MapBuilder | |
149 | + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_BUILD_TYPE, Build.TYPE); | |
150 | + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_DEVICE, Build.DEVICE); | |
151 | + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_MODEL, Build.MODEL); | |
152 | + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_BUILD_VERSION, Util.BuildUtil.getBuildVersion()); | |
153 | + | |
154 | + DisplayMetrics metrics = Util.getDefaultDisplayMetrics(mContext); | |
155 | + float rate = Util.getDefaultDisplayRefreshRate(mContext); | |
156 | + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_RESOLUTION, | |
157 | + metrics.widthPixels + " * " + metrics.heightPixels + " " | |
158 | + + Integer.toString((int) rate) + "Hz"); | |
159 | + set(Type.CUSTOM_DIMENSION, Dimension.DIMENSION_DENSITY, metrics.densityDpi); | |
160 | + | |
161 | + Map<String, String> map = mBuilder.build(); | |
162 | + if (DEBUG) { | |
163 | + Log.d(TAG, "Google Analytics log entry: " + map); | |
164 | + } | |
165 | + mTracker.send(map); | |
166 | + } | |
167 | + } | |
168 | + } | |
169 | +} |
@@ -0,0 +1,114 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +import android.content.Context; | |
19 | +import android.os.SystemClock; | |
20 | + | |
21 | +public class PowerStats { | |
22 | + private static final long MIN_STATS_INTERVAL_MILLIS = 15 * 60 * 1000; | |
23 | + private static final long TEN_HOUR_MILLIS = 10 * 60 * 60 * 1000; | |
24 | + | |
25 | + private static final String EVENT_CATEGORY_POWER_USAGE = "power_usage"; | |
26 | + private static final String ACTION_DISCHARGE_SCREEN_ON = "discharge_screen_on"; | |
27 | + private static final String ACTION_DISCHARGE_SCREEN_OFF = "discharge_screen_off"; | |
28 | + | |
29 | + private final boolean mIsScreenOn; | |
30 | + private final boolean mIsCharging; | |
31 | + private final long mTime; | |
32 | + private final float mPercentage; | |
33 | + | |
34 | + private static PowerStats mPowerStats; | |
35 | + | |
36 | + private PowerStats(boolean isScreenOn, boolean isCharging, long time, float percentage) { | |
37 | + mIsScreenOn = isScreenOn; | |
38 | + mIsCharging = isCharging; | |
39 | + mTime = time; | |
40 | + mPercentage = percentage; | |
41 | + } | |
42 | + | |
43 | + @Override | |
44 | + public String toString() { | |
45 | + return "isScreenOn: " + mIsScreenOn | |
46 | + + " isCharging: " + mIsCharging | |
47 | + + " time: " + mTime | |
48 | + + " percentage: " + mPercentage; | |
49 | + } | |
50 | + | |
51 | + public static void onScreenOn(Context context) { | |
52 | + onIntent(context, true, null); | |
53 | + } | |
54 | + | |
55 | + public static void onScreenOff(Context context) { | |
56 | + onIntent(context, false, null); | |
57 | + } | |
58 | + | |
59 | + public static void onPowerConnected(Context context) { | |
60 | + onIntent(context, null, true); | |
61 | + } | |
62 | + | |
63 | + public static void onPowerDisconnected(Context context) { | |
64 | + onIntent(context, null, false); | |
65 | + } | |
66 | + | |
67 | + private static void onIntent(Context context, Boolean isScreenOn, Boolean isCharging) { | |
68 | + BatteryState state = BatteryState.of(context); | |
69 | + if (state == null) { | |
70 | + return; | |
71 | + } | |
72 | + float percentage = state.getLevelPercentage(); | |
73 | + if (percentage < 0) { | |
74 | + return; | |
75 | + } | |
76 | + new PowerStats( | |
77 | + isScreenOn != null ? isScreenOn : Util.isScreenOn(context), | |
78 | + isCharging != null ? isCharging : state.isCharging(), | |
79 | + SystemClock.elapsedRealtime(), | |
80 | + percentage) | |
81 | + .handle(context); | |
82 | + } | |
83 | + | |
84 | + private void handle(Context context) { | |
85 | + PowerStats previous; | |
86 | + synchronized(PowerStats.class) { | |
87 | + previous = mPowerStats; | |
88 | + // only save battery percentage when screen on/off or charging status change. | |
89 | + if (previous != null | |
90 | + && (mIsScreenOn == previous.mIsScreenOn) | |
91 | + && (mIsCharging == previous.mIsCharging)) { | |
92 | + return; | |
93 | + } | |
94 | + mPowerStats = this; | |
95 | + } | |
96 | + if (previous == null) { | |
97 | + return; | |
98 | + } | |
99 | + long interval = mTime - previous.mTime; | |
100 | + float percentageReduce = previous.mPercentage - mPercentage; | |
101 | + if (interval > MIN_STATS_INTERVAL_MILLIS | |
102 | + && percentageReduce >= 0 | |
103 | + && !previous.mIsCharging) { | |
104 | + long value = (long) (percentageReduce * TEN_HOUR_MILLIS / interval); | |
105 | + AnalyticsHelper.newSystemCoreEvent(context, | |
106 | + EVENT_CATEGORY_POWER_USAGE, | |
107 | + previous.mIsScreenOn ? ACTION_DISCHARGE_SCREEN_ON | |
108 | + : ACTION_DISCHARGE_SCREEN_OFF) | |
109 | + .setLabel(Long.toString(value)) | |
110 | + .setValue(value) | |
111 | + .sendWithSampling(); | |
112 | + } | |
113 | + } | |
114 | +} |
@@ -0,0 +1,439 @@ | ||
1 | +/* | |
2 | + * Copyright 2016 Jide Technology Ltd. | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.android_x86.analytics; | |
17 | + | |
18 | +import java.io.ByteArrayOutputStream; | |
19 | +import java.io.IOException; | |
20 | +import java.io.UnsupportedEncodingException; | |
21 | +import java.io.InputStream; | |
22 | +import java.io.OutputStream; | |
23 | +import java.security.MessageDigest; | |
24 | +import java.security.NoSuchAlgorithmException; | |
25 | +import java.util.Formatter; | |
26 | +import java.util.Random; | |
27 | +import java.util.Collections; | |
28 | +import java.util.ArrayList; | |
29 | +import java.util.List; | |
30 | +import java.net.MalformedURLException; | |
31 | +import java.net.URI; | |
32 | +import java.net.URISyntaxException; | |
33 | +import java.net.URL; | |
34 | +import java.net.HttpURLConnection; | |
35 | + | |
36 | +import org.apache.http.HttpEntity; | |
37 | +import org.apache.http.HttpResponse; | |
38 | +import org.apache.http.HttpStatus; | |
39 | +import org.apache.http.StatusLine; | |
40 | +import org.apache.http.client.HttpClient; | |
41 | +import org.apache.http.client.methods.HttpPost; | |
42 | +import org.apache.http.impl.client.DefaultHttpClient; | |
43 | +import org.apache.http.params.CoreProtocolPNames; | |
44 | + | |
45 | +import org.json.JSONException; | |
46 | +import org.json.JSONObject; | |
47 | + | |
48 | +import android.os.PowerManager; | |
49 | +import android.os.Bundle; | |
50 | +import android.os.Build; | |
51 | +import android.content.Context; | |
52 | +import android.content.Intent; | |
53 | +import android.util.DisplayMetrics; | |
54 | +import android.view.WindowManager; | |
55 | +import android.net.ConnectivityManager; | |
56 | +import android.net.NetworkInfo; | |
57 | +import android.net.wifi.WifiInfo; | |
58 | +import android.net.wifi.WifiManager; | |
59 | +import android.provider.Settings.Secure; | |
60 | + | |
61 | +public class Util { | |
62 | + private Util() { | |
63 | + } | |
64 | + | |
65 | + public static DisplayMetrics getDefaultDisplayMetrics(Context context) { | |
66 | + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); | |
67 | + DisplayMetrics metrics = new DisplayMetrics(); | |
68 | + wm.getDefaultDisplay().getRealMetrics(metrics); | |
69 | + return metrics; | |
70 | + } | |
71 | + | |
72 | + public static float getDefaultDisplayRefreshRate(Context context) { | |
73 | + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); | |
74 | + return wm.getDefaultDisplay().getRefreshRate(); | |
75 | + } | |
76 | + | |
77 | + /* --- DebugUtil --- */ | |
78 | + | |
79 | + /** | |
80 | + * Gets Intent's debug string with extras. | |
81 | + */ | |
82 | + public static String toString(Intent intent) { | |
83 | + StringBuilder sb = new StringBuilder(); | |
84 | + sb.append(intent.getAction()); | |
85 | + Bundle extras = intent.getExtras(); | |
86 | + if (extras != null && !extras.isEmpty()) { | |
87 | + sb.append(" " + extras); | |
88 | + } | |
89 | + return sb.toString(); | |
90 | + } | |
91 | + | |
92 | + /* --- PowerUtil --- */ | |
93 | + | |
94 | + public static boolean isScreenOn(Context context) { | |
95 | + return ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).isScreenOn(); | |
96 | + } | |
97 | + | |
98 | + /* --- BuildUtil --- */ | |
99 | + | |
100 | + public static class BuildUtil { | |
101 | + | |
102 | + public static final String UNKNOWN = "unknown"; | |
103 | + | |
104 | + public static final String NIGHTLY = "nightly"; | |
105 | + public static final String BETA = "beta"; | |
106 | + public static final String STABLE = "stable"; | |
107 | + | |
108 | + /** | |
109 | + * Gets locale string. | |
110 | + */ | |
111 | + public static String getLocale(Context context) { | |
112 | + return context.getResources().getConfiguration().locale.toString(); | |
113 | + } | |
114 | + | |
115 | + /** | |
116 | + * Gets build flavor | |
117 | + */ | |
118 | + public static String getFlavor() { | |
119 | + String version = Build.VERSION.INCREMENTAL; | |
120 | + if (!version.isEmpty()) { | |
121 | + switch (version.charAt(0)) { | |
122 | + case 'N': | |
123 | + return NIGHTLY; | |
124 | + case 'B': | |
125 | + return BETA; | |
126 | + case 'S': | |
127 | + return STABLE; | |
128 | + } | |
129 | + } | |
130 | + return UNKNOWN; | |
131 | + } | |
132 | + | |
133 | + /** | |
134 | + * Gets ANDROID_ID or empty string if not present. | |
135 | + */ | |
136 | + public static String getAndroidID(Context context) { | |
137 | + String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); | |
138 | + return androidId == null ? "" : androidId; | |
139 | + } | |
140 | + | |
141 | + /** | |
142 | + * Gets full build version. | |
143 | + */ | |
144 | + public static String getBuildVersion() { | |
145 | + if (Build.VERSION.INCREMENTAL.startsWith(Build.VERSION.RELEASE)) { | |
146 | + return Build.VERSION.INCREMENTAL; | |
147 | + } | |
148 | + return Build.VERSION.RELEASE + '-' + Build.VERSION.INCREMENTAL; | |
149 | + } | |
150 | + } | |
151 | + | |
152 | + /* --- IOUtil --- */ | |
153 | + | |
154 | + public static class IOUtil { | |
155 | + | |
156 | + public static final String UTF8 = "UTF-8"; | |
157 | + | |
158 | + private static final int BUFFER_SIZE = 16 * 1024; | |
159 | + | |
160 | + /** | |
161 | + * Reads input stream to an output stream and close input stream (not close output stream). | |
162 | + * @param in | |
163 | + * @param out | |
164 | + * @throws IOException | |
165 | + */ | |
166 | + public static void toOutputStream(InputStream in, OutputStream out) throws IOException { | |
167 | + try { | |
168 | + byte[] buffer = new byte[BUFFER_SIZE]; | |
169 | + int size; | |
170 | + while ((size = in.read(buffer)) != -1) { | |
171 | + out.write(buffer, 0, size); | |
172 | + } | |
173 | + } finally { | |
174 | + in.close(); | |
175 | + } | |
176 | + } | |
177 | + | |
178 | + /** | |
179 | + * Writes byte array to output stream and close output stream. | |
180 | + * @throws IOException | |
181 | + */ | |
182 | + public static void toAndCloseOutputStream(byte[] data, OutputStream out) throws IOException { | |
183 | + try { | |
184 | + out.write(data); | |
185 | + } finally { | |
186 | + out.close(); | |
187 | + } | |
188 | + } | |
189 | + | |
190 | + /** | |
191 | + * Writes a string to output stream and close output stream. | |
192 | + * @throws IOException | |
193 | + */ | |
194 | + public static void toAndCloseOutputStream(String s, OutputStream out) throws IOException { | |
195 | + toAndCloseOutputStream(s.getBytes(UTF8), out); | |
196 | + } | |
197 | + | |
198 | + /** | |
199 | + * Reads input stream to a string and close input stream. | |
200 | + * @throws IOException | |
201 | + */ | |
202 | + public static String toString(InputStream in) throws IOException { | |
203 | + ByteArrayOutputStream out = new ByteArrayOutputStream(); | |
204 | + try { | |
205 | + toOutputStream(in, out); | |
206 | + return out.toString(UTF8); | |
207 | + } finally { | |
208 | + out.close(); | |
209 | + } | |
210 | + } | |
211 | + } | |
212 | + | |
213 | + /* --- HttpUtil --- */ | |
214 | + | |
215 | + public static class HttpStatusException extends Exception { | |
216 | + private final int mStatusCode; | |
217 | + | |
218 | + public HttpStatusException(int statusCode) { | |
219 | + super(); | |
220 | + mStatusCode = statusCode; | |
221 | + } | |
222 | + | |
223 | + public HttpStatusException(int statusCode, String message) { | |
224 | + super(message); | |
225 | + mStatusCode = statusCode; | |
226 | + } | |
227 | + | |
228 | + @Override | |
229 | + public String getMessage() { | |
230 | + return "StatusCode: " + mStatusCode + " " + super.getMessage(); | |
231 | + } | |
232 | + | |
233 | + public int getStatusCode() { | |
234 | + return mStatusCode; | |
235 | + } | |
236 | + } | |
237 | + | |
238 | + public static class HttpStatusLineException extends HttpStatusException { | |
239 | + private final StatusLine mStatusLine; | |
240 | + | |
241 | + public HttpStatusLineException(StatusLine statusLine) { | |
242 | + super(statusLine.getStatusCode()); | |
243 | + mStatusLine = statusLine; | |
244 | + } | |
245 | + | |
246 | + public HttpStatusLineException(StatusLine statusLine, String message) { | |
247 | + super(statusLine.getStatusCode(), message); | |
248 | + mStatusLine = statusLine; | |
249 | + } | |
250 | + | |
251 | + @Override | |
252 | + public String getMessage() { | |
253 | + return "StatusLine: " + mStatusLine + " " + super.getMessage(); | |
254 | + } | |
255 | + | |
256 | + public StatusLine getStatusLine() { | |
257 | + return mStatusLine; | |
258 | + } | |
259 | + } | |
260 | + | |
261 | + /** | |
262 | + * HTTP post to given URL. | |
263 | + */ | |
264 | + public static HttpEntity doPost(String url, HttpEntity entity) | |
265 | + throws URISyntaxException, IOException, HttpStatusLineException { | |
266 | + HttpClient client = new DefaultHttpClient(); | |
267 | + client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, "android"); | |
268 | + HttpPost request = new HttpPost(); | |
269 | + request.setURI(new URI(url)); | |
270 | + request.setEntity(entity); | |
271 | + | |
272 | + HttpResponse response = client.execute(request); | |
273 | + StatusLine statusLine = response.getStatusLine(); | |
274 | + if (statusLine.getStatusCode() != HttpStatus.SC_OK) { | |
275 | + throw new HttpStatusLineException(statusLine); | |
276 | + } | |
277 | + return response.getEntity(); | |
278 | + } | |
279 | + | |
280 | + /** | |
281 | + * Gets JSON from URL | |
282 | + */ | |
283 | + public static JSONObject getJsonFromUrl(String url) | |
284 | + throws MalformedURLException, JSONException, IOException { | |
285 | + return new JSONObject(IOUtil.toString(new URL(url).openStream())); | |
286 | + } | |
287 | + | |
288 | + /** | |
289 | + * Posts JSON request and get JSON reply. | |
290 | + */ | |
291 | + public static JSONObject postAndGetJson(String url, String jsonRequest) | |
292 | + throws MalformedURLException, IOException, HttpStatusException, JSONException { | |
293 | + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); | |
294 | + conn.setDoOutput(true); | |
295 | + conn.setDoInput(true); | |
296 | + conn.setRequestMethod("POST"); | |
297 | + conn.setRequestProperty("Content-Type", "application/json; charset=utf8"); | |
298 | + conn.setRequestProperty("Accept", "application/json"); | |
299 | + IOUtil.toAndCloseOutputStream(jsonRequest, conn.getOutputStream()); | |
300 | + | |
301 | + int responseCode = conn.getResponseCode(); | |
302 | + if (responseCode != HttpURLConnection.HTTP_OK) { | |
303 | + throw new HttpStatusException(responseCode); | |
304 | + } | |
305 | + return new JSONObject(IOUtil.toString(conn.getInputStream())); | |
306 | + } | |
307 | + | |
308 | + /** | |
309 | + * Posts JSON request and get JSON reply. | |
310 | + */ | |
311 | + public static JSONObject postAndGetJson(String url, JSONObject jsonRequest) | |
312 | + throws MalformedURLException, IOException, HttpStatusException, JSONException { | |
313 | + return postAndGetJson(url, jsonRequest.toString()); | |
314 | + } | |
315 | + | |
316 | + /* --- NetUtil --- */ | |
317 | + | |
318 | + public static ConnectivityManager getConnectivityManager(Context context) { | |
319 | + return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); | |
320 | + } | |
321 | + | |
322 | + /** | |
323 | + * Whether network is good to send cloud server requests | |
324 | + */ | |
325 | + public static boolean isNetworkGood(ConnectivityManager cm) { | |
326 | + NetworkInfo info = cm.getActiveNetworkInfo(); | |
327 | + return info != null && info.isConnected(); | |
328 | + } | |
329 | + | |
330 | + /** | |
331 | + * Whether network is good to send cloud server requests | |
332 | + */ | |
333 | + public static boolean isNetworkGood(Context context) { | |
334 | + return isNetworkGood(getConnectivityManager(context)); | |
335 | + } | |
336 | + | |
337 | + /** | |
338 | + * Gets MAC address or empty string. | |
339 | + */ | |
340 | + public static String getMacAddress(Context context) { | |
341 | + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); | |
342 | + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); | |
343 | + if (wifiInfo != null) { | |
344 | + String mac = wifiInfo.getMacAddress(); | |
345 | + if (mac != null) { | |
346 | + return mac; | |
347 | + } | |
348 | + } | |
349 | + return ""; | |
350 | + } | |
351 | + | |
352 | + /* --- SecurityUtil --- */ | |
353 | + | |
354 | + public static class SecurityUtil { | |
355 | + | |
356 | + /** | |
357 | + * Converts bytes to hex string. | |
358 | + */ | |
359 | + public static String byteToHex(byte[] bytes) { | |
360 | + Formatter formatter = new Formatter(); | |
361 | + for (byte b : bytes) { | |
362 | + formatter.format("%02x", b); | |
363 | + } | |
364 | + String result = formatter.toString(); | |
365 | + formatter.close(); | |
366 | + return result; | |
367 | + } | |
368 | + | |
369 | + /** | |
370 | + * Use SHA-1 algorithm to hash given string and return a hex string. | |
371 | + */ | |
372 | + public static String sha1(String s) | |
373 | + throws NoSuchAlgorithmException, UnsupportedEncodingException { | |
374 | + return byteToHex(MessageDigest.getInstance("SHA-1").digest(s.getBytes("UTF-8"))); | |
375 | + } | |
376 | + | |
377 | + } | |
378 | + | |
379 | + /* --- SignatureBuilder --- */ | |
380 | + | |
381 | + public static class SignatureBuilder { | |
382 | + private final List<String> mInfos; | |
383 | + | |
384 | + private SignatureBuilder(String... infos) { | |
385 | + mInfos = new ArrayList<String>(); | |
386 | + for (String s : infos) { | |
387 | + mInfos.add(s); | |
388 | + } | |
389 | + } | |
390 | + | |
391 | + /** | |
392 | + * Gets a random SignatureBuilder | |
393 | + */ | |
394 | + public static SignatureBuilder of() { | |
395 | + return new SignatureBuilder( | |
396 | + String.valueOf(System.currentTimeMillis()), | |
397 | + String.valueOf(new Random().nextLong()), | |
398 | + Build.SERIAL); | |
399 | + } | |
400 | + | |
401 | + /** | |
402 | + * Gets informations used to generate signature. | |
403 | + */ | |
404 | + public List<String> getInfos() { | |
405 | + return mInfos; | |
406 | + } | |
407 | + | |
408 | + /** | |
409 | + * Builds signature with secret key and separator. | |
410 | + */ | |
411 | + public String build(String key, String separator) | |
412 | + throws NoSuchAlgorithmException, UnsupportedEncodingException { | |
413 | + List<String> list = new ArrayList<String>(mInfos); | |
414 | + list.add(key); | |
415 | + Collections.sort(list); | |
416 | + | |
417 | + StringBuilder sb = new StringBuilder(); | |
418 | + for (String s : list) { | |
419 | + sb.append(s).append(separator); | |
420 | + } | |
421 | + return SecurityUtil.sha1(sb.toString()); | |
422 | + } | |
423 | + | |
424 | + /** | |
425 | + * Builds signature with secret key and separator, return comma separated string of source | |
426 | + * informations and signature. | |
427 | + */ | |
428 | + public String buildToJoinString(String key, String separator) | |
429 | + throws NoSuchAlgorithmException, UnsupportedEncodingException { | |
430 | + String signature = build(key, separator); | |
431 | + StringBuilder sb = new StringBuilder(); | |
432 | + for (String s : mInfos) { | |
433 | + sb.append(s).append(','); | |
434 | + } | |
435 | + sb.append(signature); | |
436 | + return sb.toString(); | |
437 | + } | |
438 | + } | |
439 | +} |