fixed text count

This commit is contained in:
Mariotaku Lee 2016-09-09 11:58:26 +08:00
parent b39929f82b
commit 99503e83a5
42 changed files with 1247 additions and 1285 deletions

View File

@ -305,4 +305,7 @@ public interface SharedPreferenceConstants {
String KEY_NEW_DOCUMENT_API = "new_document_api";
@Preference(type = BOOLEAN, hasDefault = true, defaultBoolean = false)
String KEY_DRAWER_TOGGLE = "drawer_toggle";
String KEY_MEDIA_LINK_COUNTS_IN_STATUS = "media_link_counts_in_status";
}

View File

@ -0,0 +1,3 @@
{
"media_link_counts_in_status": true
}

View File

@ -88,7 +88,7 @@ public class HotMobiLogger implements HotMobiConstants {
}
public static HotMobiLogger getInstance(Context context) {
return DependencyHolder.get(context).getHotMobiLogger();
return DependencyHolder.Companion.get(context).getHotMobiLogger();
}
public static File getLogFile(Context context, @Nullable UserKey accountKey, String type) {

View File

@ -26,7 +26,7 @@ public class LocationUtils implements HotMobiConstants, Constants {
public static LatLng getCachedLatLng(@NonNull final Context context) {
final Context appContext = context.getApplicationContext();
final SharedPreferences prefs = DependencyHolder.get(context).getPreferences();
final SharedPreferences prefs = DependencyHolder.Companion.get(context).getPreferences();
if (!prefs.getBoolean(KEY_USAGE_STATISTICS, false)) return null;
if (BuildConfig.DEBUG) {
Log.d(HotMobiLogger.LOGTAG, "getting cached location");

View File

@ -144,7 +144,7 @@ public class NetworkDiagnosticsFragment extends BaseSupportFragment {
publishProgress(new LogText("Text below may have personal information, BE CAREFUL TO MAKE IT PUBLIC",
LogText.State.WARNING));
publishProgress(LogText.LINEBREAK, LogText.LINEBREAK);
DependencyHolder holder = DependencyHolder.get(mContext);
DependencyHolder holder = DependencyHolder.Companion.get(mContext);
final TwidereDns dns = holder.getDns();
final SharedPreferencesWrapper prefs = holder.getPreferences();
publishProgress(new LogText("Network preferences"), LogText.LINEBREAK);

View File

@ -1,87 +0,0 @@
package org.mariotaku.twidere.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Created by mariotaku on 16/2/11.
*/
public class BaseRefreshTaskParam implements RefreshTaskParam {
private final UserKey[] accountKeys;
private final String[] maxIds;
private final String[] sinceIds;
private final long[] maxSortIds;
private final long[] sinceSortIds;
private boolean isLoadingMore;
private boolean shouldAbort;
public BaseRefreshTaskParam(UserKey[] accountKeys, String[] maxIds, String[] sinceIds) {
this(accountKeys, maxIds, sinceIds, null, null);
}
public BaseRefreshTaskParam(UserKey[] accountKeys, String[] maxIds, String[] sinceIds,
long[] maxSortIds, long[] sinceSortIds) {
this.accountKeys = accountKeys;
this.maxIds = maxIds;
this.sinceIds = sinceIds;
this.maxSortIds = maxSortIds;
this.sinceSortIds = sinceSortIds;
}
@NonNull
@Override
public UserKey[] getAccountKeys() {
return accountKeys;
}
@Nullable
@Override
public String[] getMaxIds() {
return maxIds;
}
@Nullable
@Override
public String[] getSinceIds() {
return sinceIds;
}
@Override
public boolean hasMaxIds() {
return maxIds != null;
}
@Override
public boolean hasSinceIds() {
return sinceIds != null;
}
@Override
public long[] getMaxSortIds() {
return maxSortIds;
}
@Override
public long[] getSinceSortIds() {
return sinceSortIds;
}
@Override
public boolean isLoadingMore() {
return isLoadingMore;
}
public void setLoadingMore(boolean isLoadingMore) {
this.isLoadingMore = isLoadingMore;
}
@Override
public boolean shouldAbort() {
return shouldAbort;
}
public void setShouldAbort(boolean shouldAbort) {
this.shouldAbort = shouldAbort;
}
}

View File

@ -0,0 +1,75 @@
package org.mariotaku.twidere.model;
import android.content.SharedPreferences;
import android.support.annotation.WorkerThread;
import com.bluelinelabs.logansquare.JsonMapper;
import com.bluelinelabs.logansquare.LoganSquare;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.http.HttpRequest;
import org.mariotaku.restfu.http.HttpResponse;
import org.mariotaku.restfu.http.RestHttpClient;
import java.io.IOException;
import static org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_MEDIA_LINK_COUNTS_IN_STATUS;
/**
* Created by mariotaku on 16/9/9.
*/
@JsonObject
public class DefaultFeatures {
private final static String REMOTE_SETTINGS_URL = "https://raw.githubusercontent.com/TwidereProject/Twidere-Android/master/twidere/src/main/assets/data/default_features.json";
@JsonField(name = "media_link_counts_in_status")
boolean mediaLinkCountsInStatus = true;
public boolean isMediaLinkCountsInStatus() {
return mediaLinkCountsInStatus;
}
@WorkerThread
public boolean loadRemoteSettings(RestHttpClient client) throws IOException {
HttpRequest request = new HttpRequest.Builder().method(GET.METHOD).url(REMOTE_SETTINGS_URL).build();
final HttpResponse response = client.newCall(request).execute();
try {
final JsonMapper<DefaultFeatures> mapper = LoganSquare.mapperFor(DefaultFeatures.class);
final JsonParser jsonParser = LoganSquare.JSON_FACTORY.createParser(response.getBody().stream());
if (jsonParser.getCurrentToken() == null) {
jsonParser.nextToken();
}
if (jsonParser.getCurrentToken() != JsonToken.START_OBJECT) {
jsonParser.skipChildren();
return false;
}
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldName = jsonParser.getCurrentName();
jsonParser.nextToken();
mapper.parseField(this, fieldName, jsonParser);
jsonParser.skipChildren();
}
} finally {
response.close();
}
return true;
}
public void load(SharedPreferences preferences) {
mediaLinkCountsInStatus = preferences.getBoolean(KEY_MEDIA_LINK_COUNTS_IN_STATUS,
mediaLinkCountsInStatus);
}
public void save(SharedPreferences preferences) {
final SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(KEY_MEDIA_LINK_COUNTS_IN_STATUS, mediaLinkCountsInStatus);
editor.apply();
}
}

View File

@ -1,33 +0,0 @@
package org.mariotaku.twidere.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Created by mariotaku on 16/2/14.
*/
public interface RefreshTaskParam {
@NonNull
UserKey[] getAccountKeys();
@Nullable
String[] getMaxIds();
@Nullable
String[] getSinceIds();
@Nullable
long[] getMaxSortIds();
@Nullable
long[] getSinceSortIds();
boolean hasMaxIds();
boolean hasSinceIds();
boolean isLoadingMore();
boolean shouldAbort();
}

View File

@ -1,66 +0,0 @@
package org.mariotaku.twidere.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Created by mariotaku on 16/2/14.
*/
public abstract class SimpleRefreshTaskParam implements RefreshTaskParam {
UserKey[] cached;
@NonNull
@Override
public final UserKey[] getAccountKeys() {
if (cached != null) return cached;
return cached = getAccountKeysWorker();
}
@NonNull
public abstract UserKey[] getAccountKeysWorker();
@Nullable
@Override
public String[] getMaxIds() {
return null;
}
@Nullable
@Override
public String[] getSinceIds() {
return null;
}
@Override
public boolean hasMaxIds() {
return getMaxIds() != null;
}
@Override
public boolean hasSinceIds() {
return getSinceIds() != null;
}
@Nullable
@Override
public long[] getSinceSortIds() {
return null;
}
@Nullable
@Override
public long[] getMaxSortIds() {
return null;
}
@Override
public boolean isLoadingMore() {
return false;
}
@Override
public boolean shouldAbort() {
return false;
}
}

View File

@ -1,109 +0,0 @@
/*
* 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.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.annotation.CustomTabType;
import org.mariotaku.twidere.annotation.NotificationType;
import org.mariotaku.twidere.annotation.ReadPositionTag;
import org.mariotaku.twidere.model.StringLongPair;
import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.util.CustomTabUtils;
import org.mariotaku.twidere.util.ReadStateManager;
import org.mariotaku.twidere.util.UriExtraUtils;
import org.mariotaku.twidere.util.Utils;
import org.mariotaku.twidere.util.dagger.DependencyHolder;
import edu.tsinghua.hotmobi.HotMobiLogger;
import edu.tsinghua.hotmobi.model.NotificationEvent;
/**
* Created by mariotaku on 15/4/4.
*/
public class NotificationReceiver extends BroadcastReceiver implements Constants {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action == null) return;
switch (action) {
case BROADCAST_NOTIFICATION_DELETED: {
final Uri uri = intent.getData();
if (uri == null) return;
DependencyHolder holder = DependencyHolder.get(context);
@NotificationType
final String notificationType = uri.getQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE);
final UserKey accountKey = UserKey.valueOf(uri.getQueryParameter(QUERY_PARAM_ACCOUNT_KEY));
final long itemId = NumberUtils.toLong(UriExtraUtils.getExtra(uri, "item_id"), -1);
final long itemUserId = NumberUtils.toLong(UriExtraUtils.getExtra(uri, "item_user_id"), -1);
final boolean itemUserFollowing = Boolean.parseBoolean(UriExtraUtils.getExtra(uri, "item_user_following"));
final long timestamp = NumberUtils.toLong(uri.getQueryParameter(QUERY_PARAM_TIMESTAMP), -1);
if (CustomTabType.NOTIFICATIONS_TIMELINE.equals(CustomTabUtils.getTabTypeAlias(notificationType))
&& accountKey != null && itemId != -1 && timestamp != -1) {
final HotMobiLogger logger = holder.getHotMobiLogger();
logger.log(accountKey, NotificationEvent.deleted(context, timestamp, notificationType, accountKey,
itemId, itemUserId, itemUserFollowing));
}
final ReadStateManager manager = holder.getReadStateManager();
final String paramReadPosition, paramReadPositions;
@ReadPositionTag
final String tag = getPositionTag(notificationType);
if (tag != null && !TextUtils.isEmpty(paramReadPosition = uri.getQueryParameter(QUERY_PARAM_READ_POSITION))) {
final long def = -1;
manager.setPosition(Utils.getReadPositionTagWithAccount(tag, accountKey),
NumberUtils.toLong(paramReadPosition, def));
} else if (!TextUtils.isEmpty(paramReadPositions = uri.getQueryParameter(QUERY_PARAM_READ_POSITIONS))) {
try {
final StringLongPair[] pairs = StringLongPair.valuesOf(paramReadPositions);
for (StringLongPair pair : pairs) {
manager.setPosition(tag, pair.getKey(), pair.getValue());
}
} catch (NumberFormatException ignore) {
}
}
break;
}
}
}
@ReadPositionTag
@Nullable
private static String getPositionTag(@Nullable @NotificationType String type) {
if (type == null) return null;
switch (type) {
case NotificationType.HOME_TIMELINE:
return ReadPositionTag.HOME_TIMELINE;
case NotificationType.INTERACTIONS:
return ReadPositionTag.ACTIVITIES_ABOUT_ME;
case NotificationType.DIRECT_MESSAGES: {
return ReadPositionTag.DIRECT_MESSAGES;
}
}
return null;
}
}

View File

@ -39,12 +39,12 @@ public class GetActivitiesAboutMeTask extends GetActivitiesTask {
@Override
protected void saveReadPosition(@NonNull UserKey accountKey, ParcelableCredentials credentials, @NonNull MicroBlog twitter) {
if (ParcelableAccount.Type.TWITTER.equals(ParcelableAccountUtils.getAccountType(credentials))) {
if (Utils.isOfficialCredentials(context, credentials)) {
if (Utils.isOfficialCredentials(getContext(), credentials)) {
try {
CursorTimestampResponse response = twitter.getActivitiesAboutMeUnread(true);
final String tag = Utils.getReadPositionTagWithAccount(ReadPositionTag.ACTIVITIES_ABOUT_ME,
accountKey);
readStateManager.setPosition(tag, response.getCursor(), false);
getReadStateManager().setPosition(tag, response.getCursor(), false);
} catch (MicroBlogException e) {
// Ignore
}
@ -56,7 +56,7 @@ public class GetActivitiesAboutMeTask extends GetActivitiesTask {
protected ResponseList<Activity> getActivities(@NonNull final MicroBlog twitter,
@NonNull final ParcelableCredentials credentials,
@NonNull final Paging paging) throws MicroBlogException {
if (Utils.isOfficialCredentials(context, credentials)) {
if (Utils.isOfficialCredentials(getContext(), credentials)) {
return twitter.getActivitiesAboutMe(paging);
}
final ResponseList<Activity> activities = new ResponseList<>();

View File

@ -1,52 +0,0 @@
package org.mariotaku.twidere.task;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.mariotaku.microblog.library.MicroBlog;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.model.ResponseList;
import org.mariotaku.microblog.library.twitter.model.Status;
import org.mariotaku.twidere.provider.TwidereDataStore;
import org.mariotaku.twidere.task.twitter.GetStatusesTask;
import org.mariotaku.twidere.util.ErrorInfoStore;
import edu.tsinghua.hotmobi.model.TimelineType;
/**
* Created by mariotaku on 16/2/11.
*/
public class GetHomeTimelineTask extends GetStatusesTask {
public GetHomeTimelineTask(Context context) {
super(context);
}
@NonNull
@Override
public ResponseList<Status> getStatuses(final MicroBlog twitter, final Paging paging)
throws MicroBlogException {
return twitter.getHomeTimeline(paging);
}
@NonNull
@Override
protected Uri getContentUri() {
return TwidereDataStore.Statuses.CONTENT_URI;
}
@TimelineType
@Override
protected String getTimelineType() {
return TimelineType.HOME;
}
@NonNull
@Override
protected String getErrorInfoKey() {
return ErrorInfoStore.KEY_HOME_TIMELINE;
}
}

View File

@ -1,239 +0,0 @@
package org.mariotaku.twidere.task.twitter;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.util.Log;
import com.squareup.otto.Bus;
import org.mariotaku.abstask.library.AbstractTask;
import org.mariotaku.microblog.library.MicroBlog;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.Activity;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.model.ResponseList;
import org.mariotaku.sqliteqb.library.Expression;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.model.ParcelableActivity;
import org.mariotaku.twidere.model.ParcelableCredentials;
import org.mariotaku.twidere.model.RefreshTaskParam;
import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.model.message.GetActivitiesTaskEvent;
import org.mariotaku.twidere.model.util.ParcelableActivityUtils;
import org.mariotaku.twidere.model.util.ParcelableCredentialsUtils;
import org.mariotaku.twidere.provider.TwidereDataStore.Activities;
import org.mariotaku.twidere.util.ContentValuesCreator;
import org.mariotaku.twidere.util.DataStoreUtils;
import org.mariotaku.twidere.util.ErrorInfoStore;
import org.mariotaku.twidere.util.MicroBlogAPIFactory;
import org.mariotaku.twidere.util.ReadStateManager;
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
import org.mariotaku.twidere.util.UriUtils;
import org.mariotaku.twidere.util.UserColorNameManager;
import org.mariotaku.twidere.util.content.ContentResolverUtils;
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
/**
* Created by mariotaku on 16/1/4.
*/
public abstract class GetActivitiesTask extends AbstractTask<RefreshTaskParam, Object, Object>
implements Constants {
protected final Context context;
@Inject
protected SharedPreferencesWrapper preferences;
@Inject
protected Bus bus;
@Inject
protected ErrorInfoStore errorInfoStore;
@Inject
protected ReadStateManager readStateManager;
@Inject
protected UserColorNameManager userColorNameManager;
public GetActivitiesTask(Context context) {
this.context = context;
GeneralComponentHelper.build(context).inject(this);
}
@Override
public Object doLongOperation(RefreshTaskParam param) {
if (param.shouldAbort()) return null;
final UserKey[] accountIds = param.getAccountKeys();
final String[] maxIds = param.getMaxIds();
final long[] maxSortIds = param.getMaxSortIds();
final String[] sinceIds = param.getSinceIds();
final ContentResolver cr = context.getContentResolver();
final int loadItemLimit = preferences.getInt(KEY_LOAD_ITEM_LIMIT);
boolean saveReadPosition = false;
for (int i = 0; i < accountIds.length; i++) {
final UserKey accountKey = accountIds[i];
final boolean noItemsBefore = DataStoreUtils.getActivitiesCount(context, getContentUri(),
accountKey) <= 0;
final ParcelableCredentials credentials = ParcelableCredentialsUtils.getCredentials(context,
accountKey);
if (credentials == null) continue;
final MicroBlog twitter = MicroBlogAPIFactory.getInstance(context, credentials, true,
true);
if (twitter == null) continue;
final Paging paging = new Paging();
paging.count(loadItemLimit);
String maxId = null;
long maxSortId = -1;
if (maxIds != null) {
maxId = maxIds[i];
if (maxSortIds != null) {
maxSortId = maxSortIds[i];
}
if (maxId != null) {
paging.maxId(maxId);
}
}
String sinceId = null;
if (sinceIds != null) {
sinceId = sinceIds[i];
if (sinceId != null) {
paging.sinceId(sinceId);
if (maxIds == null || maxId == null) {
paging.setLatestResults(true);
saveReadPosition = true;
}
}
}
// We should delete old activities has intersection with new items
try {
final ResponseList<Activity> activities = getActivities(twitter, credentials, paging);
storeActivities(cr, loadItemLimit, credentials, noItemsBefore, activities, sinceId,
maxId, false);
if (saveReadPosition) {
saveReadPosition(accountKey, credentials, twitter);
}
errorInfoStore.remove(getErrorInfoKey(), accountKey);
} catch (MicroBlogException e) {
if (BuildConfig.DEBUG) {
Log.w(LOGTAG, e);
}
if (e.getErrorCode() == 220) {
errorInfoStore.put(getErrorInfoKey(), accountKey,
ErrorInfoStore.CODE_NO_ACCESS_FOR_CREDENTIALS);
} else if (e.isCausedByNetworkIssue()) {
errorInfoStore.put(getErrorInfoKey(), accountKey,
ErrorInfoStore.CODE_NETWORK_ERROR);
}
}
}
return null;
}
@NonNull
protected abstract String getErrorInfoKey();
private void storeActivities(ContentResolver cr, int loadItemLimit, ParcelableCredentials credentials,
boolean noItemsBefore, ResponseList<Activity> activities,
final String sinceId, final String maxId, boolean notify) {
long[] deleteBound = new long[2];
Arrays.fill(deleteBound, -1);
List<ContentValues> valuesList = new ArrayList<>();
int minIdx = -1;
long minPositionKey = -1;
if (!activities.isEmpty()) {
final long firstSortId = activities.get(0).getCreatedAt().getTime();
final long lastSortId = activities.get(activities.size() - 1).getCreatedAt().getTime();
// Get id diff of first and last item
final long sortDiff = firstSortId - lastSortId;
for (int i = 0, j = activities.size(); i < j; i++) {
Activity item = activities.get(i);
final ParcelableActivity activity = ParcelableActivityUtils.INSTANCE.fromActivity(item,
credentials.account_key, false);
activity.position_key = GetStatusesTask.getPositionKey(activity.timestamp,
activity.timestamp, lastSortId, sortDiff, i, j);
if (deleteBound[0] < 0) {
deleteBound[0] = activity.min_sort_position;
} else {
deleteBound[0] = Math.min(deleteBound[0], activity.min_sort_position);
}
if (deleteBound[1] < 0) {
deleteBound[1] = activity.max_sort_position;
} else {
deleteBound[1] = Math.max(deleteBound[1], activity.max_sort_position);
}
if (minIdx == -1 || item.compareTo(activities.get(minIdx)) < 0) {
minIdx = i;
minPositionKey = activity.position_key;
}
activity.inserted_date = System.currentTimeMillis();
final ContentValues values = ContentValuesCreator.createActivity(activity,
credentials, userColorNameManager);
valuesList.add(values);
}
}
int olderCount = -1;
if (minPositionKey > 0) {
olderCount = DataStoreUtils.getActivitiesCount(context, getContentUri(), minPositionKey,
Activities.POSITION_KEY, false, credentials.account_key);
}
final Uri writeUri = UriUtils.appendQueryParameters(getContentUri(), QUERY_PARAM_NOTIFY,
notify);
if (deleteBound[0] > 0 && deleteBound[1] > 0) {
final Expression where = Expression.and(
Expression.equalsArgs(Activities.ACCOUNT_KEY),
Expression.greaterEqualsArgs(Activities.MIN_SORT_POSITION),
Expression.lesserEqualsArgs(Activities.MAX_SORT_POSITION)
);
final String[] whereArgs = {credentials.account_key.toString(), String.valueOf(deleteBound[0]),
String.valueOf(deleteBound[1])};
int rowsDeleted = cr.delete(writeUri, where.getSQL(), whereArgs);
// Why loadItemLimit / 2? because it will not acting strange in most cases
boolean insertGap = valuesList.size() >= loadItemLimit && !noItemsBefore && olderCount > 0
&& rowsDeleted <= 0 && activities.size() > loadItemLimit / 2;
if (insertGap && !valuesList.isEmpty()) {
valuesList.get(valuesList.size() - 1).put(Activities.IS_GAP, true);
}
}
ContentResolverUtils.bulkInsert(cr, writeUri, valuesList);
if (maxId != null && sinceId == null) {
final ContentValues noGapValues = new ContentValues();
noGapValues.put(Activities.IS_GAP, false);
final String noGapWhere = Expression.and(Expression.equalsArgs(Activities.ACCOUNT_KEY),
Expression.equalsArgs(Activities.MIN_REQUEST_POSITION),
Expression.equalsArgs(Activities.MAX_REQUEST_POSITION)).getSQL();
final String[] noGapWhereArgs = {credentials.toString(), maxId, maxId};
cr.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs);
}
}
protected abstract void saveReadPosition(@NonNull final UserKey accountId,
ParcelableCredentials credentials, @NonNull final MicroBlog twitter);
protected abstract ResponseList<Activity> getActivities(@NonNull final MicroBlog twitter,
@NonNull final ParcelableCredentials credentials,
@NonNull final Paging paging)
throws MicroBlogException;
@Override
public void afterExecute(Object handler, Object result) {
context.getContentResolver().notifyChange(getContentUri(), null);
bus.post(new GetActivitiesTaskEvent(getContentUri(), false, null));
}
protected abstract Uri getContentUri();
@UiThread
@Override
public void beforeExecute() {
bus.post(new GetActivitiesTaskEvent(getContentUri(), true, null));
}
}

View File

@ -1,273 +0,0 @@
package org.mariotaku.twidere.task.twitter;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import com.squareup.otto.Bus;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.mariotaku.abstask.library.AbstractTask;
import org.mariotaku.abstask.library.TaskStarter;
import org.mariotaku.microblog.library.MicroBlog;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.model.ResponseList;
import org.mariotaku.microblog.library.twitter.model.Status;
import org.mariotaku.sqliteqb.library.Columns;
import org.mariotaku.sqliteqb.library.Expression;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.model.ParcelableCredentials;
import org.mariotaku.twidere.model.ParcelableStatus;
import org.mariotaku.twidere.model.ParcelableStatusValuesCreator;
import org.mariotaku.twidere.model.RefreshTaskParam;
import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.model.message.GetStatusesTaskEvent;
import org.mariotaku.twidere.model.util.ParcelableCredentialsUtils;
import org.mariotaku.twidere.model.util.ParcelableStatusUtils;
import org.mariotaku.twidere.provider.TwidereDataStore.AccountSupportColumns;
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses;
import org.mariotaku.twidere.task.CacheUsersStatusesTask;
import org.mariotaku.twidere.util.AsyncTwitterWrapper;
import org.mariotaku.twidere.util.DataStoreUtils;
import org.mariotaku.twidere.util.ErrorInfoStore;
import org.mariotaku.twidere.util.MicroBlogAPIFactory;
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
import org.mariotaku.twidere.util.TwitterWrapper;
import org.mariotaku.twidere.util.UriUtils;
import org.mariotaku.twidere.util.UserColorNameManager;
import org.mariotaku.twidere.util.content.ContentResolverUtils;
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import edu.tsinghua.hotmobi.HotMobiLogger;
import edu.tsinghua.hotmobi.model.RefreshEvent;
import edu.tsinghua.hotmobi.model.TimelineType;
/**
* Created by mariotaku on 16/1/2.
*/
public abstract class GetStatusesTask extends AbstractTask<RefreshTaskParam,
List<TwitterWrapper.StatusListResponse>, Object> implements Constants {
protected final Context context;
@Inject
protected SharedPreferencesWrapper preferences;
@Inject
protected Bus bus;
@Inject
protected ErrorInfoStore errorInfoStore;
@Inject
protected UserColorNameManager manager;
@Inject
protected AsyncTwitterWrapper wrapper;
public GetStatusesTask(Context context) {
this.context = context;
GeneralComponentHelper.build(context).inject(this);
}
@NonNull
public abstract ResponseList<Status> getStatuses(MicroBlog twitter, Paging paging)
throws MicroBlogException;
@NonNull
protected abstract Uri getContentUri();
@TimelineType
protected abstract String getTimelineType();
@Override
public void afterExecute(Object handler, List<TwitterWrapper.StatusListResponse> result) {
context.getContentResolver().notifyChange(getContentUri(), null);
bus.post(new GetStatusesTaskEvent(getContentUri(), false, AsyncTwitterWrapper.getException(result)));
}
@Override
protected void beforeExecute() {
bus.post(new GetStatusesTaskEvent(getContentUri(), true, null));
}
@Override
public List<TwitterWrapper.StatusListResponse> doLongOperation(final RefreshTaskParam param) {
if (param.shouldAbort()) return Collections.emptyList();
final UserKey[] accountKeys = param.getAccountKeys();
final String[] maxIds = param.getMaxIds();
final String[] sinceIds = param.getSinceIds();
final long[] maxSortIds = param.getMaxSortIds();
final long[] sinceSortIds = param.getSinceSortIds();
final List<TwitterWrapper.StatusListResponse> result = new ArrayList<>();
int idx = 0;
final int loadItemLimit = preferences.getInt(KEY_LOAD_ITEM_LIMIT, DEFAULT_LOAD_ITEM_LIMIT);
for (final UserKey accountKey : accountKeys) {
final ParcelableCredentials credentials = ParcelableCredentialsUtils.getCredentials(context,
accountKey);
if (credentials == null) continue;
final MicroBlog twitter = MicroBlogAPIFactory.getInstance(context, credentials,
true, true);
if (twitter == null) continue;
try {
final Paging paging = new Paging();
paging.count(loadItemLimit);
final String maxId, sinceId;
long maxSortId = -1, sinceSortId = -1;
if (maxIds != null && maxIds[idx] != null) {
maxId = maxIds[idx];
paging.maxId(maxId);
if (maxSortIds != null) {
maxSortId = maxSortIds[idx];
}
} else {
maxSortId = -1;
maxId = null;
}
if (sinceIds != null && sinceIds[idx] != null) {
sinceId = sinceIds[idx];
long sinceIdLong = NumberUtils.toLong(sinceId, -1);
//TODO handle non-twitter case
if (sinceIdLong != -1) {
paging.sinceId(String.valueOf(sinceIdLong - 1));
} else {
paging.sinceId(sinceId);
}
if (sinceSortIds != null) {
sinceSortId = sinceSortIds[idx];
}
if (maxIds == null || sinceIds[idx] == null) {
paging.setLatestResults(true);
}
} else {
sinceId = null;
}
final List<Status> statuses = getStatuses(twitter, paging);
storeStatus(accountKey, credentials, statuses, sinceId, maxId, sinceSortId,
maxSortId, loadItemLimit, false);
// TODO cache related data and preload
final CacheUsersStatusesTask cacheTask = new CacheUsersStatusesTask(context);
cacheTask.setParams(new TwitterWrapper.StatusListResponse(accountKey, statuses));
TaskStarter.execute(cacheTask);
errorInfoStore.remove(getErrorInfoKey(), accountKey.getId());
} catch (final MicroBlogException e) {
if (BuildConfig.DEBUG) {
Log.w(LOGTAG, e);
}
if (e.isCausedByNetworkIssue()) {
errorInfoStore.put(getErrorInfoKey(), accountKey.getId(),
ErrorInfoStore.CODE_NETWORK_ERROR);
}
result.add(new TwitterWrapper.StatusListResponse(accountKey, e));
}
idx++;
}
return result;
}
@NonNull
protected abstract String getErrorInfoKey();
private void storeStatus(@NonNull final UserKey accountKey, ParcelableCredentials credentials,
@NonNull final List<Status> statuses,
final String sinceId, final String maxId,
final long sinceSortId, final long maxSortId,
int loadItemLimit, final boolean notify) {
final Uri uri = getContentUri();
final Uri writeUri = UriUtils.appendQueryParameters(uri, QUERY_PARAM_NOTIFY, notify);
final ContentResolver resolver = context.getContentResolver();
final boolean noItemsBefore = DataStoreUtils.getStatusCount(context, uri, accountKey) <= 0;
final ContentValues[] values = new ContentValues[statuses.size()];
final String[] statusIds = new String[statuses.size()];
int minIdx = -1;
long minPositionKey = -1;
boolean hasIntersection = false;
if (!statuses.isEmpty()) {
final long firstSortId = statuses.get(0).getSortId();
final long lastSortId = statuses.get(statuses.size() - 1).getSortId();
// Get id diff of first and last item
final long sortDiff = firstSortId - lastSortId;
for (int i = 0, j = statuses.size(); i < j; i++) {
final Status item = statuses.get(i);
final ParcelableStatus status = ParcelableStatusUtils.INSTANCE.fromStatus(item, accountKey,
false);
ParcelableStatusUtils.INSTANCE.updateExtraInformation(status, credentials, manager);
status.position_key = getPositionKey(status.timestamp, status.sort_id, lastSortId,
sortDiff, i, j);
status.inserted_date = System.currentTimeMillis();
values[i] = ParcelableStatusValuesCreator.create(status);
if (minIdx == -1 || item.compareTo(statuses.get(minIdx)) < 0) {
minIdx = i;
minPositionKey = status.position_key;
}
if (sinceId != null && item.getSortId() <= sinceSortId) {
hasIntersection = true;
}
statusIds[i] = item.getId();
}
}
// Delete all rows conflicting before new data inserted.
final Expression accountWhere = Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY);
final Expression statusWhere = Expression.inArgs(new Columns.Column(Statuses.STATUS_ID),
statusIds.length);
final String deleteWhere = Expression.and(accountWhere, statusWhere).getSQL();
final String[] deleteWhereArgs = new String[statusIds.length + 1];
System.arraycopy(statusIds, 0, deleteWhereArgs, 1, statusIds.length);
deleteWhereArgs[0] = accountKey.toString();
int olderCount = -1;
if (minPositionKey > 0) {
olderCount = DataStoreUtils.getStatusesCount(context, uri, null, minPositionKey,
Statuses.POSITION_KEY, false, new UserKey[]{accountKey});
}
final int rowsDeleted = resolver.delete(writeUri, deleteWhere, deleteWhereArgs);
// BEGIN HotMobi
final RefreshEvent event = RefreshEvent.create(context, statusIds, getTimelineType());
HotMobiLogger.getInstance(context).log(accountKey, event);
// END HotMobi
// Insert a gap.
final boolean deletedOldGap = rowsDeleted > 0 && ArrayUtils.contains(statusIds, maxId);
final boolean noRowsDeleted = rowsDeleted == 0;
// Why loadItemLimit / 2? because it will not acting strange in most cases
final boolean insertGap = minIdx != -1 && olderCount > 0 && (noRowsDeleted || deletedOldGap)
&& !noItemsBefore && !hasIntersection && statuses.size() > loadItemLimit / 2;
if (insertGap) {
values[minIdx].put(Statuses.IS_GAP, true);
}
// Insert previously fetched items.
ContentResolverUtils.bulkInsert(resolver, writeUri, values);
if (maxId != null && sinceId == null) {
final ContentValues noGapValues = new ContentValues();
noGapValues.put(Statuses.IS_GAP, false);
final String noGapWhere = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.STATUS_ID)).getSQL();
final String[] noGapWhereArgs = {accountKey.toString(), maxId};
resolver.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs);
}
}
public static long getPositionKey(long timestamp, long sortId, long lastSortId, long sortDiff,
int position, int count) {
if (sortDiff == 0) return timestamp;
int extraValue;
if (sortDiff > 0) {
// descent sorted by time
extraValue = count - 1 - position;
} else {
// ascent sorted by time
extraValue = position;
}
return timestamp + (sortId - lastSortId) * (499 - count) / sortDiff + extraValue;
}
}

View File

@ -1,114 +0,0 @@
package org.mariotaku.twidere.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.mariotaku.restfu.http.RestHttpClient;
import org.mariotaku.restfu.okhttp3.OkHttpRestClient;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.util.dagger.DependencyHolder;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
import okhttp3.Authenticator;
import okhttp3.ConnectionPool;
import okhttp3.Credentials;
import okhttp3.Dns;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import static android.text.TextUtils.isEmpty;
/**
* Created by mariotaku on 16/1/27.
*/
public class HttpClientFactory implements Constants {
private HttpClientFactory() {
}
public static RestHttpClient createRestHttpClient(final Context context,
final SharedPreferencesWrapper prefs, final Dns dns,
final ConnectionPool connectionPool) {
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
initOkHttpClient(context, prefs, builder, dns, connectionPool);
return new OkHttpRestClient(builder.build());
}
public static void initOkHttpClient(final Context context, final SharedPreferencesWrapper prefs,
final OkHttpClient.Builder builder, final Dns dns,
final ConnectionPool connectionPool) {
updateHttpClientConfiguration(context, builder, prefs, dns, connectionPool);
DebugModeUtils.initForOkHttpClient(builder);
}
@SuppressLint("SSLCertificateSocketFactoryGetInsecure")
public static void updateHttpClientConfiguration(final Context context,
final OkHttpClient.Builder builder,
final SharedPreferencesWrapper prefs, final Dns dns,
final ConnectionPool connectionPool) {
final boolean enableProxy = prefs.getBoolean(KEY_ENABLE_PROXY, false);
builder.connectTimeout(prefs.getInt(KEY_CONNECTION_TIMEOUT, 10), TimeUnit.SECONDS);
builder.connectionPool(connectionPool);
if (enableProxy) {
final String proxyType = prefs.getString(KEY_PROXY_TYPE, null);
final String proxyHost = prefs.getString(KEY_PROXY_HOST, null);
final int proxyPort = NumberUtils.toInt(prefs.getString(KEY_PROXY_PORT, null), -1);
if (!isEmpty(proxyHost) && TwidereMathUtils.inRange(proxyPort, 0, 65535,
TwidereMathUtils.RANGE_INCLUSIVE_INCLUSIVE)) {
final Proxy.Type type = getProxyType(proxyType);
if (type != Proxy.Type.DIRECT) {
builder.proxy(new Proxy(type, InetSocketAddress.createUnresolved(proxyHost, proxyPort)));
}
}
final String username = prefs.getString(KEY_PROXY_USERNAME, null);
final String password = prefs.getString(KEY_PROXY_PASSWORD, null);
builder.authenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
final Request.Builder builder = response.request().newBuilder();
if (response.code() == 407) {
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
final String credential = Credentials.basic(username, password);
builder.header("Proxy-Authorization", credential);
}
}
return builder.build();
}
});
}
builder.dns(dns);
}
private static Proxy.Type getProxyType(String proxyType) {
if (proxyType == null) return Proxy.Type.DIRECT;
switch (proxyType.toLowerCase()) {
// case "socks": {
// return Proxy.Type.SOCKS;
// }
case "http": {
return Proxy.Type.HTTP;
}
}
return Proxy.Type.DIRECT;
}
public static void reloadConnectivitySettings(Context context) {
final DependencyHolder holder = DependencyHolder.get(context);
final RestHttpClient client = holder.getRestHttpClient();
if (client instanceof OkHttpRestClient) {
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
initOkHttpClient(context, holder.getPreferences(), builder,
holder.getDns(), holder.getConnectionPoll());
final OkHttpRestClient restClient = (OkHttpRestClient) client;
restClient.setClient(builder.build());
}
}
}

