added network usage

This commit is contained in:
Mariotaku Lee 2015-06-24 23:13:03 +08:00
parent 4a0df8b563
commit 3b36188f29
64 changed files with 432 additions and 105 deletions

View File

@ -9,7 +9,7 @@ buildscript {
classpath 'com.github.ben-manes:gradle-versions-plugin:0.9'
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
classpath('fr.avianey.androidsvgdrawable:gradle-plugin:1.0.1') {
classpath('fr.avianey.androidsvgdrawable:gradle-plugin:1.0.2') {
// should be excluded to avoid conflict
exclude group: 'xerces'
}

View File

@ -43,7 +43,7 @@ dependencies {
compile 'com.android.support:support-v4:22.2.0'
compile 'com.bluelinelabs:logansquare:1.1.0'
compile 'org.apache.commons:commons-lang3:3.4'
compile 'com.github.mariotaku:RestFu:b40c366f1c'
compile 'com.github.mariotaku:RestFu:d965fcf941'
compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.1'
compile project(':twidere.component.querybuilder')
compile fileTree(dir: 'libs', include: ['*.jar'])

View File

@ -181,6 +181,7 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst
int TABLE_ID_CACHED_STATUSES = 62;
int TABLE_ID_CACHED_HASHTAGS = 63;
int TABLE_ID_CACHED_RELATIONSHIPS = 64;
int TABLE_ID_NETWORK_USAGES = 71;
int VIRTUAL_TABLE_ID_DATABASE_READY = 100;
int VIRTUAL_TABLE_ID_NOTIFICATIONS = 101;
int VIRTUAL_TABLE_ID_PREFERENCES = 102;

View File

@ -0,0 +1,37 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.model;
/**
* Created by mariotaku on 15/6/24.
*/
public enum RequestType {
API("api"), MEDIA("media"), USAGE_STATISTICS("usage_statistics");
public String getName() {
return name;
}
private final String name;
RequestType(String name) {
this.name = name;
}
}

View File

@ -35,6 +35,7 @@ public interface TwidereDataStore {
String TYPE_BOOLEAN_DEFAULT_TRUE = "INTEGER(1) DEFAULT 1";
String TYPE_BOOLEAN_DEFAULT_FALSE = "INTEGER(1) DEFAULT 0";
String TYPE_TEXT = "TEXT";
String TYPE_DOUBLE_NOT_NULL = "DOUBLE NOT NULL";
String TYPE_TEXT_NOT_NULL = "TEXT NOT NULL";
String TYPE_TEXT_NOT_NULL_UNIQUE = "TEXT NOT NULL UNIQUE";
@ -914,4 +915,28 @@ public interface TwidereDataStore {
Uri CONTENT_URI = Uri.withAppendedPath(UnreadCounts.CONTENT_URI, CONTENT_PATH_SEGMENT);
}
}
interface NetworkUsages extends BaseColumns {
String TABLE_NAME = "network_usages";
String CONTENT_PATH = TABLE_NAME;
Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH);
String TIME_IN_HOURS = "time_in_hours";
String REQUEST_TYPE = "request_type";
String REQUEST_NETWORK = "request_network";
String KILOBYTES_SENT = "kilobytes_sent";
String KILOBYTES_RECEIVED = "kilobytes_received";
String[] COLUMNS = {_ID, TIME_IN_HOURS, REQUEST_TYPE, REQUEST_NETWORK, KILOBYTES_SENT,
KILOBYTES_RECEIVED};
String[] TYPES = {TYPE_PRIMARY_KEY, TYPE_INT, TYPE_TEXT_NOT_NULL, TYPE_TEXT_NOT_NULL,
TYPE_DOUBLE_NOT_NULL, TYPE_DOUBLE_NOT_NULL};
}
}

View File

@ -31,6 +31,7 @@ import org.mariotaku.restfu.http.RestHttpClient;
import org.mariotaku.restfu.http.RestHttpRequest;
import org.mariotaku.restfu.http.RestHttpResponse;
import org.mariotaku.twidere.model.ParcelableMedia;
import org.mariotaku.twidere.model.RequestType;
import org.mariotaku.twidere.util.HtmlLinkExtractor.HtmlLink;
import java.io.IOException;
@ -352,6 +353,7 @@ public class MediaPreviewUtils {
final RestHttpRequest.Builder builder = new RestHttpRequest.Builder();
builder.method(GET.METHOD);
builder.url(Endpoint.constructUrl(URL_PHOTOZOU_PHOTO_INFO, Pair.create("photo_id", id)));
builder.extra(RequestType.MEDIA);
final RestHttpResponse response = client.execute(builder.build());
final PhotoZouPhotoInfo info = LoganSquare.parse(response.getBody().stream(), PhotoZouPhotoInfo.class);
if (info.info != null && info.info.photo != null) {

View File

@ -453,8 +453,8 @@ public class NyanDrawingHelper {
}
}
private static interface StarAnimFrames {
static final byte[][] FRAME1 = {
private interface StarAnimFrames {
byte[][] FRAME1 = {
{
0, 0, 0, 0, 0, 0, 0
},

View File

@ -23,8 +23,58 @@ package org.mariotaku.querybuilder;
* Created by mariotaku on 15/3/30.
*/
public class Constraint implements SQLLang {
private final String name;
private final String type;
private final SQLQuery constraint;
public Constraint(String name, String type, SQLQuery constraint) {
this.name = name;
this.type = type;
this.constraint = constraint;
}
@Override
public String getSQL() {
return null;
final StringBuilder sb = new StringBuilder();
if (name != null) {
sb.append("CONSTRAINT ");
sb.append(name);
sb.append(" ");
}
sb.append(type);
sb.append(" ");
sb.append(constraint.getSQL());
return sb.toString();
}
public static Constraint unique(String name, Columns columns, OnConflict onConflict) {
return new Constraint(name, "UNIQUE", new ColumnConflictConstaint(columns, onConflict));
}
public static Constraint unique(Columns columns, OnConflict onConflict) {
return unique(null, columns, onConflict);
}
private static final class ColumnConflictConstaint implements SQLQuery {
private final Columns columns;
private final OnConflict onConflict;
public ColumnConflictConstaint(Columns columns, OnConflict onConflict) {
this.columns = columns;
this.onConflict = onConflict;
}
@Override
public String getSQL() {
final StringBuilder sb = new StringBuilder();
sb.append("(");
sb.append(columns.getSQL());
sb.append(") ");
sb.append("ON CONFLICT ");
sb.append(onConflict.getAction());
return sb.toString();
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.querybuilder;
/**
* Created by mariotaku on 15/6/24.
*/
public final class RawSQLLang implements SQLLang {
private final String statement;
public RawSQLLang(String statement) {
this.statement = statement;
}
@Override
public String getSQL() {
return statement;
}
}

View File

@ -34,5 +34,5 @@ public interface SQLLang extends Cloneable {
*
* @return SQL query
*/
public String getSQL();
String getSQL();
}

View File

@ -1,11 +1,11 @@
/**
* This is free and unencumbered software released into the public domain.
*
* <p/>
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* <p/>
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
@ -13,7 +13,7 @@
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
* <p/>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@ -21,7 +21,7 @@
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* <p/>
* For more information, please refer to <http://unlicense.org/>
*/
@ -110,6 +110,10 @@ public class SQLQueryBuilder {
return new SQLUpdateQuery.Builder().update(onConflict, table);
}
public static SQLUpdateQuery.Builder update(final OnConflict onConflict, final String table) {
return update(onConflict, new Table(table));
}
public static SQLInsertQuery.Builder insertInto(final String table) {
return insertInto(null, table);
}

View File

@ -15,6 +15,10 @@ public class SetValue implements SQLLang {
this.expression = expression;
}
public SetValue(String column, SQLLang expression) {
this(new Columns.Column(column), expression);
}
@Override
public String getSQL() {

View File

@ -35,8 +35,8 @@ public class SQLCreateTableQuery implements SQLQuery {
if (newColumns != null && newColumns.length > 0) {
sb.append('(');
sb.append(Utils.toString(newColumns, ',', true));
if (constraints != null) {
sb.append(' ');
if (constraints != null && constraints.length > 0) {
sb.append(", ");
sb.append(Utils.toString(constraints, ',', true));
sb.append(' ');
}

View File

@ -9,7 +9,7 @@ public class SQLInsertQuery implements SQLQuery {
private OnConflict onConflict;
private String table;
private String[] columns;
private SQLSelectQuery select;
private String values;
SQLInsertQuery() {
@ -21,11 +21,18 @@ public class SQLInsertQuery implements SQLQuery {
final StringBuilder sb = new StringBuilder();
sb.append("INSERT ");
if (onConflict != null) {
sb.append(String.format("OR %s ", onConflict.getAction()));
sb.append("OR ");
sb.append(onConflict.getAction());
sb.append(" ");
}
sb.append(String.format("INTO %s ", table));
sb.append(String.format("(%s) ", Utils.toString(columns, ',', false)));
sb.append(String.format("%s ", select.getSQL()));
sb.append("INTO ");
sb.append(table);
sb.append(" (");
sb.append(Utils.toString(columns, ',', false));
sb.append(") ");
sb.append("VALUES (");
sb.append(values);
sb.append(") ");
return sb.toString();
}
@ -38,7 +45,11 @@ public class SQLInsertQuery implements SQLQuery {
}
void setSelect(final SQLSelectQuery select) {
this.select = select;
this.values = select.getSQL();
}
void setValues(final String... values) {
this.values = Utils.toString(values, ',', false);
}
void setTable(final String table) {
@ -68,6 +79,18 @@ public class SQLInsertQuery implements SQLQuery {
return this;
}
public Builder values(final String[] values) {
checkNotBuilt();
query.setValues(values);
return this;
}
public Builder values(final String values) {
checkNotBuilt();
query.setValues(values);
return this;
}
public Builder insertInto(final OnConflict onConflict, final String table) {
checkNotBuilt();
query.setOnConflict(onConflict);

View File

@ -91,11 +91,11 @@ dependencies {
compile 'com.soundcloud.android:android-crop:1.0.0@aar'
compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.1'
compile 'com.github.mariotaku:PickNCrop:76563fae81'
compile 'com.diogobernardino:williamchart:1.7.0'
googleCompile 'com.google.android.gms:play-services-maps:7.5.0'
googleCompile 'com.google.maps.android:android-maps-utils:0.3.4'
fdroidCompile 'org.osmdroid:osmdroid-android:4.3'
fdroidCompile 'org.slf4j:slf4j-simple:1.7.12'
debugCompile 'im.dino:dbinspector:3.1.0@aar'
debugCompile 'com.facebook.stetho:stetho:1.1.1'
debugCompile 'com.facebook.stetho:stetho-okhttp:1.1.1'
compile project(':twidere.component.common')
@ -104,9 +104,9 @@ dependencies {
// googleCompile fileTree(dir: 'libs/google', include: ['*.jar'])
}
task svgToPng(type: SvgDrawableTask) {
task svgToDrawable(type: SvgDrawableTask) {
// specify where to pick SVG from
from = file('src/main/svg-png')
from = file('src/main/svg/drawable')
// specify the android res folder
to = file('src/main/res-svg2png')
// create qualified directories if missing
@ -116,7 +116,9 @@ task svgToPng(type: SvgDrawableTask) {
// let generate PNG for the following densities only
targetedDensities = ['hdpi', 'mdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi']
// relative path of the file specifying nine patch specs
ninePatchConfig = file('src/main/svg-png/9patch.json')
ninePatchConfig = file('src/main/svg/drawable/9patch.json')
// output format of the generated resources
outputFormat = 'PNG'
outputType = 'drawable'
}

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name="im.dino.dbinspector.activities.DbInspectorActivity"
android:exported="false"
android:label="Database Inspector"
tools:replace="android:label">
<intent-filter tools:node="removeAll">
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="org.mariotaku.twidere.HIDDEN_SETTINGS_ENTRY"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -23,15 +23,19 @@ import android.app.Application;
import com.facebook.stetho.Stetho;
import com.facebook.stetho.okhttp.StethoInterceptor;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import java.util.List;
/**
* Created by mariotaku on 15/5/27.
*/
public class DebugModeUtils {
public static void initForHttpClient(final OkHttpClient client) {
client.networkInterceptors().add(new StethoInterceptor());
final List<Interceptor> interceptors = client.networkInterceptors();
interceptors.add(new StethoInterceptor());
}
public static void initForApplication(final Application application) {

View File

@ -13,6 +13,7 @@ import org.mariotaku.restfu.http.mime.FileTypedData;
import org.mariotaku.restfu.http.mime.MultipartTypedBody;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.model.RequestType;
import org.mariotaku.twidere.util.TwitterAPIFactory;
import java.io.File;
@ -66,6 +67,7 @@ public class SpiceAsyUploadTask extends AsyncTask<Object, Object, Object> implem
final MultipartTypedBody body = new MultipartTypedBody();
body.add("file", new FileTypedData(tmp));
builder.body(body);
builder.extra(RequestType.USAGE_STATISTICS);
final RestHttpResponse response = client.execute(builder.build());
if (response.isSuccessful()) {
SpiceProfilingUtil.log("server has already received file " + tmp.getName());

View File

@ -33,7 +33,7 @@ import static org.mariotaku.twidere.annotation.Preference.Type.STRING;
public interface Constants extends TwidereConstants {
String DATABASES_NAME = "twidere.sqlite";
int DATABASES_VERSION = 99;
int DATABASES_VERSION = 104;
int MENU_GROUP_STATUS_EXTENSION = 10;
int MENU_GROUP_COMPOSE_EXTENSION = 11;

View File

@ -72,7 +72,6 @@ import org.mariotaku.twidere.api.twitter.auth.OAuthAuthorization;
import org.mariotaku.twidere.api.twitter.auth.OAuthEndpoint;
import org.mariotaku.twidere.api.twitter.auth.OAuthToken;
import org.mariotaku.twidere.api.twitter.model.User;
import org.mariotaku.twidere.app.TwidereApplication;
import org.mariotaku.twidere.fragment.support.BaseSupportDialogFragment;
import org.mariotaku.twidere.fragment.support.SupportProgressDialogFragment;
import org.mariotaku.twidere.graphic.EmptyDrawable;
@ -123,7 +122,6 @@ public class SignInActivity extends BaseAppCompatActivity implements OnClickList
private LinearLayout mSignInSignUpContainer, mUsernamePasswordContainer;
private final Handler mHandler = new Handler();
private TwidereApplication mApplication;
private SharedPreferences mPreferences;
private ContentResolver mResolver;
private AbstractSignInTask mTask;
@ -308,7 +306,6 @@ public class SignInActivity extends BaseAppCompatActivity implements OnClickList
super.onCreate(savedInstanceState);
mPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE);
mResolver = getContentResolver();
mApplication = TwidereApplication.getInstance(this);
setContentView(R.layout.activity_sign_in);
setSupportActionBar((Toolbar) findViewById(R.id.action_bar));
@ -334,15 +331,17 @@ public class SignInActivity extends BaseAppCompatActivity implements OnClickList
mAPIChangeTimestamp = savedInstanceState.getLong(EXTRA_API_LAST_CHANGE);
}
mUsernamePasswordContainer
.setVisibility(mAuthType == ParcelableCredentials.AUTH_TYPE_TWIP_O_MODE ? View.GONE : View.VISIBLE);
mSignInSignUpContainer.setOrientation(mAuthType == ParcelableCredentials.AUTH_TYPE_TWIP_O_MODE ? LinearLayout.VERTICAL
: LinearLayout.HORIZONTAL);
final boolean isTwipOMode = mAuthType == ParcelableCredentials.AUTH_TYPE_TWIP_O_MODE;
mUsernamePasswordContainer.setVisibility(isTwipOMode ? View.GONE : View.VISIBLE);
mSignInSignUpContainer.setOrientation(isTwipOMode ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
mEditUsername.setText(mUsername);
mEditUsername.addTextChangedListener(this);
mEditPassword.setText(mPassword);
mEditPassword.addTextChangedListener(this);
mSignUpButton.setOnClickListener(this);
final Resources resources = getResources();
final ColorStateList color = ColorStateList.valueOf(resources.getColor(R.color.material_light_green));
ViewCompat.setBackgroundTintList(mSignInButton, color);

View File

@ -61,8 +61,14 @@ import com.squareup.otto.Bus;
import org.apache.commons.lang3.ArrayUtils;
import org.mariotaku.querybuilder.Columns.Column;
import org.mariotaku.querybuilder.Expression;
import org.mariotaku.querybuilder.OnConflict;
import org.mariotaku.querybuilder.RawItemArray;
import org.mariotaku.querybuilder.RawSQLLang;
import org.mariotaku.querybuilder.SQLQueryBuilder;
import org.mariotaku.querybuilder.SetValue;
import org.mariotaku.querybuilder.query.SQLInsertQuery;
import org.mariotaku.querybuilder.query.SQLSelectQuery;
import org.mariotaku.querybuilder.query.SQLUpdateQuery;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.R;
@ -78,6 +84,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers;
import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages;
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts;
import org.mariotaku.twidere.provider.TwidereDataStore.Mentions;
import org.mariotaku.twidere.provider.TwidereDataStore.NetworkUsages;
import org.mariotaku.twidere.provider.TwidereDataStore.Preferences;
import org.mariotaku.twidere.provider.TwidereDataStore.SearchHistory;
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses;
@ -160,6 +167,7 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
case TABLE_ID_DIRECT_MESSAGES_CONVERSATION:
case TABLE_ID_DIRECT_MESSAGES:
case TABLE_ID_DIRECT_MESSAGES_CONVERSATIONS_ENTRIES:
case TABLE_ID_NETWORK_USAGES:
return 0;
}
int result = 0;
@ -300,6 +308,35 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
rowId = mDatabaseWrapper.insertWithOnConflict(table, null, values,
SQLiteDatabase.CONFLICT_IGNORE);
}
} else if (tableId == TABLE_ID_NETWORK_USAGES) {
rowId = 0;
final long timeInHours = values.getAsLong(NetworkUsages.TIME_IN_HOURS);
final String requestNetwork = values.getAsString(NetworkUsages.REQUEST_NETWORK);
final String requestType = values.getAsString(NetworkUsages.REQUEST_TYPE);
final SQLInsertQuery insertOrIgnore = SQLQueryBuilder.insertInto(OnConflict.IGNORE, table)
.columns(new String[]{NetworkUsages.TIME_IN_HOURS, NetworkUsages.REQUEST_NETWORK, NetworkUsages.REQUEST_TYPE,
NetworkUsages.KILOBYTES_RECEIVED, NetworkUsages.KILOBYTES_SENT})
.values("?, ?, ?, ?, ?")
.build();
final SQLUpdateQuery updateIncremental = SQLQueryBuilder.update(OnConflict.REPLACE, table)
.set(
new SetValue(NetworkUsages.KILOBYTES_RECEIVED, new RawSQLLang(NetworkUsages.KILOBYTES_RECEIVED + " + ?")),
new SetValue(NetworkUsages.KILOBYTES_SENT, new RawSQLLang(NetworkUsages.KILOBYTES_SENT + " + ?"))
)
.where(Expression.and(
Expression.equals(NetworkUsages.TIME_IN_HOURS, timeInHours),
Expression.equalsArgs(NetworkUsages.REQUEST_NETWORK),
Expression.equalsArgs(NetworkUsages.REQUEST_TYPE)
))
.build();
mDatabaseWrapper.beginTransaction();
mDatabaseWrapper.execSQL(insertOrIgnore.getSQL(),
new Object[]{timeInHours, requestNetwork, requestType, 0.0, 0.0});
mDatabaseWrapper.execSQL(updateIncremental.getSQL(),
new Object[]{values.getAsDouble(NetworkUsages.KILOBYTES_RECEIVED),
values.getAsDouble(NetworkUsages.KILOBYTES_SENT), requestNetwork, requestType});
mDatabaseWrapper.setTransactionSuccessful();
mDatabaseWrapper.endTransaction();
} else if (shouldReplaceOnConflict(tableId)) {
rowId = mDatabaseWrapper.insertWithOnConflict(table, null, values,
SQLiteDatabase.CONFLICT_REPLACE);
@ -508,6 +545,7 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
case TABLE_ID_DIRECT_MESSAGES_CONVERSATION:
case TABLE_ID_DIRECT_MESSAGES:
case TABLE_ID_DIRECT_MESSAGES_CONVERSATIONS_ENTRIES:
case TABLE_ID_NETWORK_USAGES:
return 0;
}
result = mDatabaseWrapper.update(table, values, selection, selectionArgs);
@ -578,6 +616,11 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
throw new SecurityException("Access database " + table + " requires level PERMISSION_LEVEL_READ");
break;
}
default: {
if (!mPermissionsManager.checkSignature(Binder.getCallingUid())) {
throw new SecurityException("Internal database is not allowed for third-party applications");
}
}
}
}
@ -618,6 +661,11 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
throw new SecurityException("Access database " + table + " requires level PERMISSION_LEVEL_WRITE");
break;
}
default: {
if (!mPermissionsManager.checkSignature(Binder.getCallingUid())) {
throw new SecurityException("Internal database is not allowed for third-party applications");
}
}
}
}

View File

@ -31,6 +31,7 @@ import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.util.AsyncTaskUtils;
import org.mariotaku.twidere.util.Utils;
import org.mariotaku.twidere.util.net.NetworkUsageUtils;
import edu.tsinghua.spice.Task.SpiceAsyUploadTask;
import edu.tsinghua.spice.Utilies.NetworkStateUtil;
@ -60,7 +61,9 @@ public class ConnectivityStateReceiver extends BroadcastReceiver implements Cons
+ location.getLatitude() + "," + location.getLongitude() + "," + location.getProvider());
}
}
final boolean isWifi = Utils.isOnWifi(context.getApplicationContext());
final int networkType = Utils.getActiveNetworkType(context.getApplicationContext());
NetworkUsageUtils.setNetworkType(networkType);
final boolean isWifi = networkType == ConnectivityManager.TYPE_WIFI;
final boolean isCharging = SpiceProfilingUtil.isCharging(context.getApplicationContext());
if (isWifi && isCharging) {
final long currentTime = System.currentTimeMillis();

View File

@ -40,6 +40,7 @@ import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.api.twitter.TwitterException;
import org.mariotaku.twidere.api.twitter.TwitterOAuth;
import org.mariotaku.twidere.api.twitter.auth.OAuthToken;
import org.mariotaku.twidere.model.RequestType;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
@ -75,7 +76,7 @@ public class OAuthPasswordAuthenticator implements Constants {
try {
requestToken = oauth.getRequestToken(OAUTH_CALLBACK_OOB);
} catch (final TwitterException e) {
// if (e.isCausedByNetworkIssue()) throw new AuthenticationException(e);
if (e.isCausedByNetworkIssue()) throw new AuthenticationException(e);
throw new AuthenticityTokenException(e);
}
RestHttpResponse authorizePage = null, authorizeResult = null;
@ -86,6 +87,7 @@ public class OAuthPasswordAuthenticator implements Constants {
authorizePageBuilder.method(GET.METHOD);
authorizePageBuilder.url(endpoint.construct("/oauth/authorize", Pair.create("oauth_token",
requestToken.getOauthToken())));
authorizePageBuilder.extra(RequestType.API);
final RestHttpRequest authorizePageRequest = authorizePageBuilder.build();
authorizePage = client.execute(authorizePageRequest);
final String[] cookieHeaders = authorizePage.getHeaders("Set-Cookie");
@ -120,6 +122,7 @@ public class OAuthPasswordAuthenticator implements Constants {
authorizeResultBuilder.url(endpoint.construct("/oauth/authorize"));
authorizeResultBuilder.headers(requestHeaders);
authorizeResultBuilder.body(authorizationResultBody);
authorizeResultBuilder.extra(RequestType.API);
authorizeResult = client.execute(authorizeResultBuilder.build());
final String oauthPin = readOAuthPINFromHtml(BaseTypedData.reader(authorizeResult.getBody()));
if (isEmpty(oauthPin)) throw new WrongUserPassException();

View File

@ -29,6 +29,7 @@ import org.mariotaku.restfu.http.RestHttpRequest;
import org.mariotaku.restfu.http.RestHttpResponse;
import org.mariotaku.restfu.http.mime.TypedData;
import org.mariotaku.twidere.activity.support.ThemedImagePickerActivity;
import org.mariotaku.twidere.model.RequestType;
import java.io.IOException;
@ -46,6 +47,7 @@ public class RestFuNetworkStreamDownloader extends ThemedImagePickerActivity.Net
final RestHttpRequest.Builder builder = new RestHttpRequest.Builder();
builder.method(GET.METHOD);
builder.url(uri.toString());
builder.extra(RequestType.MEDIA);
final RestHttpResponse response = client.execute(builder.build());
if (response.isSuccessful()) {
final TypedData body = response.getBody();

View File

@ -2,6 +2,7 @@ package org.mariotaku.twidere.util;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
public class SQLiteDatabaseWrapper {
@ -47,6 +48,18 @@ public class SQLiteDatabaseWrapper {
return mDatabase.insertWithOnConflict(table, nullColumnHack, initialValues, conflictAlgorithm);
}
public void execSQL(String sql) throws SQLException {
tryCreateDatabase();
if (mDatabase == null) return;
mDatabase.execSQL(sql);
}
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
tryCreateDatabase();
if (mDatabase == null) return;
mDatabase.execSQL(sql, bindArgs);
}
public boolean isReady() {
if (mLazyLoadCallback != null) return true;
return mDatabase != null;

View File

@ -44,6 +44,7 @@ import org.mariotaku.twidere.api.twitter.util.TwitterConverter;
import org.mariotaku.twidere.app.TwidereApplication;
import org.mariotaku.twidere.model.ConsumerKeyType;
import org.mariotaku.twidere.model.ParcelableCredentials;
import org.mariotaku.twidere.model.RequestType;
import org.mariotaku.twidere.util.net.OkHttpRestClient;
import java.net.InetSocketAddress;
@ -119,7 +120,7 @@ public class TwitterAPIFactory implements TwidereConstants {
client.setProxy(getProxy(prefs));
}
Internal.instance.setNetwork(client, TwidereApplication.getInstance(context).getNetwork());
return new OkHttpRestClient(client);
return new OkHttpRestClient(context, client);
}
@ -373,7 +374,7 @@ public class TwitterAPIFactory implements TwidereConstants {
headers.add(Pair.create("Authorization", authorization.getHeader(endpoint, info)));
}
headers.add(Pair.create("User-Agent", userAgent));
return new RestHttpRequest(restMethod, url, headers, info.getBody(), null);
return new RestHttpRequest(restMethod, url, headers, info.getBody(), RequestType.API);
}
}

View File

@ -214,6 +214,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.Drafts;
import org.mariotaku.twidere.provider.TwidereDataStore.Filters;
import org.mariotaku.twidere.provider.TwidereDataStore.Filters.Users;
import org.mariotaku.twidere.provider.TwidereDataStore.Mentions;
import org.mariotaku.twidere.provider.TwidereDataStore.NetworkUsages;
import org.mariotaku.twidere.provider.TwidereDataStore.Notifications;
import org.mariotaku.twidere.provider.TwidereDataStore.Permissions;
import org.mariotaku.twidere.provider.TwidereDataStore.Preferences;
@ -319,6 +320,8 @@ public final class Utils implements Constants {
TABLE_ID_SAVED_SEARCHES);
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, SearchHistory.CONTENT_PATH,
TABLE_ID_SEARCH_HISTORY);
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, NetworkUsages.CONTENT_PATH,
TABLE_ID_NETWORK_USAGES);
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Notifications.CONTENT_PATH,
VIRTUAL_TABLE_ID_NOTIFICATIONS);
@ -2184,6 +2187,8 @@ public final class Utils implements Constants {
return SavedSearches.TABLE_NAME;
case TABLE_ID_SEARCH_HISTORY:
return SearchHistory.TABLE_NAME;
case TABLE_ID_NETWORK_USAGES:
return NetworkUsages.TABLE_NAME;
default:
return null;
}
@ -2518,6 +2523,13 @@ public final class Utils implements Constants {
&& networkInfo.isConnected();
}
public static int getActiveNetworkType(final Context context) {
if (context == null) return -1;
final ConnectivityManager conn = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo networkInfo = conn.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected() ? networkInfo.getType() : -1;
}
public static boolean isRedirected(final int code) {
return code == 301 || code == 302 || code == 307;
}

View File

@ -26,6 +26,7 @@ import android.text.TextUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.mariotaku.querybuilder.Columns;
import org.mariotaku.querybuilder.Columns.Column;
import org.mariotaku.querybuilder.Constraint;
import org.mariotaku.querybuilder.Expression;
import org.mariotaku.querybuilder.NewColumn;
import org.mariotaku.querybuilder.OnConflict;
@ -48,38 +49,23 @@ import static org.mariotaku.querybuilder.SQLQueryBuilder.insertInto;
import static org.mariotaku.querybuilder.SQLQueryBuilder.select;
public final class DatabaseUpgradeHelper {
public static void safeUpgrade(final SQLiteDatabase db, final String table, final String[] newColNames,
final String[] newColTypes, final boolean dropDirectly, final boolean strictMode,
final Map<String, String> colAliases) {
safeUpgrade(db, table, newColNames, newColTypes, dropDirectly, strictMode, colAliases, OnConflict.REPLACE);
}
public static void safeUpgrade(final SQLiteDatabase db, final String table, final String[] newColNames,
final String[] newColTypes, final boolean dropDirectly, final boolean strictMode,
final Map<String, String> colAliases, final OnConflict onConflict) {
final String[] newColTypes, final boolean dropDirectly,
final Map<String, String> colAliases, final OnConflict onConflict,
final Constraint... constraints) {
if (newColNames == null || newColTypes == null || newColNames.length != newColTypes.length)
throw new IllegalArgumentException("Invalid parameters for upgrading table " + table
+ ", length of columns and types not match.");
// First, create the table if not exists.
final NewColumn[] newCols = NewColumn.createNewColumns(newColNames, newColTypes);
final String createQuery = createTable(true, table).columns(newCols).buildSQL();
final String createQuery = createTable(true, table).columns(newCols).constraint(constraints).buildSQL();
db.execSQL(createQuery);
// We need to get all data from old table.
final String[] oldCols = getColumnNames(db, table);
if (strictMode) {
final String oldCreate = getCreateSQL(db, table);
final Map<String, String> map = getTypeMapByCreateQuery(oldCreate);
boolean different = false;
for (final NewColumn newCol : newCols) {
if (!newCol.getType().equalsIgnoreCase(map.get(newCol.getName()))) {
different = true;
}
}
if (!different) return;
} else if (oldCols == null || TwidereArrayUtils.contentMatch(newColNames, oldCols)) return;
if (oldCols == null || TwidereArrayUtils.contentMatch(newColNames, oldCols)) return;
if (dropDirectly) {
db.beginTransaction();
db.execSQL(dropTable(true, table).getSQL());
@ -104,8 +90,8 @@ public final class DatabaseUpgradeHelper {
}
public static void safeUpgrade(final SQLiteDatabase db, final String table, final String[] newColNames,
final String[] newColTypes, final boolean dropDirectly, final Map<String, String> colAliases) {
safeUpgrade(db, table, newColNames, newColTypes, dropDirectly, true, colAliases, OnConflict.REPLACE);
final String[] newColTypes, final boolean dropDirectly, final Map<String, String> colAliases, final Constraint... constraints) {
safeUpgrade(db, table, newColNames, newColTypes, dropDirectly, colAliases, OnConflict.REPLACE, constraints);
}
private static String createInsertDataQuery(final String table, final String tempTable, final String[] newCols,

View File

@ -28,6 +28,7 @@ import android.os.Build;
import org.mariotaku.querybuilder.Columns;
import org.mariotaku.querybuilder.Columns.Column;
import org.mariotaku.querybuilder.Constraint;
import org.mariotaku.querybuilder.Expression;
import org.mariotaku.querybuilder.NewColumn;
import org.mariotaku.querybuilder.OnConflict;
@ -51,6 +52,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages;
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts;
import org.mariotaku.twidere.provider.TwidereDataStore.Filters;
import org.mariotaku.twidere.provider.TwidereDataStore.Mentions;
import org.mariotaku.twidere.provider.TwidereDataStore.NetworkUsages;
import org.mariotaku.twidere.provider.TwidereDataStore.SavedSearches;
import org.mariotaku.twidere.provider.TwidereDataStore.SearchHistory;
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses;
@ -96,6 +98,7 @@ public final class TwidereSQLiteOpenHelper extends SQLiteOpenHelper implements C
db.execSQL(createTable(Tabs.TABLE_NAME, Tabs.COLUMNS, Tabs.TYPES, true));
db.execSQL(createTable(SavedSearches.TABLE_NAME, SavedSearches.COLUMNS, SavedSearches.TYPES, true));
db.execSQL(createTable(SearchHistory.TABLE_NAME, SearchHistory.COLUMNS, SearchHistory.TYPES, true));
db.execSQL(createTable(NetworkUsages.TABLE_NAME, NetworkUsages.COLUMNS, NetworkUsages.TYPES, true, createNetworkUsagesConstraint()));
createViews(db);
createTriggers(db);
@ -105,6 +108,10 @@ public final class TwidereSQLiteOpenHelper extends SQLiteOpenHelper implements C
db.endTransaction();
}
private Constraint createNetworkUsagesConstraint() {
return Constraint.unique(new Columns(NetworkUsages.TIME_IN_HOURS, NetworkUsages.REQUEST_NETWORK, NetworkUsages.REQUEST_TYPE), OnConflict.IGNORE);
}
private void createIndices(SQLiteDatabase db) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
db.execSQL(createIndex("statuses_index", Statuses.TABLE_NAME, new String[]{Statuses.ACCOUNT_ID}, true));
@ -248,6 +255,8 @@ public final class TwidereSQLiteOpenHelper extends SQLiteOpenHelper implements C
safeUpgrade(db, Tabs.TABLE_NAME, Tabs.COLUMNS, Tabs.TYPES, false, null);
safeUpgrade(db, SavedSearches.TABLE_NAME, SavedSearches.COLUMNS, SavedSearches.TYPES, true, null);
safeUpgrade(db, SearchHistory.TABLE_NAME, SearchHistory.COLUMNS, SearchHistory.TYPES, true, null);
safeUpgrade(db, NetworkUsages.TABLE_NAME, NetworkUsages.COLUMNS, NetworkUsages.TYPES, true, null,
createNetworkUsagesConstraint());
db.beginTransaction();
createViews(db);
createTriggers(db);
@ -257,9 +266,10 @@ public final class TwidereSQLiteOpenHelper extends SQLiteOpenHelper implements C
}
private static String createTable(final String tableName, final String[] columns, final String[] types,
final boolean createIfNotExists) {
final boolean createIfNotExists, final Constraint... constraints) {
final SQLCreateTableQuery.Builder qb = SQLQueryBuilder.createTable(createIfNotExists, tableName);
qb.columns(NewColumn.createNewColumns(columns, types));
qb.constraint(constraints);
return qb.buildSQL();
}

View File

@ -48,6 +48,7 @@ import org.mariotaku.twidere.constant.SharedPreferenceConstants;
import org.mariotaku.twidere.model.ParcelableAccount;
import org.mariotaku.twidere.model.ParcelableCredentials;
import org.mariotaku.twidere.model.ParcelableMedia;
import org.mariotaku.twidere.model.RequestType;
import org.mariotaku.twidere.util.MediaPreviewUtils;
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
import org.mariotaku.twidere.util.TwidereLinkify;
@ -186,7 +187,12 @@ public class TwidereImageDownloader extends BaseImageDownloader implements Const
} else {
requestUri = modifiedUri.toString();
}
final RestHttpResponse resp = mClient.execute(new RestHttpRequest.Builder().method(method).url(requestUri).headers(additionalHeaders).build());
final RestHttpRequest.Builder builder = new RestHttpRequest.Builder();
builder.method(method);
builder.url(requestUri);
builder.headers(additionalHeaders);
builder.extra(RequestType.MEDIA);
final RestHttpResponse resp = mClient.execute(builder.build());
final TypedData body = resp.getBody();
return new ContentLengthInputStream(body.stream(), (int) body.length());
}

View File

@ -0,0 +1,90 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.net;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import org.mariotaku.twidere.model.RequestType;
import org.mariotaku.twidere.provider.TwidereDataStore.NetworkUsages;
import org.mariotaku.twidere.util.Utils;
import java.io.IOException;
/**
* Created by mariotaku on 15/6/24.
*/
public class NetworkUsageUtils {
public static void initForHttpClient(Context context, OkHttpClient client) {
client.networkInterceptors().add(new NetworkUsageInterceptor(context));
}
private static int sNetworkType;
public static void setNetworkType(int networkType) {
NetworkUsageUtils.sNetworkType = networkType;
}
private static class NetworkUsageInterceptor implements Interceptor {
private final Context context;
public NetworkUsageInterceptor(Context context) {
this.context = context;
setNetworkType(Utils.getActiveNetworkType(context));
}
@Override
public Response intercept(Chain chain) throws IOException {
final Request request = chain.request();
final Object tag = request.tag();
if (!(tag instanceof RequestType)) return chain.proceed(request);
final ContentValues values = new ContentValues();
values.put(NetworkUsages.TIME_IN_HOURS, System.currentTimeMillis() / 1000 / 60 / 60);
values.put(NetworkUsages.KILOBYTES_SENT, getBodyLength(request.body()) / 1024.0);
values.put(NetworkUsages.REQUEST_TYPE, ((RequestType) tag).getName());
values.put(NetworkUsages.REQUEST_NETWORK, sNetworkType);
final Response response = chain.proceed(request);
values.put(NetworkUsages.KILOBYTES_RECEIVED, getBodyLength(response.body()) / 1024.0);
final ContentResolver cr = context.getContentResolver();
cr.insert(NetworkUsages.CONTENT_URI, values);
return response;
}
private long getBodyLength(RequestBody body) throws IOException {
if (body == null) return 0;
final long length = body.contentLength();
return length > 0 ? length : 0;
}
private long getBodyLength(ResponseBody body) throws IOException {
if (body == null) return 0;
final long length = body.contentLength();
return length > 0 ? length : 0;
}
}
}

View File

@ -19,6 +19,7 @@
package org.mariotaku.twidere.util.net;
import android.content.Context;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -59,12 +60,9 @@ public class OkHttpRestClient implements RestHttpClient {
private final OkHttpClient client;
public OkHttpRestClient() {
this(new OkHttpClient());
}
public OkHttpRestClient(OkHttpClient client) {
public OkHttpRestClient(Context context, OkHttpClient client) {
this.client = client;
NetworkUsageUtils.initForHttpClient(context, client);
DebugModeUtils.initForHttpClient(client);
}
@ -85,6 +83,7 @@ public class OkHttpRestClient implements RestHttpClient {
builder.addHeader(header.first, header.second);
}
}
builder.tag(restHttpRequest.getExtra());
return client.newCall(builder.build());
}
@ -132,6 +131,11 @@ public class OkHttpRestClient implements RestHttpClient {
body.writeTo(sink.outputStream());
}
@Override
public long contentLength() throws IOException {
return body.length();
}
@Nullable
public static RequestBody wrap(@Nullable TypedData body) {
if (body == null) return null;

View File

@ -85,7 +85,6 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:minHeight="48dp"
android:onClick="onClick"
android:text="@string/register"/>
<Button

View File

Before

Width:  |  Height:  |  Size: 516 B

After

Width:  |  Height:  |  Size: 516 B

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

Before

Width:  |  Height:  |  Size: 300 B

After

Width:  |  Height:  |  Size: 300 B

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 386 B

View File

Before

Width:  |  Height:  |  Size: 874 B

After

Width:  |  Height:  |  Size: 874 B

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 B

View File

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 232 B

View File

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 404 B

View File

Before

Width:  |  Height:  |  Size: 568 B

After

Width:  |  Height:  |  Size: 568 B

View File

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 529 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

View File

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

View File

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 206 B

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB