diff options
author | gkipnis <gkipnis@Garys-MBP.sjc.cyngn.internal> | 2016-02-11 14:41:27 -0800 |
---|---|---|
committer | gkipnis <gkipnis@Garys-MBP.sjc.cyngn.internal> | 2016-02-11 14:41:27 -0800 |
commit | a19a59d45c8d92171aaf31361ddbdef82d6da0b7 (patch) | |
tree | e92fac0f57ca033ef05e89d72c3ce22da3b5498c | |
download | android_packages_providers_DataUsageProvider-a19a59d45c8d92171aaf31361ddbdef82d6da0b7.tar.gz android_packages_providers_DataUsageProvider-a19a59d45c8d92171aaf31361ddbdef82d6da0b7.tar.bz2 android_packages_providers_DataUsageProvider-a19a59d45c8d92171aaf31361ddbdef82d6da0b7.zip |
Initial checkin of the DataUsageProvider
Implements the provider, service and other misc logic
for the DataUsage Warning/Disable support that was added to the
Settings App.
33 files changed, 1727 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..59edc71 --- /dev/null +++ b/Android.mk @@ -0,0 +1,28 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +# LOCAL_JAVA_LIBRARIES := bouncycastle conscrypt telephony-common ims-common + +LOCAL_STATIC_JAVA_LIBRARIES := \ + org.cyanogenmod.platform.sdk \ + android-support-v4 \ + android-support-v13 \ + gson + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + +LOCAL_JAVA_LIBRARIES := + +LOCAL_PACKAGE_NAME := DataUsageProvider +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true + +# include frameworks/opt/setupwizard/navigationbar/common.mk +# include frameworks/opt/setupwizard/library/common.mk +# include frameworks/base/packages/SettingsLib/common.mk + +include $(BUILD_PACKAGE) diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..40cd622 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.cyanogenmod.providers.datausage" + android:sharedUserId="android.uid.system" + android:versionCode="1" + android:versionName="1.0"> + + <uses-sdk android:minSdkVersion="22"/> + + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" /> + + <application android:label="@string/app_name" + android:icon="@drawable/ic_launcher"> + + <!-- Runs in the phone process since it needs access to the Phone object --> + <service android:name=".DataUsageService" + android:label="DataUsageService"> + </service> + + <service android:name=".DataUsageAppInstallService" + android:label="DataUsageAppInstallService" > + </service> + + <provider android:name=".DataUsageProvider" + android:authorities="org.cyanogenmod.providers.datausage" + android:readPermission="cyanogenmod.permission.READ_DATAUSAGE" + android:writePermission="cyanogenmod.permission.WRITE_DATAUSAGE" + android:exported="true"> + </provider> + + <receiver android:name=".DataUsageServiceEnableReceiver"> + <intent-filter> + <action android:name="org.cyanogenmod.providers.datausage.enable" /> + </intent-filter> + </receiver> + + <receiver android:name=".DataUsageNotificationReceiver"> + <intent-filter> + <action android:name="org.cyanogenmod.providers.datausage.hide_action"/> + <action android:name="org.cyanogenmod.providers.datausage.disable_action"/> + </intent-filter> + </receiver> + + <receiver android:name=".DataUsageAppInstallReceiver" + android:exported="true" > + <intent-filter> + <action android:name="android.intent.action.PACKAGE_ADDED" /> + <action android:name="android.intent.action.PACKAGE_REMOVED" /> + <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" /> + <action android:name="android.intent.action.PACKAGE_CHANGED" /> + <action android:name="android.intent.action.PACKAGE_REPLACED" /> + <data android:scheme="package" /> + </intent-filter> + </receiver> + + <receiver android:name=".BootReceiver" android:enabled="true"> + <intent-filter> + <action android:name="android.intent.action.BOOT_COMPLETED" /> + </intent-filter> + </receiver> + + + </application> +</manifest> diff --git a/res/drawable-hdpi/data_usage_48dp.png b/res/drawable-hdpi/data_usage_48dp.png Binary files differnew file mode 100755 index 0000000..a888965 --- /dev/null +++ b/res/drawable-hdpi/data_usage_48dp.png diff --git a/res/drawable-hdpi/data_usage_disable_24dp.png b/res/drawable-hdpi/data_usage_disable_24dp.png Binary files differnew file mode 100755 index 0000000..1fce6de --- /dev/null +++ b/res/drawable-hdpi/data_usage_disable_24dp.png diff --git a/res/drawable-hdpi/data_usage_hide_24dp.png b/res/drawable-hdpi/data_usage_hide_24dp.png Binary files differnew file mode 100755 index 0000000..1a9cd75 --- /dev/null +++ b/res/drawable-hdpi/data_usage_hide_24dp.png diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..96a442e --- /dev/null +++ b/res/drawable-hdpi/ic_launcher.png diff --git a/res/drawable-ldpi/ic_launcher.png b/res/drawable-ldpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..9923872 --- /dev/null +++ b/res/drawable-ldpi/ic_launcher.png diff --git a/res/drawable-mdpi/data_usage_48dp.png b/res/drawable-mdpi/data_usage_48dp.png Binary files differnew file mode 100755 index 0000000..a43fa3c --- /dev/null +++ b/res/drawable-mdpi/data_usage_48dp.png diff --git a/res/drawable-mdpi/data_usage_disable_24dp.png b/res/drawable-mdpi/data_usage_disable_24dp.png Binary files differnew file mode 100755 index 0000000..d9721f5 --- /dev/null +++ b/res/drawable-mdpi/data_usage_disable_24dp.png diff --git a/res/drawable-mdpi/data_usage_hide_24dp.png b/res/drawable-mdpi/data_usage_hide_24dp.png Binary files differnew file mode 100755 index 0000000..40a1a84 --- /dev/null +++ b/res/drawable-mdpi/data_usage_hide_24dp.png diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..359047d --- /dev/null +++ b/res/drawable-mdpi/ic_launcher.png diff --git a/res/drawable-xhdpi/data_usage_48dp.png b/res/drawable-xhdpi/data_usage_48dp.png Binary files differnew file mode 100755 index 0000000..8683a2e --- /dev/null +++ b/res/drawable-xhdpi/data_usage_48dp.png diff --git a/res/drawable-xhdpi/data_usage_disable_24dp.png b/res/drawable-xhdpi/data_usage_disable_24dp.png Binary files differnew file mode 100755 index 0000000..c11e596 --- /dev/null +++ b/res/drawable-xhdpi/data_usage_disable_24dp.png diff --git a/res/drawable-xhdpi/data_usage_hide_24dp.png b/res/drawable-xhdpi/data_usage_hide_24dp.png Binary files differnew file mode 100755 index 0000000..6bc4372 --- /dev/null +++ b/res/drawable-xhdpi/data_usage_hide_24dp.png diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..71c6d76 --- /dev/null +++ b/res/drawable-xhdpi/ic_launcher.png diff --git a/res/drawable-xxhdpi/data_usage_48dp.png b/res/drawable-xxhdpi/data_usage_48dp.png Binary files differnew file mode 100755 index 0000000..88c2232 --- /dev/null +++ b/res/drawable-xxhdpi/data_usage_48dp.png diff --git a/res/drawable-xxhdpi/data_usage_disable_24dp.png b/res/drawable-xxhdpi/data_usage_disable_24dp.png Binary files differnew file mode 100755 index 0000000..ce41f10 --- /dev/null +++ b/res/drawable-xxhdpi/data_usage_disable_24dp.png diff --git a/res/drawable-xxhdpi/data_usage_hide_24dp.png b/res/drawable-xxhdpi/data_usage_hide_24dp.png Binary files differnew file mode 100755 index 0000000..51b4401 --- /dev/null +++ b/res/drawable-xxhdpi/data_usage_hide_24dp.png diff --git a/res/drawable-xxxhdpi/data_usage_48dp.png b/res/drawable-xxxhdpi/data_usage_48dp.png Binary files differnew file mode 100755 index 0000000..23e6d93 --- /dev/null +++ b/res/drawable-xxxhdpi/data_usage_48dp.png diff --git a/res/drawable-xxxhdpi/data_usage_hide_24dp.png b/res/drawable-xxxhdpi/data_usage_hide_24dp.png Binary files differnew file mode 100755 index 0000000..df42fee --- /dev/null +++ b/res/drawable-xxxhdpi/data_usage_hide_24dp.png diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 0000000..a9011f2 --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources > + <!-- Data usage Notification icon color --> + <color name="data_usage_notification_icon_color">#607D8B</color> +</resources> diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..2c77dcc --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name">Data Usage Provider</string> + <string name="data_usage_disable_short">DISABLE</string> + <string name="data_usage_disable_long">DISABLE APP\'s MOBILE DATA</string> + <string name="data_usage_hide">HIDE</string> + <string name="data_usage_disable_message">Disabling Cellular Data Network Access for %s</string> + <string name="data_usage_hide_message">Hiding Data Usage Warnings for %s</string> + <string name="data_usage_notify_title">Mobile Data Alert</string> + <string name="data_usage_notify_big_text">%s is using a lot of mobile data. Tap to view details.</string> + + <!-- Title for a work profile. [CHAR LIMIT=25] --> + <string name="managed_user_title">Work profile</string> + <!-- Manage apps, individual app screen, substituted for the application's label when the app's label CAN NOT be determined.--> + <string name="unknown">Unknown</string> + <!-- Title for Guest user [CHAR LIMIT=35] --> + <string name="user_guest">Guest</string> + <!-- [CHAR LIMIT=NONE] Label of a running process that represents another user --> + <string name="running_process_item_user_label">User: <xliff:g id="user_name">%1$s</xliff:g></string> + <!-- Label for kernel threads in battery usage --> + <string name="process_kernel_label">Android OS</string> + <!-- Title of data usage item that represents all uninstalled applications. [CHAR LIMIT=48] --> + <string name="data_usage_uninstalled_apps">Removed apps</string> + <!-- Title of data usage item that represents all uninstalled applications or removed users. [CHAR LIMIT=48] --> + <string name="data_usage_uninstalled_apps_users">Removed apps and users</string> +</resources> diff --git a/src/org/cyanogenmod/providers/datausage/AppItem.java b/src/org/cyanogenmod/providers/datausage/AppItem.java new file mode 100644 index 0000000..a8ed8ca --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/AppItem.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseBooleanArray; + +public class AppItem implements Comparable<AppItem>, Parcelable { + public static final int CATEGORY_USER = 0; + public static final int CATEGORY_APP_TITLE = 1; + public static final int CATEGORY_APP = 2; + + public final int key; + public boolean restricted; + public int category; + + public SparseBooleanArray uids = new SparseBooleanArray(); + public long total; + + public AppItem() { + this.key = 0; + } + + public AppItem(int key) { + this.key = key; + } + + public AppItem(Parcel parcel) { + key = parcel.readInt(); + uids = parcel.readSparseBooleanArray(); + total = parcel.readLong(); + } + + public void addUid(int uid) { + uids.put(uid, true); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(key); + dest.writeSparseBooleanArray(uids); + dest.writeLong(total); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int compareTo(AppItem another) { + int comparison = Integer.compare(category, another.category); + if (comparison == 0) { + comparison = Long.compare(another.total, total); + } + return comparison; + } + + public static final Creator<AppItem> CREATOR = new Creator<AppItem>() { + @Override + public AppItem createFromParcel(Parcel in) { + return new AppItem(in); + } + + @Override + public AppItem[] newArray(int size) { + return new AppItem[size]; + } + }; +} diff --git a/src/org/cyanogenmod/providers/datausage/BootReceiver.java b/src/org/cyanogenmod/providers/datausage/BootReceiver.java new file mode 100644 index 0000000..75e8e52 --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/BootReceiver.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class BootReceiver extends BroadcastReceiver { + + private static final String TAG = BootReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + DataUsageUtils.startDataUsageServiceIfEnabled(context); + } +} diff --git a/src/org/cyanogenmod/providers/datausage/DataUsageAppInstallReceiver.java b/src/org/cyanogenmod/providers/datausage/DataUsageAppInstallReceiver.java new file mode 100644 index 0000000..f6de8f7 --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/DataUsageAppInstallReceiver.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +/** + * This class implements the receiver that will handle app installs & uninstalls + * when an app is installed, add an entry in the datausage table + * when an app is removed, remove the entry from the datausage table + */ + +public class DataUsageAppInstallReceiver extends BroadcastReceiver { + private static final String TAG = DataUsageAppInstallReceiver.class.getSimpleName(); + private static final boolean DEBUG = false; + + @Override + public void onReceive(final Context context, Intent intent) { + if (DEBUG) { + Log.v(TAG, "AppInstallReceiver: onReceive"); + } + + Intent appInstallServiceIntent = new Intent(context, DataUsageAppInstallService.class); + appInstallServiceIntent.setAction(intent.getAction()); + if (intent.hasExtra(Intent.EXTRA_UID)) { + appInstallServiceIntent.putExtra( + Intent.EXTRA_UID, + intent.getIntExtra(Intent.EXTRA_UID, 0)); + } + context.startService(appInstallServiceIntent); + } +} diff --git a/src/org/cyanogenmod/providers/datausage/DataUsageAppInstallService.java b/src/org/cyanogenmod/providers/datausage/DataUsageAppInstallService.java new file mode 100644 index 0000000..c906b10 --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/DataUsageAppInstallService.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class DataUsageAppInstallService extends IntentService { + private static final String TAG = DataUsageAppInstallService.class.getSimpleName(); + private static final boolean DEBUG = true; + + /** + * Creates an IntentService. Invoked by your subclass's constructor. + * + * @param name Used to name the worker thread, important only for debugging. + */ + public DataUsageAppInstallService() { + super(TAG); + } + + @Override + protected void onHandleIntent(Intent intent) { + String action = intent.getAction(); + Context context = this; + + final boolean added; + final boolean changed; + final boolean removed; + if ("ANDROID.INTENT.ACTION.PACKAGE_ADDED".equalsIgnoreCase(action)) { + added = true; + removed = false; + } else if ("ANDROID.INTENT.ACTION.PACKAGE_CHANGED".equalsIgnoreCase(action)) { + added = false; + removed = false; + changed = true; + } else if ("ANDROID.INTENT.ACTION.PACKAGE_REPLACED".equalsIgnoreCase(action)) { + added = false; + removed = false; + changed = true; + } else if ("ANDROID.INTENT.ACTION.PACKAGE_REMOVED".equalsIgnoreCase(action)) { + added = false; + removed = true; + } else if ("ANDROID.INTENT.ACTION.PACKAGE_FULLY_REMOVED".equalsIgnoreCase(action)) { + added = false; + removed = true; + } else { + Log.e(TAG, "Unknown Action:" + action); + return; + } + + int uid = -1; + if (intent.hasExtra(Intent.EXTRA_UID)) { + uid = intent.getIntExtra(Intent.EXTRA_UID, 0); + } + + if (uid <= 0) { + Log.e(TAG, "Invalid UID:" + uid + " for Action:" + action); + return; + } + + UidDetailProvider uidDetailProvider = new UidDetailProvider(context); + UidDetail uidDetail = uidDetailProvider.getUidDetail(uid, true); + String label = ""; + if (uidDetail != null) { + label = uidDetail.label.toString(); + } + + if (added) { + if (DEBUG) { + Log.v(TAG, "Adding " + label + " to DataUsage DB"); + } + DataUsageUtils.addApp(context, uid, label); + } else if (removed) { + if (DEBUG) { + Log.v(TAG, "Removing " + label + " to DataUsage DB"); + } + DataUsageUtils.removeApp(context, uid); + } + } +}
\ No newline at end of file diff --git a/src/org/cyanogenmod/providers/datausage/DataUsageNotificationReceiver.java b/src/org/cyanogenmod/providers/datausage/DataUsageNotificationReceiver.java new file mode 100644 index 0000000..3a66fc5 --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/DataUsageNotificationReceiver.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.widget.Toast; + +import android.net.NetworkPolicyManager; +import static android.net.NetworkPolicyManager.POLICY_REJECT_APP_METERED_USAGE; + +import cyanogenmod.providers.DataUsageContract; + + +/** + * This class implements the receiver that will handle clicks on the buttons + * in the Data Usage Notification + * Disable - disables the wireless network traffic for the specified uid + * Hide - disables data usage checks for the specified uid + */ + +public class DataUsageNotificationReceiver extends BroadcastReceiver { + private static final String TAG = DataUsageNotificationReceiver.class.getSimpleName(); + private static final boolean DEBUG = true; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + int uid = 0; + String title; + if (intent.hasExtra(DataUsageService.DATA_USAGE_NOTIFICATION_UID)) { + // Settings app uses long, but NetworkPolicyManager uses int + // I guess UIDs are limited to 32 bits, so casting should not cause a problem + uid = (int)intent.getLongExtra(DataUsageService.DATA_USAGE_NOTIFICATION_UID, 0); + } + if (intent.hasExtra(DataUsageService.DATA_USAGE_NOTIFICATION_TITLE)) { + title = intent.getStringExtra(DataUsageService.DATA_USAGE_NOTIFICATION_TITLE); + } else { + title = ""; + } + + if (uid == 0) { + Log.e(TAG, "Invalid UID:" + uid + " for Action:" + action); + return; + } + + if (DataUsageService.HIDE_ACTION.equals(action)) { + Toast.makeText(context, context.getString(R.string.data_usage_hide_message, title), + Toast + .LENGTH_LONG) + .show(); + + ContentValues values = new ContentValues(); + values.put(DataUsageContract.ENB, 0); + values.put(DataUsageContract.ACTIVE, 0); + values.put(DataUsageContract.BYTES, 0); + + DataUsageUtils.enbApp(context, uid, false); + + } else if (DataUsageService.DISABLE_ACTION.equals(action)) { + Toast.makeText(context, context.getString(R.string.data_usage_disable_message, title), + Toast.LENGTH_LONG).show(); + NetworkPolicyManager policyManager = NetworkPolicyManager.from(context); + policyManager.addUidPolicy(uid, POLICY_REJECT_APP_METERED_USAGE); + } + + // cancel the notification + NotificationManager notificationManager = (NotificationManager)context.getSystemService + (Context.NOTIFICATION_SERVICE); + notificationManager.cancel(DataUsageService.DATA_USAGE_SERVICE_NOTIFICATION_ID); + } +} diff --git a/src/org/cyanogenmod/providers/datausage/DataUsageProvider.java b/src/org/cyanogenmod/providers/datausage/DataUsageProvider.java new file mode 100644 index 0000000..b038aae --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/DataUsageProvider.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.provider.ContactsContract; +import android.util.Log; + +import cyanogenmod.providers.DataUsageContract; + +/** + * ContentProvider for the DataUsage statistics gathering of the Settings App + * Keeps track of various per App configuration/state variables that are used to determine + * if and when to generate an App specific DataUsage warning + */ + +public class DataUsageProvider extends ContentProvider { + private static final boolean DEBUG = false; + private static final String TAG = DataUsageProvider.class.getSimpleName(); + private static final String DATABASE_NAME = "datausage.db"; + private static final int DATABASE_VERSION = 1; + + private DatabaseHelper mOpenHelper; + + // define database matching constants + private static final int DATAUSAGE_ALL = 0; + private static final int DATAUSAGE_ID = 1; + private static final int DATAUSAGE_UID = 2; + + // build a URI matcher - add routes to it (if any) + private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + static { + sURIMatcher.addURI(DataUsageContract.DATAUSAGE_AUTHORITY, + DataUsageContract.DATAUSAGE_TABLE, DATAUSAGE_ALL); + sURIMatcher.addURI(DataUsageContract.DATAUSAGE_AUTHORITY, + DataUsageContract.DATAUSAGE_TABLE + "/#", DATAUSAGE_ID); + sURIMatcher.addURI(DataUsageContract.DATAUSAGE_AUTHORITY, + DataUsageContract.DATAUSAGE_TABLE + "/uid/*", DATAUSAGE_UID); + } + + // Database Helper Class + private static class DatabaseHelper extends SQLiteOpenHelper { + private Context mContext; + + public DatabaseHelper (Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + // setup database schema + db.execSQL( + "CREATE TABLE " + DataUsageContract.DATAUSAGE_TABLE + + "(" + DataUsageContract._ID + " INTEGER PRIMARY KEY, " + + DataUsageContract.UID + " INTEGER, " + + DataUsageContract.ENB + " INTEGER DEFAULT 0, " + + DataUsageContract.ACTIVE + " INTEGER DEFAULT 0, " + + DataUsageContract.LABEL + " STRING, " + + DataUsageContract.BYTES + " INTEGER DEFAULT 0, " + + DataUsageContract.SLOW_AVG + " INTEGER DEFAULT 0, " + + DataUsageContract.SLOW_SAMPLES + " INTEGER DEFAULT 0, " + + DataUsageContract.FAST_AVG + " INTEGER DEFAULT 0, " + + DataUsageContract.FAST_SAMPLES + " INTEGER DEFAULT 0, " + + DataUsageContract.EXTRA + " STRING );" + ); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + return; + } + } + + + @Override + public boolean onCreate() { + mOpenHelper = new DatabaseHelper(getContext()); + return true; + } + + @Override + public Cursor query( + Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder + ) { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(DataUsageContract.DATAUSAGE_TABLE); + + int match = sURIMatcher.match(uri); + + if (DEBUG) { + Log.v(TAG, "Query uri=" + uri + ", match=" + match); + } + + switch (match) { + case DATAUSAGE_ALL: + break; + + case DATAUSAGE_ID: + break; + + case DATAUSAGE_UID: + break; + + default: + Log.e(TAG, "query: invalid request: " + uri); + return null; + } + + Cursor cursor; + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); + + return cursor; + } + + @Override + public String getType(Uri uri) { + int match = sURIMatcher.match(uri); + + switch(match) { + case DATAUSAGE_ALL: + return "vnd.android.cursor.dir/datausage_entry"; + case DATAUSAGE_ID: + case DATAUSAGE_UID: + return "vnd.android.cursor.item/datausage_entry"; + default: + throw new IllegalArgumentException("UNKNOWN URI: " + uri); + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + int match = sURIMatcher.match(uri); + if (DEBUG) { + Log.v(TAG, "Insert uri=" + uri + ", match=" + match); + } + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowID = db.insert(DataUsageContract.DATAUSAGE_TABLE, null, values); + + if (DEBUG) { + Log.v(TAG, "inserted " + values + " rowID=" + rowID); + } + + return ContentUris.withAppendedId(DataUsageContract.CONTENT_URI, rowID); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int match = sURIMatcher.match(uri); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + + if (DEBUG) { + Log.v(TAG, "Delete uri=" + uri + ", match=" + match); + } + + switch(match) { + case DATAUSAGE_ALL: + break; + case DATAUSAGE_UID: + if (selection != null || selectionArgs != null) { + throw new UnsupportedOperationException( + "Cannot delete URI:" + uri + " with a select clause" + ); + } + String uidNumber = uri.getLastPathSegment(); + selection = DataUsageContract.UID + " = ? "; + selectionArgs = new String [] {uidNumber}; + break; + default: + throw new UnsupportedOperationException( + "Cannot delete URI:" + uri + ); + } + int count = db.delete(DataUsageContract.DATAUSAGE_TABLE, selection, selectionArgs); + return count; + } + + // update is always done by UID + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + long count = 0; + int match = sURIMatcher.match(uri); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + String uid; + + if (DEBUG) { + Log.v(TAG, "Update uri=" + uri + ", match=" + match); + } + + switch(match) { + case DATAUSAGE_ALL: + uid = selectionArgs[0]; + break; + case DATAUSAGE_UID: + if (selection != null || selectionArgs != null) { + throw new UnsupportedOperationException( + "Cannot update URI " + uri + " with a select clause" + ); + } + selection = DataUsageContract.UID + " = ? "; + uid = uri.getLastPathSegment(); + selectionArgs = new String [] { uid }; + break; + default: + throw new UnsupportedOperationException("Cannot update that URI: " + uri); + + } + + // if no record is found, then perform an insert, so make the db transaction atomic + if (DEBUG) { + Log.v(TAG, "Update: Values:" + values.toString() + " selection:" + selection + " " + + " selectionArgs:" + selectionArgs[0]); + } + // count = db.update(DATAUSAGE_TABLE, values, selection, selectionArgs); + + db.beginTransaction(); + try { + count = db.update(DataUsageContract.DATAUSAGE_TABLE, values, selection, selectionArgs); + + if (DEBUG) { + Log.v(TAG, "Update count:" + count); + } + if (count == 0) { + if (DEBUG) { + Log.v(TAG, "Count==0, Performing Insert"); + } + values.put(DataUsageContract.UID, uid); + count = db.insert(DataUsageContract.DATAUSAGE_TABLE, null, values); + } + db.setTransactionSuccessful(); + } finally { + if (DEBUG) { + Log.v(TAG, "dbEndTransaction"); + } + db.endTransaction(); + } + if (DEBUG) { + Log.v(TAG, "Update result for uri=" + uri + " count=" + count); + } + return (int)count; + } +} diff --git a/src/org/cyanogenmod/providers/datausage/DataUsageService.java b/src/org/cyanogenmod/providers/datausage/DataUsageService.java new file mode 100644 index 0000000..ce328cc --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/DataUsageService.java @@ -0,0 +1,569 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.app.ActivityManager; +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.TaskStackBuilder; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import android.net.NetworkTemplate; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import android.net.INetworkStatsService; +import android.net.INetworkStatsSession; +import static android.net.TrafficStats.UID_REMOVED; + +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.net.NetworkStats; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.content.pm.UserInfo; + +import android.util.SparseArray; +import com.google.gson.Gson; + +import cyanogenmod.providers.DataUsageContract; + + +/** + * IntentService, launched by the AlarmManager at Boot Time from (BootReceiver) used + * to collect per app cellular usage networking statistics and generate warning messages + * to the user when an App consumes too much BW, giving the user an option to disable + * Warning Message generation or to disable Network Access for the offending App + */ + +public class DataUsageService extends IntentService { + private final static String TAG = DataUsageService.class.getSimpleName(); + private final static String TAB_MOBILE = "mobile"; + private Context mContext; + private final static boolean DEBUG = true; + + // Service worker tasks will run on the background thread, create a Handler to + // communicate with UI Thread, if needed + private Handler mUiHandler = new Handler(); + + private INetworkStatsService mStatsService; + private INetworkStatsSession mStatsSession; + private NetworkTemplate mTemplate; + private SubscriptionManager mSubscriptionManager; + private List<SubscriptionInfo> mSubInfoList; + private Map<Integer,String> mMobileTagMap; + private UserManager mUserManager; + private List<UserHandle> mProfiles; + private long mLargest; + private int mCurrentUserId; + private UidDetailProvider mUidDetailProvider; + SparseArray<AppItem> mKnownItems; + private NotificationManager mNotificationManager; + + // quick way to generate warnings + // TODO - set to false before releasing + private static final boolean FAST_MODE = false; + + // specifies minimum number of samples to collect before running algorithm + // 1 hours worth of active traffic to establish a baseline + private static final int MIN_SLOW_SAMPLE_COUNT = FAST_MODE ? 5 : 60; + // 5 min worth of active traffic + private static final int MIN_FAST_SAMPLE_COUNT = FAST_MODE ? 1 : 5; + + // specifies percentage by which fast average must exceed slow avg to trigger a warning + // one standard deviation - or should it be 34%, since we are only looking at above and not + // below. And how many standard deviations should it be? + private static final int WARNING_PERCENTAGE = FAST_MODE ? 10 : 68; + + // specifies the number of samples to keep in the database for postprocessing and + // algorithm evaluation + private final static int MAX_EXTRA_SAMPLE_COUNT = 1000; + + // specifies maximum bw that is still considered as idle - to discard pings, etc... + private static final long MAX_IDLE_BW = 5 * 1024; + // specifies the sample period in msec + public static final long SAMPLE_PERIOD = 60000; + public static final long START_DELAY = 60000; + + // notification ID to use by the DataUsageService for updates to notifications + public static final int DATA_USAGE_SERVICE_NOTIFICATION_ID = 102030; + + public static final String HIDE_ACTION = + "org.cyanogenmod.providers.datausage.hide_action"; + public static final String DISABLE_ACTION = + "org.cyanogenmod.providers.datausage.disable_action"; + public static final int DATA_USAGE_BROADCAST_REQUEST_CODE = 0x102040; // TODO - ??? + public static final String DATA_USAGE_NOTIFICATION_UID = + "org.cyanogenmod.providers.datausage.notification_uid"; + public static final String DATA_USAGE_NOTIFICATION_TITLE = + "org.cyanogenmod.providers.datausage.notification_title"; + + public DataUsageService() { + super(TAG); + } + + @android.support.annotation.Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + } + + /** + * When periodic alarm is generated, via AlarmManager, the Intent is delivered here + * @param intent + */ + @Override + protected void onHandleIntent(Intent intent) { + mContext = this; + + // initialize various networking managers/interfaces/sessions/etc... + mStatsService = INetworkStatsService.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); + mStatsSession = null; + try { + mStatsSession = mStatsService.openSession(); + mSubscriptionManager = SubscriptionManager.from(mContext); + mSubInfoList = mSubscriptionManager.getActiveSubscriptionInfoList(); + mMobileTagMap = initMobileTabTag(mSubInfoList); + mTemplate = buildTemplateMobileAll( + getActiveSubscriberId(mContext, getSubId(TAB_MOBILE + "1"))); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException: " + e.getMessage()); + } + + mUserManager = (UserManager)mContext.getSystemService(Context.USER_SERVICE); + mProfiles = mUserManager.getUserProfiles(); + mCurrentUserId = ActivityManager.getCurrentUser(); + mUidDetailProvider = new UidDetailProvider(mContext); + mKnownItems = new SparseArray<AppItem>(); + mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); + + // run the actual dataUsage collection and processing + dataUsageUpdate(); + } + + private static String getActiveSubscriberId(Context context, int subId) { + final TelephonyManager tele = TelephonyManager.from(context); + String retVal = tele.getSubscriberId(subId); + return retVal; + } + + + private int getSubId(String currentTab) { + if (mMobileTagMap != null) { + Set<Integer> set = mMobileTagMap.keySet(); + for (Integer subId : set) { + if (mMobileTagMap.get(subId).equals(currentTab)) { + return subId; + } + } + } + return -1; + } + + private Map<Integer, String> initMobileTabTag(List<SubscriptionInfo> subInfoList) { + Map<Integer, String> map = null; + if (subInfoList != null) { + String mobileTag; + map = new HashMap<Integer, String>(); + for (SubscriptionInfo subInfo : subInfoList) { + mobileTag = TAB_MOBILE + String.valueOf(subInfo.getSubscriptionId()); + map.put(subInfo.getSubscriptionId(), mobileTag); + } + } + return map; + } + + /** + * Accumulate data usage of a network stats entry for the item mapped by the collapse key. + * Creates the item, if needed + * + */ + private void accumulate(int collapseKey, NetworkStats.Entry entry, int itemCategory) { + int uid = entry.uid; + AppItem item = mKnownItems.get(collapseKey); + if (item == null) { + item = new AppItem(collapseKey); + item.category = itemCategory; + mKnownItems.put(item.key, item); + } + item.addUid(uid); + item.total += entry.rxBytes + entry.txBytes; + if (mLargest < item.total) { + mLargest = item.total; + } + + } + + + + private void clearStats() { + for(int i = 0; i < mKnownItems.size(); i++) { + int key = mKnownItems.keyAt(i); + AppItem appItem = mKnownItems.get(key); + appItem.total = 0; + } + } + + private class DataUsageExtraInfo { + ArrayList<Long> samples; + } + private String mAppWarnExtra; + + private void dataUsageUpdate() { + long startTime = 0; + long endTime = System.currentTimeMillis() * 2; + mLargest = 0; + + clearStats(); + + NetworkStats networkStats = null; + try { + if (mStatsSession != null) { + networkStats = mStatsSession.getSummaryForAllUid(mTemplate, startTime, endTime, + false); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException: " + e.getMessage()); + } + + + // collect network stats for all app consuming bw + if (networkStats != null) { + int size = networkStats.size(); + NetworkStats.Entry entry = null; + + for(int i = 0; i < size; i++) { + entry = networkStats.getValues(i, entry); + int collapseKey; + int category; + int uid = entry.uid; + int userId = UserHandle.getUserId(uid); + if (UserHandle.isApp(uid)) { + if (mProfiles.contains(new UserHandle(userId))) { + if (userId != mCurrentUserId) { + // add to a managed user item + int managedKey = UidDetailProvider.buildKeyForUser(userId); + accumulate(managedKey, entry, AppItem.CATEGORY_USER); + } + collapseKey = uid; + category = AppItem.CATEGORY_APP; + } else { + // if it is a removed user, add it to the removed users' key + UserInfo userInfo = mUserManager.getUserInfo(userId); + if (userInfo == null) { + collapseKey = UID_REMOVED; + category = AppItem.CATEGORY_APP; + } else { + collapseKey = UidDetailProvider.buildKeyForUser(userId); + category = AppItem.CATEGORY_USER; + } + } + accumulate(collapseKey, entry, category); + } + } + } + boolean appWarnActive = false; + long appWarnBytes = 0; + long appWarnUid; + int appWarnSlowSamples; + int appWarnFastSamples; + long appWarnSlowAvg; + long appWarnFastAvg; + String appWarnExtra = ""; + + // lookup Apps in the DB that have warning enabled + Cursor cursor = getContentResolver().query( + DataUsageContract.CONTENT_URI, + null, // projection - return all + DataUsageContract.ENB + " = ? ", + new String [] { "1" }, + null + ); + if (cursor == null) { + return; + } + + while(cursor.moveToNext()) { + appWarnUid = cursor.getInt(DataUsageContract.COLUMN_OF_UID); + appWarnActive = cursor.getInt(DataUsageContract.COLUMN_OF_ACTIVE) > 0; + appWarnBytes = cursor.getLong(DataUsageContract.COLUMN_OF_BYTES); + appWarnSlowSamples = cursor.getInt(DataUsageContract.COLUMN_OF_SLOW_SAMPLES); + appWarnSlowAvg = cursor.getLong(DataUsageContract.COLUMN_OF_SLOW_AVG); + appWarnFastSamples = cursor.getInt(DataUsageContract.COLUMN_OF_FAST_SAMPLES); + appWarnFastAvg = cursor.getLong(DataUsageContract.COLUMN_OF_FAST_AVG); + mAppWarnExtra = cursor.getString(DataUsageContract.COLUMN_OF_EXTRA); + + AppItem appItem = mKnownItems.get((int)appWarnUid); + + if (appItem != null) { + final UidDetail detail = mUidDetailProvider.getUidDetail(appItem.key, true); + long bytesDelta = appWarnBytes == 0 ? 0 : appItem.total - appWarnBytes; + if (DEBUG) { + Log.v(TAG, detail.label.toString() + + " cur:" + appItem.total + + " prev:" + appWarnBytes + + " SlowSamples:" + appWarnSlowSamples + + " SlowAvg:" + appWarnSlowAvg + + " FastSamples:" + appWarnFastSamples + + " FastAvg:" + appWarnFastAvg + ); + } + if (bytesDelta > MAX_IDLE_BW) { + // enough BW consumed during this sample - evaluate algorithm + if (appWarnSlowSamples < MIN_SLOW_SAMPLE_COUNT) { + // not enough samples acquired for the slow average, keep accumulating + // samples + appWarnSlowAvg = computeAvg(appWarnSlowAvg, appWarnSlowSamples, + MIN_SLOW_SAMPLE_COUNT, bytesDelta); + appWarnSlowSamples++; + + // fast average requires fewer samples than slow average, so at this point + // we may have accumulated enough or not, need to check + if (appWarnFastSamples < MIN_FAST_SAMPLE_COUNT) { + // not enough fast samples + appWarnFastAvg = computeAvg(appWarnFastAvg, appWarnFastSamples, + MIN_FAST_SAMPLE_COUNT, bytesDelta); + appWarnFastSamples++; + } else { + // enough fast samples + appWarnFastAvg = computeAvg(appWarnFastAvg, appWarnFastSamples, + MIN_FAST_SAMPLE_COUNT, bytesDelta); + } + + updateDb(appItem.key, + appWarnSlowAvg, appWarnSlowSamples, + appWarnFastAvg, appWarnFastSamples, + 0, appItem.total); + } else { + // enough samples acquired for the average, evaluate warning algorithm + float avgExceedPercent = appWarnFastAvg-appWarnSlowAvg; + avgExceedPercent /= appWarnSlowAvg; + avgExceedPercent *= 100; + + if ((appWarnFastAvg > appWarnSlowAvg) && (avgExceedPercent > + WARNING_PERCENTAGE)) { + genNotification(appItem.key, detail.label.toString(), !appWarnActive); + if (!appWarnActive) { + appWarnActive = true; + } + } else { + appWarnActive = false; + } + appWarnSlowAvg = computeAvg(appWarnSlowAvg, appWarnSlowSamples, + MIN_SLOW_SAMPLE_COUNT, bytesDelta); + appWarnFastAvg = computeAvg(appWarnFastAvg, appWarnFastSamples, + MIN_FAST_SAMPLE_COUNT, bytesDelta); + updateDb( + appItem.key, + appWarnSlowAvg, appWarnSlowSamples, + appWarnFastAvg, appWarnFastSamples, + appWarnActive ? 1 : 0, appItem.total + ); + + } + } else { + // not enough BW consumed during this sample - simply update bytes + updateDb(appItem.key, appItem.total); + } + } + } + cursor.close(); + } + + long computeAvg(long avg, int samples, int min_samples, long delta) { + float temp; + + if (samples < min_samples) { + temp = avg * samples; + temp += delta; + temp /= (samples + 1); + return (long)temp; + } else { + temp = avg * (samples - 1); + temp += delta; + temp /= samples; + return (long)temp; + } + } + + + private void updateDb(int uid, long bytes) { + ContentValues values = new ContentValues(); + + values.put(DataUsageContract.BYTES, bytes); + getContentResolver().update( + DataUsageContract.CONTENT_URI, + values, + DataUsageContract.UID + " = ? ", + new String[]{String.valueOf(uid)} + ); + } + + private void updateDb( + int uid, long slowAvg, int slowSamples, long fastAvg, int fastSamples, + int active, long bytes + ) { + ContentValues values = new ContentValues(); + String extraInfo = genExtraInfo(bytes); + values.put(DataUsageContract.SLOW_AVG, slowAvg); + values.put(DataUsageContract.SLOW_SAMPLES, slowSamples); + values.put(DataUsageContract.FAST_AVG, fastAvg); + values.put(DataUsageContract.FAST_SAMPLES, fastSamples); + values.put(DataUsageContract.ACTIVE, active); + values.put(DataUsageContract.BYTES, bytes); + values.put(DataUsageContract.EXTRA, extraInfo); + + getContentResolver().update( + DataUsageContract.CONTENT_URI, + values, + DataUsageContract.UID + " = ? ", + new String[]{String.valueOf(uid)} + ); + } + + + private String genExtraInfo(long bytes) { + + Gson gson = new Gson(); + + DataUsageExtraInfo extraInfo; + if (mAppWarnExtra == null || mAppWarnExtra == "") { + extraInfo = null; + } else { + try { + extraInfo = gson.fromJson(mAppWarnExtra, DataUsageExtraInfo.class); + } catch (Exception e) { + extraInfo = null; + } + } + + if (extraInfo == null) { + extraInfo = new DataUsageExtraInfo(); + extraInfo.samples = new ArrayList<Long>(); + } + + if (extraInfo.samples.size() == MAX_EXTRA_SAMPLE_COUNT) { + extraInfo.samples.remove(0); + } + extraInfo.samples.add(bytes); + String extraInfoJson = gson.toJson(extraInfo); + return extraInfoJson; + } + + + + private void genNotification(long uid, String appTitle, boolean firstTime) { + Intent hideIntent = new Intent(); + hideIntent.setAction(HIDE_ACTION); + hideIntent.putExtra(DATA_USAGE_NOTIFICATION_UID, uid); + hideIntent.putExtra(DATA_USAGE_NOTIFICATION_TITLE, appTitle); + PendingIntent hidePendingIntent = PendingIntent.getBroadcast( + mContext, DATA_USAGE_BROADCAST_REQUEST_CODE, hideIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ); + + Intent disableIntent = new Intent(); + disableIntent.setAction(DISABLE_ACTION); + disableIntent.putExtra(DATA_USAGE_NOTIFICATION_UID, uid); + disableIntent.putExtra(DATA_USAGE_NOTIFICATION_TITLE, appTitle); + PendingIntent disablePendingIntent = PendingIntent.getBroadcast( + mContext, DATA_USAGE_BROADCAST_REQUEST_CODE, disableIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ); + + Intent dataUsageIntent = new Intent(); + dataUsageIntent.setAction(cyanogenmod.content.Intent.ACTION_DATA_USAGE); + dataUsageIntent.addCategory(Intent.CATEGORY_DEFAULT); + // dataUsageIntent.setData(Uri.parse("package:" + mContext.getPackageName())); + dataUsageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + dataUsageIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + dataUsageIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + + PendingIntent dataUsagePendingIntent = PendingIntent.getActivity(mContext, 0, + dataUsageIntent, 0); + + // NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) + Notification.Builder builder = new Notification.Builder(mContext) + // .setSmallIcon(R.drawable.data_warning) + // .setSmallIcon(R.drawable.ic_sim_card_alert_white_48dp) + .setSmallIcon(R.drawable.data_usage_48dp) + .setContentTitle(getResources().getString(R.string.data_usage_notify_title)) + .setAutoCancel(true) // remove notification when clicked on + .setContentText(appTitle) // non-expanded view message + .setColor(mContext.getColor(R.color.data_usage_notification_icon_color)) + .setStyle(new Notification.BigTextStyle() + .bigText(getResources().getString(R.string.data_usage_notify_big_text, appTitle))); + + if (firstTime) { + builder.addAction( + // R.drawable.data_warning_disable, + // android.R.drawable.stat_sys_data_bluetooth, + R.drawable.data_usage_disable_24dp, + getResources().getString(R.string.data_usage_disable_long), + disablePendingIntent); + } else { + builder.addAction( + // R.drawable.data_warning_disable, + // android.R.drawable.stat_sys_data_bluetooth, + R.drawable.data_usage_disable_24dp, + getResources().getString(R.string.data_usage_disable_short), + disablePendingIntent); + builder.addAction( + // R.drawable.data_warning_hide, + // android.R.drawable.stat_sys_download_done, + R.drawable.data_usage_hide_24dp, + getResources().getString(R.string.data_usage_hide), + hidePendingIntent) + ; + } + + builder.setContentIntent(dataUsagePendingIntent); + mNotificationManager.notify(DATA_USAGE_SERVICE_NOTIFICATION_ID, builder.build()); + } + + + + @Override + public void onDestroy() { + super.onDestroy(); + } +} diff --git a/src/org/cyanogenmod/providers/datausage/DataUsageServiceEnableReceiver.java b/src/org/cyanogenmod/providers/datausage/DataUsageServiceEnableReceiver.java new file mode 100644 index 0000000..38f60f6 --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/DataUsageServiceEnableReceiver.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +public class DataUsageServiceEnableReceiver extends BroadcastReceiver { + private static final String TAG = DataUsageServiceEnableReceiver.class.getSimpleName(); + public static final String PREF_FILE = "data_usage_service"; + public static final String PREF_ENB_DATA_USAGE_NOTIFY = "enb_data_usage_notify"; + + @Override + public void onReceive(Context context, Intent intent) { + boolean enb = intent.getBooleanExtra("enable", false); + DataUsageUtils.enbDataUsageService(context, enb); + } +} diff --git a/src/org/cyanogenmod/providers/datausage/DataUsageUtils.java b/src/org/cyanogenmod/providers/datausage/DataUsageUtils.java new file mode 100644 index 0000000..5b5ce36 --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/DataUsageUtils.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.UserInfo; +import android.database.Cursor; +import android.util.Log; + +import org.cyanogenmod.providers.datausage.R; + +import cyanogenmod.providers.DataUsageContract; + + +public final class DataUsageUtils { + + /** + * Returns a label for the user, in the form of "User: user name" or "Work profile". + */ + public static String getUserLabel(Context context, UserInfo info) { + String name = info != null ? info.name : null; + if (info.isManagedProfile()) { + // We use predefined values for managed profiles + return context.getString(R.string.managed_user_title); + } else if (info.isGuest()) { + name = context.getString(R.string.user_guest); + } + if (name == null && info != null) { + name = Integer.toString(info.id); + } else if (info == null) { + name = context.getString(R.string.unknown); + } + return context.getResources().getString(R.string.running_process_item_user_label, name); + } + + + private static final String TAG = DataUsageUtils.class.getSimpleName(); + private static final int DATAUSAGE_SERVICE_ALARM_ID = 0x102030; + private static boolean DEBUG = true; + + public static void addApp(Context context, int uid, String label) { + if (DEBUG) { + Log.v(TAG, "addApp: uid:" + uid + " label:" + label); + } + + ContentValues values = new ContentValues(); + + values.put(DataUsageContract.UID, uid); + values.put(DataUsageContract.LABEL, label); + + context.getContentResolver().insert( + DataUsageContract.CONTENT_URI, + values + ); + } + + public static void removeApp(Context context, int uid) { + if (DEBUG) { + Log.v(TAG, "removeApp: uid:" + uid); + } + context.getContentResolver().delete( + DataUsageContract.CONTENT_URI, + DataUsageContract.UID + " = ? ", + new String [] { String.valueOf(uid)} + ); + } + + public static void enbApp(Context context, int uid, boolean enb) { + enbApp(context, uid, enb, null); + } + + public static void enbApp(Context context, int uid, boolean enb, String label) { + if (DEBUG) { + Log.v(TAG, "enbApp: uid:" + uid + " enb:" + enb + ((label == null) ? "" : (" label:" + + label))); + } + ContentValues values = new ContentValues(); + + values.put(DataUsageContract.ENB, enb); + if (label != null) { + values.put(DataUsageContract.LABEL, label); + } + context.getContentResolver().update( + DataUsageContract.CONTENT_URI, + values, + DataUsageContract.UID + " = ? ", + new String [] { String.valueOf(uid)} + ); + } + + public static boolean getAppEnb(Context context, int uid) { + boolean appEnb = false; + Cursor cursor = context.getContentResolver().query( + DataUsageContract.CONTENT_URI, + null, + DataUsageContract.UID + " = ? ", + new String [] { String.valueOf(uid) }, + null + ); + if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) { + int enbValue = cursor.getInt(DataUsageContract.COLUMN_OF_ENB); + if (enbValue == 1) { + appEnb = true; + } + } + + if (cursor != null) { + cursor.close(); + } + + if (DEBUG) { + Log.v(TAG, "getAppEnb: uid:" + uid + " enb:" + appEnb); + } + + return appEnb; + } + + public static final String PREF_FILE = "data_usage_service"; + public static final String PREF_ENB_DATA_USAGE_NOTIFY = "enb_data_usage_notify"; + + + public static void enbDataUsageService(Context context, boolean enb) { + SharedPreferences prefs = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); + prefs.edit().putBoolean(PREF_ENB_DATA_USAGE_NOTIFY, enb).apply(); + startDataUsageService(context, enb); + } + + public static void startDataUsageService(Context context, boolean enb) { + Intent dataUsageServiceIntent = new Intent(context, DataUsageService.class); + PendingIntent alarmIntent = PendingIntent.getService( + context, DATAUSAGE_SERVICE_ALARM_ID, dataUsageServiceIntent, 0); + AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + + if (enb) { + alarmManager.setRepeating( + AlarmManager.ELAPSED_REALTIME, + DataUsageService.START_DELAY, + DataUsageService.SAMPLE_PERIOD, + alarmIntent + ); + } else { + alarmManager.cancel(alarmIntent); + } + if (DEBUG) { + Log.v(TAG, "enbDataUsageService: enb:" + enb); + } + } + + public static void startDataUsageServiceIfEnabled(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); + boolean enb = prefs.getBoolean(PREF_ENB_DATA_USAGE_NOTIFY, false); + if (enb) { + startDataUsageService(context, true); + } + if (DEBUG) { + Log.v(TAG, "startDataUsageServiceIfEnabled: enb: " + enb); + } + + } +} diff --git a/src/org/cyanogenmod/providers/datausage/UidDetail.java b/src/org/cyanogenmod/providers/datausage/UidDetail.java new file mode 100644 index 0000000..91facca --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/UidDetail.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.graphics.drawable.Drawable; + +public class UidDetail { + public CharSequence label; +} diff --git a/src/org/cyanogenmod/providers/datausage/UidDetailProvider.java b/src/org/cyanogenmod/providers/datausage/UidDetailProvider.java new file mode 100644 index 0000000..1d84018 --- /dev/null +++ b/src/org/cyanogenmod/providers/datausage/UidDetailProvider.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.cyanogenmod.providers.datausage; + +import android.app.AppGlobals; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.IPackageManager; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.TrafficStats; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + + +import android.content.pm.IPackageManager; +import android.content.pm.UserInfo; + + +/** + * Return details about a specific UID, handling special cases like + * {@link TrafficStats#UID_TETHERING} and {@link UserInfo}. + */ +public class UidDetailProvider { + private static final String TAG = "DataUsage"; + private final Context mContext; + private final SparseArray<UidDetail> mUidDetailCache; + + public static final int OTHER_USER_RANGE_START = -2000; + + public static int buildKeyForUser(int userHandle) { + return OTHER_USER_RANGE_START - userHandle; + } + + public static boolean isKeyForUser(int key) { + return key <= OTHER_USER_RANGE_START; + } + + public static int getUserIdForKey(int key) { + return OTHER_USER_RANGE_START - key; + } + + public UidDetailProvider(Context context) { + mContext = context.getApplicationContext(); + mUidDetailCache = new SparseArray<UidDetail>(); + } + + public void clearCache() { + synchronized (mUidDetailCache) { + mUidDetailCache.clear(); + } + } + + /** + * Resolve best descriptive label for the given UID. + */ + public UidDetail getUidDetail(int uid, boolean blocking) { + UidDetail detail; + + synchronized (mUidDetailCache) { + detail = mUidDetailCache.get(uid); + } + + if (detail != null) { + return detail; + } else if (!blocking) { + return null; + } + + detail = buildUidDetail(uid); + + synchronized (mUidDetailCache) { + mUidDetailCache.put(uid, detail); + } + + return detail; + } + + /** + * Build {@link UidDetail} object, blocking until all {@link Drawable} + * lookup is finished. + */ + private UidDetail buildUidDetail(int uid) { + final Resources res = mContext.getResources(); + final PackageManager pm = mContext.getPackageManager(); + + final UidDetail detail = new UidDetail(); + detail.label = pm.getNameForUid(uid); + + // handle special case labels + switch (uid) { + case android.os.Process.SYSTEM_UID: + detail.label = res.getString(R.string.process_kernel_label); + return detail; + case TrafficStats.UID_REMOVED: + detail.label = res.getString(UserManager.supportsMultipleUsers() + ? R.string.data_usage_uninstalled_apps_users + : R.string.data_usage_uninstalled_apps); + return detail; + } + + final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + + // Handle keys that are actually user handles + if (isKeyForUser(uid)) { + final int userHandle = getUserIdForKey(uid); + final UserInfo info = um.getUserInfo(userHandle); + if (info != null) { + detail.label = DataUsageUtils.getUserLabel(mContext, info); + return detail; + } + } + + // otherwise fall back to using packagemanager labels + final String[] packageNames = pm.getPackagesForUid(uid); + final int length = packageNames != null ? packageNames.length : 0; + try { + final int userId = UserHandle.getUserId(uid); + UserHandle userHandle = new UserHandle(userId); + IPackageManager ipm = AppGlobals.getPackageManager(); + if (length == 1) { + final ApplicationInfo info = ipm.getApplicationInfo(packageNames[0], + 0 /* no flags */, userId); + if (info != null) { + detail.label = info.loadLabel(pm).toString(); + } + } else if (length > 1) { + for (int i = 0; i < length; i++) { + final String packageName = packageNames[i]; + final PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); + final ApplicationInfo appInfo = ipm.getApplicationInfo(packageName, + 0 /* no flags */, userId); + + if (appInfo != null) { + if (packageInfo.sharedUserLabel != 0) { + detail.label = pm.getText(packageName, packageInfo.sharedUserLabel, + packageInfo.applicationInfo).toString(); + } + } + } + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Error while building UI detail for uid "+uid, e); + } catch (RemoteException e) { + Log.w(TAG, "Error while building UI detail for uid "+uid, e); + } + + if (TextUtils.isEmpty(detail.label)) { + detail.label = Integer.toString(uid); + } + + return detail; + } +} |