View File

@ -181,7 +181,7 @@ public class MicroBlogAPIFactory implements TwidereConstants {
} else {
userAgent = getTwidereUserAgent(context);
}
DependencyHolder holder = DependencyHolder.get(context);
DependencyHolder holder = DependencyHolder.Companion.get(context);
factory.setHttpClient(holder.getRestHttpClient());
factory.setAuthorization(auth);
factory.setEndpoint(endpoint);

View File

@ -1,60 +0,0 @@
/*
* 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;
import android.content.Context;
import android.net.Uri;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.http.ContentType;
import org.mariotaku.restfu.http.HttpRequest;
import org.mariotaku.restfu.http.HttpResponse;
import org.mariotaku.restfu.http.RestHttpClient;
import org.mariotaku.restfu.http.mime.Body;
import org.mariotaku.twidere.activity.ThemedImagePickerActivity;
import org.mariotaku.twidere.util.dagger.DependencyHolder;
import java.io.IOException;
/**
* Created by mariotaku on 15/6/17.
*/
public class RestFuNetworkStreamDownloader extends ThemedImagePickerActivity.NetworkStreamDownloader {
public RestFuNetworkStreamDownloader(Context context) {
super(context);
}
public DownloadResult get(Uri uri) throws IOException {
final RestHttpClient client = DependencyHolder.get(getContext()).getRestHttpClient();
final HttpRequest.Builder builder = new HttpRequest.Builder();
builder.method(GET.METHOD);
builder.url(uri.toString());
final HttpResponse response = client.newCall(builder.build()).execute();
if (response.isSuccessful()) {
final Body body = response.getBody();
final ContentType contentType = body.contentType();
return DownloadResult.get(body.stream(), contentType != null ? contentType.getContentType() : "image/*");
} else {
throw new IOException("Unable to get " + uri);
}
}
}

View File

@ -1,106 +0,0 @@
/*
* 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.dagger;
import android.content.Context;
import org.mariotaku.restfu.http.RestHttpClient;
import org.mariotaku.twidere.util.ActivityTracker;
import org.mariotaku.twidere.util.ExternalThemeManager;
import org.mariotaku.twidere.util.ReadStateManager;
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
import org.mariotaku.twidere.util.TwidereValidator;
import org.mariotaku.twidere.util.net.TwidereDns;
import javax.inject.Inject;
import edu.tsinghua.hotmobi.HotMobiLogger;
import okhttp3.ConnectionPool;
/**
* Created by mariotaku on 15/12/31.
*/
public class DependencyHolder {
private static DependencyHolder sInstance;
@Inject
HotMobiLogger mHotMobiLogger;
@Inject
ReadStateManager mReadStateManager;
@Inject
RestHttpClient mRestHttpClient;
@Inject
ExternalThemeManager mExternalThemeManager;
@Inject
ActivityTracker mActivityTracker;
@Inject
TwidereDns mDns;
@Inject
TwidereValidator mValidator;
@Inject
SharedPreferencesWrapper mPreferences;
@Inject
ConnectionPool mConnectionPoll;
DependencyHolder(Context context) {
GeneralComponentHelper.build(context).inject(this);
}
public static DependencyHolder get(Context context) {
if (sInstance != null) return sInstance;
return sInstance = new DependencyHolder(context);
}
public HotMobiLogger getHotMobiLogger() {
return mHotMobiLogger;
}
public ReadStateManager getReadStateManager() {
return mReadStateManager;
}
public RestHttpClient getRestHttpClient() {
return mRestHttpClient;
}
public ExternalThemeManager getExternalThemeManager() {
return mExternalThemeManager;
}
public ActivityTracker getActivityTracker() {
return mActivityTracker;
}
public TwidereDns getDns() {
return mDns;
}
public TwidereValidator getValidator() {
return mValidator;
}
public SharedPreferencesWrapper getPreferences() {
return mPreferences;
}
public ConnectionPool getConnectionPoll() {
return mConnectionPoll;
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.dagger
import android.content.Context
import edu.tsinghua.hotmobi.HotMobiLogger
import okhttp3.ConnectionPool
import org.mariotaku.restfu.http.RestHttpClient
import org.mariotaku.twidere.model.DefaultFeatures
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.net.TwidereDns
import javax.inject.Inject
/**
* Created by mariotaku on 15/12/31.
*/
class DependencyHolder internal constructor(context: Context) {
@Inject
lateinit var hotMobiLogger: HotMobiLogger
internal set
@Inject
lateinit var readStateManager: ReadStateManager
internal set
@Inject
lateinit var restHttpClient: RestHttpClient
internal set
@Inject
lateinit var externalThemeManager: ExternalThemeManager
internal set
@Inject
lateinit var activityTracker: ActivityTracker
internal set
@Inject
lateinit var dns: TwidereDns
internal set
@Inject
lateinit var validator: TwidereValidator
internal set
@Inject
lateinit var preferences: SharedPreferencesWrapper
internal set
@Inject
lateinit var connectionPoll: ConnectionPool
internal set
@Inject
lateinit var defaultFeatures: DefaultFeatures
internal set
init {
GeneralComponentHelper.build(context).inject(this)
}
companion object {
private var sInstance: DependencyHolder? = null
fun get(context: Context): DependencyHolder {
if (sInstance != null) return sInstance!!
sInstance = DependencyHolder(context)
return sInstance!!
}
}
}

View File

@ -21,11 +21,12 @@ package org.mariotaku.twidere.util.dagger
import android.support.v7.widget.RecyclerView
import dagger.Component
import org.mariotaku.twidere.activity.iface.APIEditorActivity
import org.mariotaku.twidere.activity.BaseActivity
import org.mariotaku.twidere.activity.ComposeActivity
import org.mariotaku.twidere.activity.MediaViewerActivity
import org.mariotaku.twidere.activity.iface.APIEditorActivity
import org.mariotaku.twidere.adapter.*
import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.fragment.*
import org.mariotaku.twidere.loader.MicroBlogAPIStatusesLoader
import org.mariotaku.twidere.loader.ParcelableStatusLoader
@ -133,4 +134,6 @@ interface GeneralComponent {
fun inject(loader: APIEditorActivity.LoadDefaultsChooserDialogFragment.DefaultAPIConfigLoader)
fun inject(task: UpdateStatusTask)
fun inject(application: TwidereApplication)
}

View File

@ -55,12 +55,14 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
private val quotedTextView by lazy { itemView.quotedText }
private val actionButtons by lazy { itemView.actionButtons }
private val mediaLabel by lazy { itemView.mediaLabel }
private val quotedMediaLabel by lazy { itemView.quotedMediaLabel }
private val statusContentLowerSpace by lazy { itemView.statusContentLowerSpace }
private val quotedMediaPreview by lazy { itemView.quotedMediaPreview }
private val favoriteIcon by lazy { itemView.favoriteIcon }
private val retweetIcon by lazy { itemView.retweetIcon }
private val favoriteCountView by lazy { itemView.favoriteCount }
private val mediaLabelTextView by lazy { itemView.mediaLabelText }
private val quotedMediaLabelTextView by lazy { itemView.quotedMediaLabelText }
private val replyButton by lazy { itemView.reply }
private val retweetButton by lazy { itemView.retweet }
private val favoriteButton by lazy { itemView.favorite }
@ -227,12 +229,15 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
if (!adapter.sensitiveContentEnabled && status.is_possibly_sensitive) {
// Sensitive content, show label instead of media view
quotedMediaPreview.visibility = View.GONE
quotedMediaLabel.visibility = View.VISIBLE
} else if (!adapter.mediaPreviewEnabled) {
// Media preview disabled, just show label
quotedMediaPreview.visibility = View.GONE
quotedMediaLabel.visibility = View.VISIBLE
} else {
// Show media
quotedMediaPreview.visibility = View.VISIBLE
quotedMediaLabel.visibility = View.GONE
quotedMediaPreview.displayMedia(status.quoted_media, loader, status.account_key, -1,
null, null)
@ -240,6 +245,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
} else {
// No media, hide all related views
quotedMediaPreview.visibility = View.GONE
quotedMediaLabel.visibility = View.GONE
}
} else {
quotedNameView.visibility = View.GONE

View File

@ -65,8 +65,8 @@ import org.apache.commons.lang3.ObjectUtils
import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.commons.io.StreamUtils
import org.mariotaku.ktextension.toTypedArray
import org.mariotaku.ktextension.setItemChecked
import org.mariotaku.ktextension.toTypedArray
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.Constants.*
import org.mariotaku.twidere.R
@ -103,6 +103,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
lateinit var extractor: Extractor
@Inject
lateinit var validator: TwidereValidator
@Inject
lateinit var defaultFeatures: DefaultFeatures
private var locationManager: LocationManager? = null
private var mTask: AsyncTask<Any, Any, *>? = null
@ -751,10 +753,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
}
private val media: Array<ParcelableMediaUpdate>
get() {
val list = mediaList
return list.toTypedArray()
}
get() = mediaList.toTypedArray()
private val mediaList: List<ParcelableMediaUpdate>
get() = mediaPreviewAdapter!!.asList
@ -1312,9 +1311,11 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
}
private fun updateTextCount() {
val text = ParseUtils.parseString(editText.text)
val validatedCount = if (text != null) validator.getTweetLength(text) else 0
statusTextCount.textCount = validatedCount
var text = editText.text?.toString() ?: return
if (defaultFeatures.isMediaLinkCountsInStatus && media.isNotEmpty()) {
text += " https://twitter.com/example/status/12345678901234567890/photos/1"
}
statusTextCount.textCount = validator.getTweetLength(text)
}
override fun getLightToolbarMode(toolbar: Toolbar?): Int {

View File

@ -34,6 +34,7 @@ import android.support.v4.content.ContextCompat
import android.support.v4.widget.SwipeRefreshLayout
import android.support.v7.app.AppCompatDelegate
import android.support.v7.widget.ActionBarContextView
import android.util.Log
import android.widget.ImageView
import android.widget.TextView
import com.afollestad.appthemeengine.ATE
@ -42,7 +43,11 @@ import com.pnikosis.materialishprogress.ProgressWheel
import com.rengwuxian.materialedittext.MaterialEditText
import nl.komponents.kovenant.android.startKovenant
import nl.komponents.kovenant.android.stopKovenant
import nl.komponents.kovenant.task
import org.apache.commons.lang3.ArrayUtils
import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.ktextension.configure
import org.mariotaku.restfu.http.RestHttpClient
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.R
@ -50,22 +55,38 @@ import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.activity.AssistLauncherActivity
import org.mariotaku.twidere.activity.MainActivity
import org.mariotaku.twidere.activity.MainHondaJOJOActivity
import org.mariotaku.twidere.constant.defaultFeatureLastUpdated
import org.mariotaku.twidere.model.DefaultFeatures
import org.mariotaku.twidere.service.RefreshService
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.content.TwidereSQLiteOpenHelper
import org.mariotaku.twidere.util.dagger.DependencyHolder
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import org.mariotaku.twidere.util.net.TwidereDns
import org.mariotaku.twidere.util.theme.*
import org.mariotaku.twidere.view.ProfileImageView
import org.mariotaku.twidere.view.TabPagerIndicator
import org.mariotaku.twidere.view.ThemedMultiValueSwitch
import org.mariotaku.twidere.view.TimelineContentTextView
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeListener {
@Inject
lateinit internal var activityTracker: ActivityTracker
@Inject
lateinit internal var restHttpClient: RestHttpClient
@Inject
lateinit internal var dns: TwidereDns
@Inject
lateinit internal var defaultFeatures: DefaultFeatures
@Inject
lateinit internal var externalThemeManager: ExternalThemeManager
@Inject
lateinit internal var kPreferences: KPreferences
var handler: Handler? = null
private set
private var mPreferences: SharedPreferences? = null
private var mSQLiteOpenHelper: SQLiteOpenHelper? = null
private var profileImageViewViewProcessor: ProfileImageViewViewProcessor? = null
private var fontFamilyTagProcessor: FontFamilyTagProcessor? = null
@ -102,9 +123,93 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
resetTheme(preferences)
super.onCreate()
startKovenant()
initAppThemeEngine(preferences)
initializeAsyncTask()
initDebugMode()
initBugReport()
handler = Handler()
profileImageViewViewProcessor = ProfileImageViewViewProcessor()
fontFamilyTagProcessor = FontFamilyTagProcessor()
updateEasterEggIcon()
migrateUsageStatisticsPreferences()
Utils.startRefreshServiceIfNeeded(this)
GeneralComponentHelper.build(this).inject(this)
registerActivityLifecycleCallbacks(activityTracker)
listenExternalThemeChange()
loadDefaultFeatures()
}
private fun loadDefaultFeatures() {
val lastUpdated = kPreferences[defaultFeatureLastUpdated]
if (lastUpdated > 0 && TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - lastUpdated) < 12) {
return
}
task {
defaultFeatures.loadRemoteSettings(restHttpClient)
}.success {
if (BuildConfig.DEBUG) {
Log.d(LOGTAG, "Loaded remote features")
}
}.fail {
if (BuildConfig.DEBUG) {
Log.w(LOGTAG, "Unable to load remote features", it)
}
}.always {
kPreferences[defaultFeatureLastUpdated] = System.currentTimeMillis()
}
}
private fun listenExternalThemeChange() {
val packageFilter = IntentFilter()
packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED)
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED)
packageFilter.addAction(Intent.ACTION_PACKAGE_REPLACED)
registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
val packages = packageManager.getPackagesForUid(uid)
val manager = externalThemeManager
if (ArrayUtils.contains(packages, manager.emojiPackageName)) {
manager.reloadEmojiPreferences()
}
}
}, packageFilter)
}
private fun updateEasterEggIcon() {
val pm = packageManager
val main = ComponentName(this, MainActivity::class.java)
val main2 = ComponentName(this, MainHondaJOJOActivity::class.java)
val mainDisabled = pm.getComponentEnabledSetting(main) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED
val main2Disabled = pm.getComponentEnabledSetting(main2) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED
val noEntry = mainDisabled && main2Disabled
if (noEntry) {
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
} else if (!mainDisabled) {
pm.setComponentEnabledSetting(main2, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}
if (!Utils.isComposeNowSupported(this)) {
val assist = ComponentName(this, AssistLauncherActivity::class.java)
pm.setComponentEnabledSetting(assist, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}
}
private fun initAppThemeEngine(preferences: SharedPreferences) {
profileImageViewViewProcessor = configure(ProfileImageViewViewProcessor()) {
setStyle(Utils.getProfileImageStyle(preferences))
}
fontFamilyTagProcessor = configure(FontFamilyTagProcessor()) {
setFontFamily(ThemeUtils.getThemeFontFamily(preferences))
}
ATE.registerViewProcessor(TabPagerIndicator::class.java, TabPagerIndicatorViewProcessor())
ATE.registerViewProcessor(FloatingActionButton::class.java, FloatingActionButtonViewProcessor())
@ -127,9 +232,6 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
ATE.registerTagProcessor(ThemedMultiValueSwitch.PREFIX_TINT, ThemedMultiValueSwitch.TintTagProcessor())
profileImageViewViewProcessor!!.setStyle(Utils.getProfileImageStyle(preferences))
fontFamilyTagProcessor!!.setFontFamily(ThemeUtils.getThemeFontFamily(preferences))
val themeColor = preferences.getInt(KEY_THEME_COLOR, ContextCompat.getColor(this,
R.color.branding_color))
if (!ATE.config(this, VALUE_THEME_NAME_LIGHT).isConfigured) {
@ -142,52 +244,6 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
if (!ATE.config(this, null).isConfigured) {
ATE.config(this, null).accentColor(ThemeUtils.getOptimalAccentColor(themeColor, Color.WHITE)).coloredActionBar(false).coloredStatusBar(false).commit()
}
initializeAsyncTask()
initDebugMode()
initBugReport()
handler = Handler()
val pm = packageManager
val main = ComponentName(this, MainActivity::class.java)
val main2 = ComponentName(this, MainHondaJOJOActivity::class.java)
val mainDisabled = pm.getComponentEnabledSetting(main) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED
val main2Disabled = pm.getComponentEnabledSetting(main2) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED
val noEntry = mainDisabled && main2Disabled
if (noEntry) {
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
} else if (!mainDisabled) {
pm.setComponentEnabledSetting(main2, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}
if (!Utils.isComposeNowSupported(this)) {
val assist = ComponentName(this, AssistLauncherActivity::class.java)
pm.setComponentEnabledSetting(assist, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}
migrateUsageStatisticsPreferences()
Utils.startRefreshServiceIfNeeded(this)
val holder = DependencyHolder.get(this)
registerActivityLifecycleCallbacks(holder.activityTracker)
val packageFilter = IntentFilter()
packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED)
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED)
packageFilter.addAction(Intent.ACTION_PACKAGE_REPLACED)
registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
val packages = packageManager.getPackagesForUid(uid)
val holder = DependencyHolder.get(context)
val manager = holder.externalThemeManager
if (ArrayUtils.contains(packages, manager.emojiPackageName)) {
manager.reloadEmojiPreferences()
}
}
}, packageFilter)
}
private fun initDebugMode() {
@ -226,7 +282,6 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
}
override fun onLowMemory() {
val holder = DependencyHolder.get(this)
super.onLowMemory()
}
@ -248,7 +303,7 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
editor.apply()
}
KEY_EMOJI_SUPPORT -> {
DependencyHolder.get(this).externalThemeManager.reloadEmojiPreferences()
externalThemeManager.reloadEmojiPreferences()
}
KEY_THEME -> {
resetTheme(preferences)
@ -296,8 +351,6 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
}
private fun reloadDnsSettings() {
val holder = DependencyHolder.get(this)
val dns = holder.dns
dns.reloadDnsSettings()
}

View File

@ -29,6 +29,7 @@ val statusShortenerKey = KNullableStringKey(KEY_STATUS_SHORTENER, null)
val mediaUploaderKey = KNullableStringKey(KEY_MEDIA_UPLOADER, null)
val newDocumentApiKey = KBooleanKey(KEY_NEW_DOCUMENT_API, Build.VERSION.SDK_INT == Build.VERSION_CODES.M)
val loadItemLimitKey: KIntKey = KIntKey(KEY_LOAD_ITEM_LIMIT, DEFAULT_LOAD_ITEM_LIMIT)
val defaultFeatureLastUpdated: KLongKey = KLongKey("default_feature_last_updated", -1)
object defaultAPIConfigKey : KPreferenceKey<CustomAPIConfig> {
override fun contains(preferences: SharedPreferences): Boolean {

View File

@ -404,7 +404,7 @@ abstract class AbsStatusesFragment protected constructor() :
}
if (status == null) return
val accountIds = arrayOf(status.account_key)
val maxIds = arrayOf(status.id)
val maxIds = arrayOf<String?>(status.id)
val maxSortIds = longArrayOf(status.sort_id)
getStatuses(BaseRefreshTaskParam(accountIds, maxIds, null, maxSortIds, null))
}
@ -425,8 +425,7 @@ abstract class AbsStatusesFragment protected constructor() :
val context = context ?: return
val adapter = adapter
val status = adapter!!.getStatus(position) ?: return
handleStatusActionClick(context, fragmentManager, twitterWrapper,
holder as StatusViewHolder, status, id)
handleStatusActionClick(context, fragmentManager, twitterWrapper, holder as StatusViewHolder, status, id)
}
override fun createItemDecoration(context: Context, recyclerView: RecyclerView, layoutManager: LinearLayoutManager): RecyclerView.ItemDecoration? {

View File

@ -171,23 +171,21 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
return this@CursorActivitiesFragment.accountKeys
}
override fun getMaxIds(): Array<String>? {
return getOldestActivityIds(accountKeys)
}
override val maxIds: Array<String?>?
get() = getOldestActivityIds(accountKeys)
override fun getMaxSortIds(): LongArray? {
val context = context ?: return null
return DataStoreUtils.getOldestActivityMaxSortPositions(context,
contentUri, accountKeys)
}
override val maxSortIds: LongArray?
get() {
val context = context ?: return null
return DataStoreUtils.getOldestActivityMaxSortPositions(context,
contentUri, accountKeys)
}
override fun hasMaxIds(): Boolean {
return true
}
override val hasMaxIds: Boolean
get() = true
override fun shouldAbort(): Boolean {
return context == null
}
override val shouldAbort: Boolean
get() = context == null
})
}
@ -198,23 +196,21 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
return this@CursorActivitiesFragment.accountKeys
}
override fun getSinceIds(): Array<String>? {
return getNewestActivityIds(accountKeys)
}
override val sinceIds: Array<String?>?
get() = getNewestActivityIds(accountKeys)
override fun getSinceSortIds(): LongArray? {
val context = context ?: return null
return DataStoreUtils.getNewestActivityMaxSortPositions(context,
contentUri, accountKeys)
}
override val sinceSortIds: LongArray?
get() {
val context = context ?: return null
return DataStoreUtils.getNewestActivityMaxSortPositions(context,
contentUri, accountKeys)
}
override fun hasSinceIds(): Boolean {
return true
}
override val hasSinceIds: Boolean
get() = true
override fun shouldAbort(): Boolean {
return context == null
}
override val shouldAbort: Boolean
get() = context == null
})
return true
}
@ -224,7 +220,7 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
return DataStoreUtils.buildActivityFilterWhereClause(table, null)
}
protected fun getNewestActivityIds(accountKeys: Array<UserKey>): Array<String>? {
protected fun getNewestActivityIds(accountKeys: Array<UserKey>): Array<String?>? {
val context = context ?: return null
return DataStoreUtils.getNewestActivityMaxPositions(context, contentUri, accountKeys)
}
@ -242,7 +238,7 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
}
}
protected fun getOldestActivityIds(accountKeys: Array<UserKey>): Array<String>? {
protected fun getOldestActivityIds(accountKeys: Array<UserKey>): Array<String?>? {
val context = context ?: return null
return DataStoreUtils.getOldestActivityMaxPositions(context, contentUri, accountKeys)
}

View File

@ -185,23 +185,21 @@ abstract class CursorStatusesFragment : AbsStatusesFragment() {
return this@CursorStatusesFragment.accountKeys
}
override fun getMaxIds(): Array<String>? {
return getOldestStatusIds(accountKeys)
}
override val maxIds: Array<String?>?
get() = getOldestStatusIds(accountKeys)
override fun getMaxSortIds(): LongArray? {
val context = context ?: return null
return DataStoreUtils.getOldestStatusSortIds(context, contentUri,
accountKeys)
}
override val maxSortIds: LongArray?
get() {
val context = context ?: return null
return DataStoreUtils.getOldestStatusSortIds(context, contentUri,
accountKeys)
}
override fun hasMaxIds(): Boolean {
return true
}
override val hasMaxIds: Boolean
get() = true
override fun shouldAbort(): Boolean {
return context == null
}
override val shouldAbort: Boolean
get() = context == null
})
}
@ -212,23 +210,17 @@ abstract class CursorStatusesFragment : AbsStatusesFragment() {
return this@CursorStatusesFragment.accountKeys
}
override fun hasMaxIds(): Boolean {
return false
}
override val hasMaxIds: Boolean
get() = false
override fun getSinceIds(): Array<String>? {
return getNewestStatusIds(accountKeys)
}
override val sinceIds: Array<String?>?
get() = getNewestStatusIds(accountKeys)
override fun getSinceSortIds(): LongArray? {
val context = context ?: return null
return DataStoreUtils.getNewestStatusSortIds(context, contentUri,
accountKeys)
}
override val sinceSortIds: LongArray?
get() = DataStoreUtils.getNewestStatusSortIds(context, contentUri, accountKeys)
override fun shouldAbort(): Boolean {
return context == null
}
override val shouldAbort: Boolean
get() = context == null
})
return true
}
@ -238,7 +230,7 @@ abstract class CursorStatusesFragment : AbsStatusesFragment() {
return buildStatusFilterWhereClause(table, null)
}
protected fun getNewestStatusIds(accountKeys: Array<UserKey>): Array<String>? {
protected fun getNewestStatusIds(accountKeys: Array<UserKey>): Array<String?>? {
val context = context ?: return null
return DataStoreUtils.getNewestStatusIds(context, contentUri, accountKeys)
}
@ -254,7 +246,7 @@ abstract class CursorStatusesFragment : AbsStatusesFragment() {
}
protected fun getOldestStatusIds(accountKeys: Array<UserKey>): Array<String>? {
protected fun getOldestStatusIds(accountKeys: Array<UserKey>): Array<String?>? {
val context = context ?: return null
return DataStoreUtils.getOldestStatusIds(context, contentUri, accountKeys)
}

View File

@ -56,7 +56,7 @@ class HomeTimelineFragment : CursorStatusesFragment() {
}
override fun getStatuses(param: RefreshTaskParam): Boolean {
if (!param.hasMaxIds()) return twitterWrapper.refreshAll(param.accountKeys)
if (!param.hasMaxIds) return twitterWrapper.refreshAll(param.accountKeys)
return twitterWrapper.getHomeTimelineAsync(param)
}

View File

@ -153,7 +153,7 @@ abstract class ParcelableStatusesFragment : AbsStatusesFragment() {
if (idx < 0) return
val status = adapter.getStatus(idx) ?: return
val accountKeys = arrayOf(status.account_key)
val maxIds = arrayOf(status.id)
val maxIds = arrayOf<String?>(status.id)
page += pageDelta
val param = BaseRefreshTaskParam(accountKeys, maxIds, null)
param.isLoadingMore = true
@ -181,13 +181,16 @@ abstract class ParcelableStatusesFragment : AbsStatusesFragment() {
override fun triggerRefresh(): Boolean {
super.triggerRefresh()
val adapter = adapter
val accountIds = accountKeys
if (adapter!!.statusCount > 0) {
val sinceIds = arrayOf(adapter.getStatus(0)!!.id)
getStatuses(BaseRefreshTaskParam(accountIds, null, sinceIds))
val adapter = adapter ?: return false
val accountKeys = accountKeys
if (adapter.statusCount > 0) {
val firstStatus = adapter.getStatus(0)!!
val sinceIds = Array(accountKeys.size) {
return@Array if (firstStatus.account_key == accountKeys[it]) firstStatus.id else null
}
getStatuses(BaseRefreshTaskParam(accountKeys, null, sinceIds))
} else {
getStatuses(BaseRefreshTaskParam(accountIds, null, null))
getStatuses(BaseRefreshTaskParam(accountKeys, null, null))
}
return true
}

View File

@ -0,0 +1,21 @@
package org.mariotaku.twidere.model
/**
* Created by mariotaku on 16/2/11.
*/
class BaseRefreshTaskParam(
override val accountKeys: Array<UserKey>,
override val maxIds: Array<String?>?,
override val sinceIds: Array<String?>?,
override val maxSortIds: LongArray? = null,
override val sinceSortIds: LongArray? = null
) : RefreshTaskParam {
override var isLoadingMore: Boolean = false
override var shouldAbort: Boolean = false
override val hasMaxIds: Boolean
get() = maxIds != null
override val hasSinceIds: Boolean
get() = sinceIds != null
}

View File

@ -0,0 +1,25 @@
package org.mariotaku.twidere.model
/**
* Created by mariotaku on 16/2/14.
*/
interface RefreshTaskParam {
val accountKeys: Array<UserKey>
val maxIds: Array<String?>?
val sinceIds: Array<String?>?
val maxSortIds: LongArray?
val sinceSortIds: LongArray?
val hasMaxIds: Boolean
val hasSinceIds: Boolean
val isLoadingMore: Boolean
val shouldAbort: Boolean
}

View File

@ -0,0 +1,42 @@
package org.mariotaku.twidere.model
/**
* Created by mariotaku on 16/2/14.
*/
abstract class SimpleRefreshTaskParam : RefreshTaskParam {
internal var cached: Array<UserKey>? = null
override val accountKeys: Array<UserKey>
get() {
if (cached != null) return cached!!
cached = getAccountKeysWorker()
return cached!!
}
abstract fun getAccountKeysWorker(): Array<UserKey>
override val maxIds: Array<String?>?
get() = null
override val sinceIds: Array<String?>?
get() = null
override val hasMaxIds: Boolean
get() = maxIds != null
override val hasSinceIds: Boolean
get() = sinceIds != null
override val sinceSortIds: LongArray?
get() = null
override val maxSortIds: LongArray?
get() = null
override val isLoadingMore: Boolean
get() = false
override val shouldAbort: Boolean
get() = false
}

View File

@ -0,0 +1,101 @@
/*
* 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.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.text.TextUtils
import edu.tsinghua.hotmobi.model.NotificationEvent
import org.apache.commons.lang3.math.NumberUtils
import org.mariotaku.ktextension.toLong
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.CustomTabType
import org.mariotaku.twidere.annotation.NotificationType
import org.mariotaku.twidere.annotation.ReadPositionTag
import org.mariotaku.twidere.constant.IntentConstants.BROADCAST_NOTIFICATION_DELETED
import org.mariotaku.twidere.model.StringLongPair
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.util.CustomTabUtils
import org.mariotaku.twidere.util.UriExtraUtils
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.util.dagger.DependencyHolder
/**
* Created by mariotaku on 15/4/4.
*/
class NotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
when (action) {
BROADCAST_NOTIFICATION_DELETED -> {
val uri = intent.data ?: return
val holder = DependencyHolder.get(context)
@NotificationType
val notificationType = uri.getQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE)
val accountKey = UserKey.valueOf(uri.getQueryParameter(QUERY_PARAM_ACCOUNT_KEY))
val itemId = NumberUtils.toLong(UriExtraUtils.getExtra(uri, "item_id"), -1)
val itemUserId = NumberUtils.toLong(UriExtraUtils.getExtra(uri, "item_user_id"), -1)
val itemUserFollowing = java.lang.Boolean.parseBoolean(UriExtraUtils.getExtra(uri, "item_user_following"))
val timestamp = NumberUtils.toLong(uri.getQueryParameter(QUERY_PARAM_TIMESTAMP), -1)
if (CustomTabType.NOTIFICATIONS_TIMELINE == CustomTabUtils.getTabTypeAlias(notificationType)
&& accountKey != null && itemId != -1L && timestamp != -1L) {
val logger = holder.hotMobiLogger
logger.log(accountKey, NotificationEvent.deleted(context, timestamp, notificationType, accountKey,
itemId, itemUserId, itemUserFollowing))
}
val manager = holder.readStateManager
val paramReadPosition: String = uri.getQueryParameter(QUERY_PARAM_READ_POSITION)
val paramReadPositions: String = uri.getQueryParameter(QUERY_PARAM_READ_POSITIONS)
@ReadPositionTag
val tag = getPositionTag(notificationType)
if (tag != null && !TextUtils.isEmpty(paramReadPosition)) {
manager.setPosition(Utils.getReadPositionTagWithAccount(tag, accountKey),
paramReadPosition.toLong(-1))
} else if (!TextUtils.isEmpty(paramReadPositions)) {
try {
val pairs = StringLongPair.valuesOf(paramReadPositions)
for (pair in pairs) {
manager.setPosition(tag!!, pair.key, pair.value)
}
} catch (ignore: NumberFormatException) {
}
}
}
}
}
@ReadPositionTag
private fun getPositionTag(@NotificationType type: String?): String? {
if (type == null) return null
when (type) {
NotificationType.HOME_TIMELINE -> return ReadPositionTag.HOME_TIMELINE
NotificationType.INTERACTIONS -> return ReadPositionTag.ACTIVITIES_ABOUT_ME
NotificationType.DIRECT_MESSAGES -> {
return ReadPositionTag.DIRECT_MESSAGES
}
}
return null
}
}

View File

@ -0,0 +1,35 @@
package org.mariotaku.twidere.task
import android.content.Context
import android.net.Uri
import edu.tsinghua.hotmobi.model.TimelineType
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.provider.TwidereDataStore
import org.mariotaku.twidere.task.twitter.GetStatusesTask
import org.mariotaku.twidere.util.ErrorInfoStore
/**
* Created by mariotaku on 16/2/11.
*/
class GetHomeTimelineTask(context: Context) : GetStatusesTask(context) {
@Throws(MicroBlogException::class)
override fun getStatuses(twitter: MicroBlog, paging: Paging): ResponseList<Status> {
return twitter.getHomeTimeline(paging)
}
override val contentUri: Uri
get() = TwidereDataStore.Statuses.CONTENT_URI
@TimelineType
override val timelineType: String
get() = TimelineType.HOME
override val errorInfoKey: String
get() = ErrorInfoStore.KEY_HOME_TIMELINE
}

View File

@ -0,0 +1,214 @@
package org.mariotaku.twidere.task.twitter
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.support.annotation.UiThread
import android.util.Log
import com.squareup.otto.Bus
import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFY
import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_LOAD_ITEM_LIMIT
import org.mariotaku.twidere.model.ParcelableCredentials
import org.mariotaku.twidere.model.RefreshTaskParam
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.message.GetActivitiesTaskEvent
import org.mariotaku.twidere.model.util.ParcelableActivityUtils
import org.mariotaku.twidere.model.util.ParcelableCredentialsUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.content.ContentResolverUtils
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import java.util.*
import javax.inject.Inject
/**
* Created by mariotaku on 16/1/4.
*/
abstract class GetActivitiesTask(protected val context: Context) : AbstractTask<RefreshTaskParam, Any, Any>(), Constants {
@Inject
lateinit var preferences: SharedPreferencesWrapper
@Inject
lateinit var bus: Bus
@Inject
lateinit var errorInfoStore: ErrorInfoStore
@Inject
lateinit var readStateManager: ReadStateManager
@Inject
lateinit var userColorNameManager: UserColorNameManager
init {
GeneralComponentHelper.build(context).inject(this)
}
public override fun doLongOperation(param: RefreshTaskParam): Any? {
if (param.shouldAbort) return null
val accountIds = param.accountKeys
val maxIds = param.maxIds
val maxSortIds = param.maxSortIds
val sinceIds = param.sinceIds
val cr = context.contentResolver
val loadItemLimit = preferences.getInt(KEY_LOAD_ITEM_LIMIT)
var saveReadPosition = false
for (i in accountIds.indices) {
val accountKey = accountIds[i]
val noItemsBefore = DataStoreUtils.getActivitiesCount(context, contentUri,
accountKey) <= 0
val credentials = ParcelableCredentialsUtils.getCredentials(context,
accountKey) ?: continue
val twitter = MicroBlogAPIFactory.getInstance(context, credentials, true,
true) ?: continue
val paging = Paging()
paging.count(loadItemLimit)
var maxId: String? = null
var maxSortId: Long = -1
if (maxIds != null) {
maxId = maxIds[i]
if (maxSortIds != null) {
maxSortId = maxSortIds[i]
}
if (maxId != null) {
paging.maxId(maxId)
}
}
var sinceId: String? = null
if (sinceIds != null) {
sinceId = sinceIds[i]
if (sinceId != null) {
paging.sinceId(sinceId)
if (maxIds == null || maxId == null) {
paging.setLatestResults(true)
saveReadPosition = true
}
}
}
// We should delete old activities has intersection with new items
try {
val activities = getActivities(twitter, credentials, paging)
storeActivities(cr, loadItemLimit, credentials, noItemsBefore, activities, sinceId,
maxId, false)
if (saveReadPosition) {
saveReadPosition(accountKey, credentials, twitter)
}
errorInfoStore.remove(errorInfoKey, accountKey)
} catch (e: MicroBlogException) {
if (BuildConfig.DEBUG) {
Log.w(LOGTAG, e)
}
if (e.errorCode == 220) {
errorInfoStore.put(errorInfoKey, accountKey,
ErrorInfoStore.CODE_NO_ACCESS_FOR_CREDENTIALS)
} else if (e.isCausedByNetworkIssue) {
errorInfoStore.put(errorInfoKey, accountKey,
ErrorInfoStore.CODE_NETWORK_ERROR)
}
}
}
return null
}
protected abstract val errorInfoKey: String
private fun storeActivities(cr: ContentResolver, loadItemLimit: Int, credentials: ParcelableCredentials,
noItemsBefore: Boolean, activities: ResponseList<Activity>,
sinceId: String?, maxId: String?, notify: Boolean) {
val deleteBound = LongArray(2, { return@LongArray -1 })
val valuesList = ArrayList<ContentValues>()
var minIdx = -1
var minPositionKey: Long = -1
if (!activities.isEmpty()) {
val firstSortId = activities.first().createdAt.time
val lastSortId = activities.last().createdAt.time
// Get id diff of first and last item
val sortDiff = firstSortId - lastSortId
for (i in activities.indices) {
val item = activities[i]
val activity = ParcelableActivityUtils.fromActivity(item,
credentials.account_key, false)
activity.position_key = GetStatusesTask.getPositionKey(activity.timestamp,
activity.timestamp, lastSortId, sortDiff, i, activities.size)
if (deleteBound[0] < 0) {
deleteBound[0] = activity.min_sort_position
} else {
deleteBound[0] = Math.min(deleteBound[0], activity.min_sort_position)
}
if (deleteBound[1] < 0) {
deleteBound[1] = activity.max_sort_position
} else {
deleteBound[1] = Math.max(deleteBound[1], activity.max_sort_position)
}
if (minIdx == -1 || item < activities[minIdx]) {
minIdx = i
minPositionKey = activity.position_key
}
activity.inserted_date = System.currentTimeMillis()
val values = ContentValuesCreator.createActivity(activity,
credentials, userColorNameManager)
valuesList.add(values)
}
}
var olderCount = -1
if (minPositionKey > 0) {
olderCount = DataStoreUtils.getActivitiesCount(context, contentUri, minPositionKey,
Activities.POSITION_KEY, false, credentials.account_key)
}
val writeUri = UriUtils.appendQueryParameters(contentUri, QUERY_PARAM_NOTIFY, notify)
if (deleteBound[0] > 0 && deleteBound[1] > 0) {
val where = Expression.and(
Expression.equalsArgs(Activities.ACCOUNT_KEY),
Expression.greaterEqualsArgs(Activities.MIN_SORT_POSITION),
Expression.lesserEqualsArgs(Activities.MAX_SORT_POSITION))
val whereArgs = arrayOf(credentials.account_key.toString(), deleteBound[0].toString(), deleteBound[1].toString())
val rowsDeleted = cr.delete(writeUri, where.sql, whereArgs)
// Why loadItemLimit / 2? because it will not acting strange in most cases
val insertGap = valuesList.size >= loadItemLimit && !noItemsBefore && olderCount > 0
&& rowsDeleted <= 0 && activities.size > loadItemLimit / 2
if (insertGap && !valuesList.isEmpty()) {
valuesList[valuesList.size - 1].put(Activities.IS_GAP, true)
}
}
ContentResolverUtils.bulkInsert(cr, writeUri, valuesList)
if (maxId != null && sinceId == null) {
val noGapValues = ContentValues()
noGapValues.put(Activities.IS_GAP, false)
val noGapWhere = Expression.and(Expression.equalsArgs(Activities.ACCOUNT_KEY),
Expression.equalsArgs(Activities.MIN_REQUEST_POSITION),
Expression.equalsArgs(Activities.MAX_REQUEST_POSITION)).sql
val noGapWhereArgs = arrayOf(credentials.toString(), maxId, maxId)
cr.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs)
}
}
protected abstract fun saveReadPosition(accountId: UserKey,
credentials: ParcelableCredentials, twitter: MicroBlog)
@Throws(MicroBlogException::class)
protected abstract fun getActivities(twitter: MicroBlog,
credentials: ParcelableCredentials,
paging: Paging): ResponseList<Activity>
public override fun afterExecute(handler: Any?, result: Any?) {
context.contentResolver.notifyChange(contentUri, null)
bus.post(GetActivitiesTaskEvent(contentUri, false, null))
}
protected abstract val contentUri: Uri
@UiThread
public override fun beforeExecute() {
bus.post(GetActivitiesTaskEvent(contentUri, true, null))
}
}

View File

@ -0,0 +1,249 @@
package org.mariotaku.twidere.task.twitter
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.util.Log
import com.squareup.otto.Bus
import edu.tsinghua.hotmobi.HotMobiLogger
import edu.tsinghua.hotmobi.model.RefreshEvent
import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.math.NumberUtils
import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.sqliteqb.library.Columns
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFY
import org.mariotaku.twidere.constant.loadItemLimitKey
import org.mariotaku.twidere.model.ParcelableCredentials
import org.mariotaku.twidere.model.ParcelableStatusValuesCreator
import org.mariotaku.twidere.model.RefreshTaskParam
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.message.GetStatusesTaskEvent
import org.mariotaku.twidere.model.util.ParcelableCredentialsUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.provider.TwidereDataStore.AccountSupportColumns
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.CacheUsersStatusesTask
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.content.ContentResolverUtils
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import java.util.*
import javax.inject.Inject
/**
* Created by mariotaku on 16/1/2.
*/
abstract class GetStatusesTask(protected val context: Context) : AbstractTask<RefreshTaskParam, List<TwitterWrapper.StatusListResponse>, Any>(), Constants {
@Inject
lateinit var preferences: KPreferences
@Inject
lateinit var bus: Bus
@Inject
lateinit var errorInfoStore: ErrorInfoStore
@Inject
lateinit var manager: UserColorNameManager
@Inject
lateinit var wrapper: AsyncTwitterWrapper
init {
GeneralComponentHelper.build(context).inject(this)
}
@Throws(MicroBlogException::class)
abstract fun getStatuses(twitter: MicroBlog, paging: Paging): ResponseList<Status>
protected abstract val contentUri: Uri
protected abstract val timelineType: String
public override fun afterExecute(handler: Any?, result: List<TwitterWrapper.StatusListResponse>?) {
context.contentResolver.notifyChange(contentUri, null)
bus.post(GetStatusesTaskEvent(contentUri, false, AsyncTwitterWrapper.getException(result)))
}
override fun beforeExecute() {
bus.post(GetStatusesTaskEvent(contentUri, true, null))
}
protected abstract val errorInfoKey: String
public override fun doLongOperation(param: RefreshTaskParam): List<TwitterWrapper.StatusListResponse> {
if (param.shouldAbort) return emptyList()
val accountKeys = param.accountKeys
val maxIds = param.maxIds
val sinceIds = param.sinceIds
val maxSortIds = param.maxSortIds
val sinceSortIds = param.sinceSortIds
val result = ArrayList<TwitterWrapper.StatusListResponse>()
val loadItemLimit = preferences[loadItemLimitKey]
for (i in 0 until accountKeys.size) {
val accountKey = accountKeys[i]
val credentials = ParcelableCredentialsUtils.getCredentials(context,
accountKey) ?: continue
val twitter = MicroBlogAPIFactory.getInstance(context, credentials,
true, true) ?: continue
try {
val paging = Paging()
paging.count(loadItemLimit)
val maxId: String?
val sinceId: String?
var maxSortId: Long = -1
var sinceSortId: Long = -1
if (maxIds != null && maxIds[i] != null) {
maxId = maxIds[i]
paging.maxId(maxId)
if (maxSortIds != null) {
maxSortId = maxSortIds[i]
}
} else {
maxSortId = -1
maxId = null
}
if (sinceIds != null && sinceIds[i] != null) {
sinceId = sinceIds[i]
val sinceIdLong = NumberUtils.toLong(sinceId, -1)
//TODO handle non-twitter case
if (sinceIdLong != -1L) {
paging.sinceId((sinceIdLong - 1).toString())
} else {
paging.sinceId(sinceId)
}
if (sinceSortIds != null) {
sinceSortId = sinceSortIds[i]
}
if (maxIds == null) {
paging.setLatestResults(true)
}
} else {
sinceId = null
}
val statuses = getStatuses(twitter, paging)
storeStatus(accountKey, credentials, statuses, sinceId, maxId, sinceSortId,
maxSortId, loadItemLimit, false)
// TODO cache related data and preload
val cacheTask = CacheUsersStatusesTask(context)
cacheTask.params = TwitterWrapper.StatusListResponse(accountKey, statuses)
TaskStarter.execute(cacheTask)
errorInfoStore.remove(errorInfoKey, accountKey.id)
} catch (e: MicroBlogException) {
if (BuildConfig.DEBUG) {
Log.w(LOGTAG, e)
}
if (e.isCausedByNetworkIssue) {
errorInfoStore.put(errorInfoKey, accountKey.id,
ErrorInfoStore.CODE_NETWORK_ERROR)
}
result.add(TwitterWrapper.StatusListResponse(accountKey, e))
}
}
return result
}
private fun storeStatus(accountKey: UserKey, credentials: ParcelableCredentials,
statuses: List<Status>,
sinceId: String?, maxId: String?,
sinceSortId: Long, maxSortId: Long,
loadItemLimit: Int, notify: Boolean) {
val uri = contentUri
val writeUri = UriUtils.appendQueryParameters(uri, QUERY_PARAM_NOTIFY, notify)
val resolver = context.contentResolver
val noItemsBefore = DataStoreUtils.getStatusCount(context, uri, accountKey) <= 0
val values = arrayOfNulls<ContentValues>(statuses.size)
val statusIds = arrayOfNulls<String>(statuses.size)
var minIdx = -1
var minPositionKey: Long = -1
var hasIntersection = false
if (!statuses.isEmpty()) {
val firstSortId = statuses.first().sortId
val lastSortId = statuses.last().sortId
// Get id diff of first and last item
val sortDiff = firstSortId - lastSortId
for (i in 0 until statuses.size) {
val item = statuses[i]
val status = ParcelableStatusUtils.fromStatus(item, accountKey,
false)
ParcelableStatusUtils.updateExtraInformation(status, credentials, manager)
status.position_key = getPositionKey(status.timestamp, status.sort_id, lastSortId,
sortDiff, i, statuses.size)
status.inserted_date = System.currentTimeMillis()
values[i] = ParcelableStatusValuesCreator.create(status)
if (minIdx == -1 || item < statuses[minIdx]) {
minIdx = i
minPositionKey = status.position_key
}
if (sinceId != null && item.sortId <= sinceSortId) {
hasIntersection = true
}
statusIds[i] = item.id
}
}
// Delete all rows conflicting before new data inserted.
val accountWhere = Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY)
val statusWhere = Expression.inArgs(Columns.Column(Statuses.STATUS_ID),
statusIds.size)
val deleteWhere = Expression.and(accountWhere, statusWhere).sql
val deleteWhereArgs = arrayOf(accountKey.toString(), *statusIds)
var olderCount = -1
if (minPositionKey > 0) {
olderCount = DataStoreUtils.getStatusesCount(context, uri, null, minPositionKey,
Statuses.POSITION_KEY, false, arrayOf(accountKey))
}
val rowsDeleted = resolver.delete(writeUri, deleteWhere, deleteWhereArgs)
// BEGIN HotMobi
val event = RefreshEvent.create(context, statusIds, timelineType)
HotMobiLogger.getInstance(context).log(accountKey, event)
// END HotMobi
// Insert a gap.
val deletedOldGap = rowsDeleted > 0 && ArrayUtils.contains(statusIds, maxId)
val noRowsDeleted = rowsDeleted == 0
// Why loadItemLimit / 2? because it will not acting strange in most cases
val insertGap = minIdx != -1 && olderCount > 0 && (noRowsDeleted || deletedOldGap)
&& !noItemsBefore && !hasIntersection && statuses.size > loadItemLimit / 2
if (insertGap) {
values[minIdx]!!.put(Statuses.IS_GAP, true)
}
// Insert previously fetched items.
ContentResolverUtils.bulkInsert(resolver, writeUri, values)
// Remove gap flag
if (maxId != null && sinceId == null) {
val noGapValues = ContentValues()
noGapValues.put(Statuses.IS_GAP, false)
val noGapWhere = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.STATUS_ID)).sql
val noGapWhereArgs = arrayOf(accountKey.toString(), maxId)
resolver.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs)
}
}
companion object {
fun getPositionKey(timestamp: Long, sortId: Long, lastSortId: Long, sortDiff: Long,
position: Int, count: Int): Long {
if (sortDiff == 0L) return timestamp
val extraValue: Int
if (sortDiff > 0) {
// descent sorted by time
extraValue = count - 1 - position
} else {
// ascent sorted by time
extraValue = position
}
return timestamp + (sortId - lastSortId) * (499 - count) / sortDiff + extraValue.toLong()
}
}
}

View File

@ -0,0 +1,98 @@
package org.mariotaku.twidere.util
import android.annotation.SuppressLint
import android.content.Context
import android.text.TextUtils
import android.text.TextUtils.isEmpty
import okhttp3.ConnectionPool
import okhttp3.Credentials
import okhttp3.Dns
import okhttp3.OkHttpClient
import org.apache.commons.lang3.math.NumberUtils
import org.mariotaku.restfu.http.RestHttpClient
import org.mariotaku.restfu.okhttp3.OkHttpRestClient
import org.mariotaku.twidere.constant.SharedPreferenceConstants.*
import org.mariotaku.twidere.util.dagger.DependencyHolder
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
/**
* Created by mariotaku on 16/1/27.
*/
object HttpClientFactory {
fun createRestHttpClient(context: Context,
prefs: SharedPreferencesWrapper, dns: Dns,
connectionPool: ConnectionPool): RestHttpClient {
val builder = OkHttpClient.Builder()
initOkHttpClient(context, prefs, builder, dns, connectionPool)
return OkHttpRestClient(builder.build())
}
fun initOkHttpClient(context: Context, prefs: SharedPreferencesWrapper,
builder: OkHttpClient.Builder, dns: Dns,
connectionPool: ConnectionPool) {
updateHttpClientConfiguration(context, builder, prefs, dns, connectionPool)
DebugModeUtils.initForOkHttpClient(builder)
}
@SuppressLint("SSLCertificateSocketFactoryGetInsecure")
fun updateHttpClientConfiguration(context: Context,
builder: OkHttpClient.Builder,
prefs: SharedPreferencesWrapper, dns: Dns,
connectionPool: ConnectionPool) {
val enableProxy = prefs.getBoolean(KEY_ENABLE_PROXY, false)
builder.connectTimeout(prefs.getInt(KEY_CONNECTION_TIMEOUT, 10).toLong(), TimeUnit.SECONDS)
builder.connectionPool(connectionPool)
if (enableProxy) {
val proxyType = prefs.getString(KEY_PROXY_TYPE, null)
val proxyHost = prefs.getString(KEY_PROXY_HOST, null)
val proxyPort = NumberUtils.toInt(prefs.getString(KEY_PROXY_PORT, null), -1)
if (!isEmpty(proxyHost) && TwidereMathUtils.inRange(proxyPort, 0, 65535,
TwidereMathUtils.RANGE_INCLUSIVE_INCLUSIVE)) {
val type = getProxyType(proxyType)
if (type != Proxy.Type.DIRECT) {
builder.proxy(Proxy(type, InetSocketAddress.createUnresolved(proxyHost, proxyPort)))
}
}
val username = prefs.getString(KEY_PROXY_USERNAME, null)
val password = prefs.getString(KEY_PROXY_PASSWORD, null)
builder.authenticator { route, response ->
val b = response.request().newBuilder()
if (response.code() == 407) {
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
val credential = Credentials.basic(username, password)
b.header("Proxy-Authorization", credential)
}
}
b.build()
}
}
builder.dns(dns)
}
private fun getProxyType(proxyType: String?): Proxy.Type {
if (proxyType == null) return Proxy.Type.DIRECT
when (proxyType.toLowerCase()) {
// case "socks": {
// return Proxy.Type.SOCKS;
// }
"http" -> {
return Proxy.Type.HTTP
}
}
return Proxy.Type.DIRECT
}
fun reloadConnectivitySettings(context: Context) {
val holder = DependencyHolder.get(context)
val client = holder.restHttpClient
if (client is OkHttpRestClient) {
val builder = OkHttpClient.Builder()
initOkHttpClient(context, holder.preferences, builder,
holder.dns, holder.connectionPoll)
client.client = builder.build()
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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
import android.content.Context
import android.net.Uri
import org.mariotaku.pickncrop.library.ImagePickerActivity
import org.mariotaku.restfu.annotation.method.GET
import org.mariotaku.restfu.http.HttpRequest
import org.mariotaku.twidere.util.dagger.DependencyHolder
import java.io.IOException
/**
* Created by mariotaku on 15/6/17.
*/
class RestFuNetworkStreamDownloader(context: Context) : ImagePickerActivity.NetworkStreamDownloader(context) {
@Throws(IOException::class)
override operator fun get(uri: Uri): ImagePickerActivity.NetworkStreamDownloader.DownloadResult {
val client = DependencyHolder.get(context).restHttpClient
val builder = HttpRequest.Builder()
builder.method(GET.METHOD)
builder.url(uri.toString())
val response = client.newCall(builder.build()).execute()
if (response.isSuccessful) {
val body = response.body
val contentType = body.contentType()
return ImagePickerActivity.NetworkStreamDownloader.DownloadResult.get(body.stream(), if (contentType != null) contentType!!.contentType else "image/*")
} else {
throw IOException("Unable to get " + uri)
}
}
}

View File

@ -36,12 +36,14 @@ import dagger.Module
import dagger.Provides
import edu.tsinghua.hotmobi.HotMobiLogger
import okhttp3.ConnectionPool
import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.mediaviewer.library.FileCache
import org.mariotaku.mediaviewer.library.MediaDownloader
import org.mariotaku.restfu.http.RestHttpClient
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.constant.SharedPreferenceConstants
import org.mariotaku.twidere.model.DefaultFeatures
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.imageloader.ReadOnlyDiskLRUNameCache
import org.mariotaku.twidere.util.imageloader.TwidereImageDownloader
@ -89,6 +91,12 @@ class ApplicationModule(private val application: Application) {
Context.MODE_PRIVATE, SharedPreferenceConstants::class.java)
}
@Provides
@Singleton
fun kPreferences(sharedPreferences: SharedPreferencesWrapper): KPreferences {
return KPreferences(sharedPreferences)
}
@Provides
@Singleton
fun permissionsManager(): PermissionsManager {
@ -220,6 +228,12 @@ class ApplicationModule(private val application: Application) {
return BidiFormatter.getInstance()
}
@Provides
@Singleton
fun defaultFeatures(): DefaultFeatures {
return DefaultFeatures()
}
private fun createDiskCache(dirName: String, preferences: SharedPreferencesWrapper): DiskCache {
val cacheDir = Utils.getExternalCacheDir(application, dirName)
val fallbackCacheDir = Utils.getInternalCacheDir(application, dirName)

View File

@ -256,6 +256,8 @@
android:id="@+id/quotedName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignTop="@+id/quotedMediaPreview"
android:layout_toEndOf="@+id/quotedMediaPreview"
android:layout_toRightOf="@+id/quotedMediaPreview"
@ -278,7 +280,9 @@
android:id="@+id/quotedText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/quotedName"
android:layout_alignLeft="@+id/quotedName"
android:layout_alignRight="@+id/quotedName"
android:layout_alignStart="@+id/quotedName"
android:layout_below="@+id/quotedName"
android:paddingBottom="@dimen/element_spacing_small"
@ -294,6 +298,38 @@
tools:text="@string/sample_status_text"
tools:visibility="visible"/>
<LinearLayout
android:id="@+id/quotedMediaLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/quotedName"
android:layout_alignLeft="@+id/quotedName"
android:layout_alignRight="@+id/quotedName"
android:layout_alignStart="@+id/quotedName"
android:layout_below="@+id/quotedText"
android:layout_marginTop="@dimen/element_spacing_xsmall"
android:gravity="center_vertical"
android:orientation="horizontal">
<org.mariotaku.twidere.view.IconActionView
android:layout_width="@dimen/element_size_small"
android:layout_height="@dimen/element_size_small"
android:layout_weight="0"
android:color="?android:textColorSecondary"
android:scaleType="centerInside"
android:src="@drawable/ic_action_gallery"
tools:tint="?android:textColorSecondary"/>
<TextView
android:id="@+id/quotedMediaLabelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/media"
android:textAppearance="?android:textAppearanceSmall"
android:textStyle="bold"/>
</LinearLayout>
</org.mariotaku.twidere.view.ColorLabelRelativeLayout>
</RelativeLayout>