1
0
mirror of https://github.com/TwidereProject/Twidere-Android synced 2025-02-08 15:58:40 +01:00

added multiple dns resolvers support

implementing clear notifications
This commit is contained in:
Mariotaku Lee 2017-04-06 23:27:47 +08:00
parent 101d63acec
commit 629f0ae0f6
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
21 changed files with 584 additions and 508 deletions

View File

@ -118,6 +118,7 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst
String AUTHORITY_RETWEETS_OF_ME = "retweets_of_me";
String AUTHORITY_MUTES_USERS = "mutes_users";
String AUTHORITY_INTERACTIONS = "interactions";
String AUTHORITY_NOTIFICATIONS = "notifications";
String AUTHORITY_ACCOUNTS = "accounts";
String AUTHORITY_DRAFTS = "drafts";
String AUTHORITY_FILTERS = "filters";
@ -155,7 +156,7 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst
String QUERY_PARAM_FINISH_ONLY = "finish_only";
String QUERY_PARAM_NEW_ITEMS_COUNT = "new_items_count";
String QUERY_PARAM_CONVERSATION_ID = "conversation_id";
String QUERY_PARAM_READ_POSITION = "param_read_position";
String QUERY_PARAM_READ_POSITION = "read_position";
String QUERY_PARAM_LIMIT = "limit";
String QUERY_PARAM_EXTRA = "extra";
String QUERY_PARAM_TIMESTAMP = "timestamp";

View File

@ -55,7 +55,7 @@ public class SystemHosts {
final String host = scanner.next();
if (host.startsWith("#")) break;
if (TextUtils.equals(hostToResolve, host)) {
final InetAddress resolved = TwidereDns.getResolvedIPAddress(host, address);
final InetAddress resolved = TwidereDns.Companion.getResolvedIPAddress(host, address);
if (resolved != null) return Collections.singletonList(resolved);
}
}

View File

@ -1,320 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.net;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.util.TimingLogger;
import org.apache.commons.lang3.StringUtils;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
import org.xbill.DNS.AAAARecord;
import org.xbill.DNS.ARecord;
import org.xbill.DNS.Address;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.Resolver;
import org.xbill.DNS.SimpleResolver;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import javax.inject.Singleton;
import okhttp3.Dns;
import static org.mariotaku.twidere.TwidereConstants.HOST_MAPPING_PREFERENCES_NAME;
import static org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_BUILTIN_DNS_RESOLVER;
import static org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_DNS_SERVER;
import static org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_TCP_DNS_QUERY;
@Singleton
public class TwidereDns implements Dns {
private static final String RESOLVER_LOGTAG = "TwidereDns";
private final SharedPreferences hostMapping;
private final SharedPreferencesWrapper preferences;
private final SystemHosts systemHosts;
private Resolver mResolver;
private boolean mUseResolver;
public TwidereDns(final Context context, SharedPreferencesWrapper preferences) {
this.preferences = preferences;
hostMapping = SharedPreferencesWrapper.getInstance(context, HOST_MAPPING_PREFERENCES_NAME, Context.MODE_PRIVATE);
systemHosts = new SystemHosts();
reloadDnsSettings();
}
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
try {
return resolveInternal(hostname, hostname, 0, mUseResolver);
} catch (IOException e) {
if (e instanceof UnknownHostException) throw (UnknownHostException) e;
throw new UnknownHostException("Unable to resolve address " + e.getMessage());
} catch (SecurityException e) {
throw new UnknownHostException("Security exception" + e.getMessage());
}
}
public List<InetAddress> lookupResolver(String hostname) throws UnknownHostException {
try {
return resolveInternal(hostname, hostname, 0, true);
} catch (IOException e) {
if (e instanceof UnknownHostException) throw (UnknownHostException) e;
throw new UnknownHostException("Unable to resolve address " + e.getMessage());
} catch (SecurityException e) {
throw new UnknownHostException("Security exception" + e.getMessage());
}
}
public void reloadDnsSettings() {
mResolver = null;
mUseResolver = preferences.getBoolean(KEY_BUILTIN_DNS_RESOLVER);
}
@NonNull
private List<InetAddress> resolveInternal(final String originalHost, final String host, final int depth,
final boolean useResolver) throws IOException, SecurityException {
final TimingLogger logger = new TimingLogger(RESOLVER_LOGTAG, "resolve");
// Return if host is an address
final List<InetAddress> fromAddressString = fromAddressString(originalHost, host);
if (fromAddressString != null) {
addLogSplit(logger, host, "valid ip address", depth);
dumpLog(logger, fromAddressString);
return fromAddressString;
}
// Load from custom mapping
addLogSplit(logger, host, "start custom mapping resolve", depth);
final List<InetAddress> fromMapping = getFromMapping(host);
addLogSplit(logger, host, "end custom mapping resolve", depth);
if (fromMapping != null) {
dumpLog(logger, fromMapping);
return fromMapping;
}
if (useResolver) {
// Load from /etc/hosts, since Dnsjava doesn't support hosts entry lookup
addLogSplit(logger, host, "start /etc/hosts resolve", depth);
final List<InetAddress> fromSystemHosts = fromSystemHosts(host);
addLogSplit(logger, host, "end /etc/hosts resolve", depth);
if (fromSystemHosts != null) {
dumpLog(logger, fromSystemHosts);
return fromSystemHosts;
}
// Use DNS resolver
addLogSplit(logger, host, "start resolver resolve", depth);
final List<InetAddress> fromResolver = fromResolver(originalHost, host);
addLogSplit(logger, host, "end resolver resolve", depth);
if (fromResolver != null) {
dumpLog(logger, fromResolver);
return fromResolver;
}
}
addLogSplit(logger, host, "start system default resolve", depth);
final List<InetAddress> fromDefault = Arrays.asList(InetAddress.getAllByName(host));
addLogSplit(logger, host, "end system default resolve", depth);
dumpLog(logger, fromDefault);
return fromDefault;
}
private void dumpLog(final TimingLogger logger, @NonNull List<InetAddress> addresses) {
if (BuildConfig.DEBUG) return;
Log.v(RESOLVER_LOGTAG, "Resolved " + addresses);
logger.dumpToLog();
}
private void addLogSplit(final TimingLogger logger, String host, String message, int depth) {
if (BuildConfig.DEBUG) return;
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < depth; i++) {
sb.append(">");
}
sb.append(" ");
sb.append(host);
sb.append(": ");
sb.append(message);
logger.addSplit(sb.toString());
}
private List<InetAddress> fromSystemHosts(String host) {
try {
return systemHosts.resolve(host);
} catch (IOException e) {
return null;
}
}
@Nullable
private List<InetAddress> fromResolver(String originalHost, String host) throws IOException {
final Resolver resolver = getResolver();
final Record[] records = lookupHostName(resolver, host, true);
final List<InetAddress> addrs = new ArrayList<>(records.length);
for (Record record : records) {
addrs.add(addrFromRecord(originalHost, record));
}
if (addrs.isEmpty()) return null;
return addrs;
}
@Nullable
private List<InetAddress> getFromMapping(final String host) throws UnknownHostException {
return getFromMappingInternal(host, host, false);
}
@Nullable
private List<InetAddress> getFromMappingInternal(String host, String origHost, boolean checkRecursive) throws UnknownHostException {
if (checkRecursive && hostMatches(host, origHost)) {
// Recursive resolution, stop this call
return null;
}
for (final Entry<String, ?> entry : hostMapping.getAll().entrySet()) {
if (hostMatches(host, entry.getKey())) {
final String value = (String) entry.getValue();
final InetAddress resolved = getResolvedIPAddress(origHost, value);
if (resolved == null) {
// Maybe another hostname
return getFromMappingInternal(value, origHost, true);
}
return Collections.singletonList(resolved);
}
}
return null;
}
@NonNull
private Resolver getResolver() throws IOException {
if (mResolver != null) return mResolver;
final boolean tcp = preferences.getBoolean(KEY_TCP_DNS_QUERY, false);
final String address = preferences.getString(KEY_DNS_SERVER, null);
final SimpleResolver resolver;
if (!TextUtils.isEmpty(address) && isValidIpAddress(address)) {
resolver = new SimpleResolver(address);
} else {
resolver = new SimpleResolver();
}
resolver.setTCP(tcp);
return mResolver = resolver;
}
private static boolean hostMatches(final String host, final String rule) {
if (rule == null || host == null) return false;
if (rule.startsWith(".")) return StringUtils.endsWithIgnoreCase(host, rule);
return host.equalsIgnoreCase(rule);
}
@Nullable
private List<InetAddress> fromAddressString(String host, String address)
throws UnknownHostException {
final InetAddress resolved = getResolvedIPAddress(host, address);
if (resolved == null) return null;
return Collections.singletonList(resolved);
}
public static InetAddress getResolvedIPAddress(@NonNull final String host,
@NonNull final String address)
throws UnknownHostException {
byte[] bytes;
bytes = Address.toByteArray(address, Address.IPv4);
if (bytes != null)
return InetAddress.getByAddress(host, bytes);
bytes = Address.toByteArray(address, Address.IPv6);
if (bytes != null)
return InetAddress.getByAddress(host, bytes);
return null;
}
private static int getInetAddressType(@NonNull final String address) {
byte[] bytes;
bytes = Address.toByteArray(address, Address.IPv4);
if (bytes != null)
return Address.IPv4;
bytes = Address.toByteArray(address, Address.IPv6);
if (bytes != null)
return Address.IPv6;
return 0;
}
public static boolean isValidIpAddress(@NonNull final String address) {
return getInetAddressType(address) != 0;
}
private static Record[] lookupHostName(Resolver resolver, String name, boolean all) throws UnknownHostException {
try {
Lookup lookup = newLookup(resolver, name, Type.A);
Record[] a = lookup.run();
if (a == null) {
if (lookup.getResult() == Lookup.TYPE_NOT_FOUND) {
Record[] aaaa = newLookup(resolver, name, Type.AAAA).run();
if (aaaa != null)
return aaaa;
}
throw new UnknownHostException("unknown host");
}
if (!all)
return a;
Record[] aaaa = newLookup(resolver, name, Type.AAAA).run();
if (aaaa == null)
return a;
Record[] merged = new Record[a.length + aaaa.length];
System.arraycopy(a, 0, merged, 0, a.length);
System.arraycopy(aaaa, 0, merged, a.length, aaaa.length);
return merged;
} catch (TextParseException e) {
throw new UnknownHostException("invalid name");
}
}
private static Lookup newLookup(Resolver resolver, String name, int type) throws TextParseException {
final Lookup lookup = new Lookup(name, type);
lookup.setResolver(resolver);
return lookup;
}
private static InetAddress addrFromRecord(String name, Record r) throws UnknownHostException {
InetAddress addr;
if (r instanceof ARecord) {
addr = ((ARecord) r).getAddress();
} else {
addr = ((AAAARecord) r).getAddress();
}
return InetAddress.getByAddress(name, addr.getAddress());
}
}

View File

@ -68,3 +68,22 @@ inline fun <R> Cursor.useCursor(block: (Cursor) -> R): R {
val Cursor.isEmpty: Boolean
get() = count == 0
/**
* @param limit -1 for no limit
* @return Remaining count, -1 if no rows present
*/
inline fun Cursor.forEachRow(limit: Int = -1, action: (cur: Cursor, pos: Int) -> Boolean): Int {
moveToFirst()
var current = 0
while (!isAfterLast) {
@Suppress("ConvertTwoComparisonsToRangeCheck")
if (limit >= 0 && current >= limit) break
if (action(this, current)) {
current++
}
moveToNext()
}
return count - position
}

View File

@ -341,7 +341,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher,
setSignInButton()
if (result.alreadyLoggedIn) {
result.updateAccount(am)
deleteAccountData(contentResolver, result.user.key)
contentResolver.deleteAccountData(result.user.key)
Toast.makeText(this, R.string.message_toast_already_logged_in, Toast.LENGTH_SHORT).show()
} else {
result.addAccount(am, preferences[randomizeAccountNameKey])

View File

@ -381,11 +381,6 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
val status = adapter.getStatus(statusPosition)
IntentUtils.openMedia(activity, status, current, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])
// BEGIN HotMobi
val event = MediaEvent.create(activity, status, current, timelineType,
adapter.mediaPreviewEnabled)
HotMobiLogger.getInstance(activity).log(status.account_key, event)
// END HotMobi
}
override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia,
@ -394,11 +389,6 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
val quotedMedia = status.quoted_media ?: return
IntentUtils.openMedia(activity, status.account_key, status.is_possibly_sensitive, status,
current, quotedMedia, preferences[newDocumentApiKey], preferences[displaySensitiveContentsKey])
// BEGIN HotMobi
val event = MediaEvent.create(activity, status, current, timelineType,
adapter.mediaPreviewEnabled)
HotMobiLogger.getInstance(activity).log(status.account_key, event)
// END HotMobi
}
override fun onItemActionClick(holder: RecyclerView.ViewHolder, id: Int, position: Int) {

View File

@ -220,7 +220,7 @@ class AccountsManagerFragment : BaseFragment(), LoaderManager.LoaderCallbacks<Li
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
val accountKey = account.getAccountKey(am)
deleteAccountData(resolver, accountKey)
resolver.deleteAccountData(accountKey)
am.removeAccountSupport(account)
}
}

View File

@ -308,10 +308,14 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
val status = adapter.getStatus(statusPosition)
IntentUtils.openMedia(activity, status, current, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])
}
val event = MediaEvent.create(activity, status, current, TimelineType.DETAILS,
adapter.mediaPreviewEnabled)
HotMobiLogger.getInstance(activity).log(status.account_key, event)
override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
val status = adapter.getStatus(statusPosition)
val quotedMedia = status.quoted_media ?: return
IntentUtils.openMedia(activity, status.account_key, status.is_possibly_sensitive, status,
current, quotedMedia, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])
}
override fun onGapClick(holder: GapViewHolder, position: Int) {

View File

@ -85,7 +85,7 @@ class ParcelableStatusLoader(
// Delete all deleted status
val cr = context.contentResolver
DataStoreUtils.deleteStatus(cr, accountKey, statusId, null)
deleteActivityStatus(cr, accountKey, statusId, null)
cr.deleteActivityStatus(accountKey, statusId, null)
}
return SingleResponse(e)
}

View File

@ -22,17 +22,14 @@ 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.mariotaku.abstask.library.TaskStarter
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.Tab
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.util.UriExtraUtils
import org.mariotaku.twidere.task.twitter.message.BatchMarkMessageReadTask
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.util.dagger.DependencyHolder
@ -50,39 +47,30 @@ class NotificationReceiver : BroadcastReceiver() {
@NotificationType
val notificationType = uri.getQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE)
val accountKey = uri.getQueryParameter(QUERY_PARAM_ACCOUNT_KEY)?.let(UserKey::valueOf)
val itemId = UriExtraUtils.getExtra(uri, "item_id").toLong(-1)
val itemUserId = UriExtraUtils.getExtra(uri, "item_user_id").toLong(-1)
val itemUserFollowing = UriExtraUtils.getExtra(uri, "item_user_following")?.toBoolean() ?: false
val timestamp = uri.getQueryParameter(QUERY_PARAM_TIMESTAMP)?.toLong() ?: -1
if (CustomTabType.NOTIFICATIONS_TIMELINE == Tab.getTypeAlias(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 = uri.getQueryParameter(QUERY_PARAM_READ_POSITION)
@ReadPositionTag
val tag = getPositionTag(notificationType)
if (tag != null && !TextUtils.isEmpty(paramReadPosition)) {
manager.setPosition(Utils.getReadPositionTagWithAccount(tag, accountKey),
paramReadPosition.toLong(-1))
when (notificationType) {
NotificationType.HOME_TIMELINE -> {
val positionTag = Utils.getReadPositionTagWithAccount(ReadPositionTag.HOME_TIMELINE,
accountKey)
val manager = holder.readStateManager
manager.setPosition(positionTag, paramReadPosition.toLong(-1))
}
NotificationType.INTERACTIONS -> {
val positionTag = Utils.getReadPositionTagWithAccount(ReadPositionTag.ACTIVITIES_ABOUT_ME,
accountKey)
val manager = holder.readStateManager
manager.setPosition(positionTag, paramReadPosition.toLong(-1))
}
}
}
@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
if (accountKey == null) return
val appContext = context.applicationContext
val task = BatchMarkMessageReadTask(appContext, accountKey,
paramReadPosition.toLong(-1))
TaskStarter.execute(task)
}
}
return null
}
}
}
}

View File

@ -83,7 +83,7 @@ class CreateFavoriteTask(
for (uri in DataStoreUtils.STATUSES_URIS) {
resolver.update(uri, values, statusWhere, statusWhereArgs)
}
updateActivityStatus(resolver, accountKey, statusId) { activity ->
resolver.updateActivityStatus(accountKey, statusId) { activity ->
val statusesMatrix = arrayOf(activity.target_statuses, activity.target_object_statuses)
for (statusesArray in statusesMatrix) {
if (statusesArray == null) continue

View File

@ -66,7 +66,7 @@ class DestroyFavoriteTask(
resolver.update(uri, values, where.sql, whereArgs)
}
updateActivityStatus(resolver, accountKey, statusId) { activity ->
resolver.updateActivityStatus(accountKey, statusId) { activity ->
val statusesMatrix = arrayOf(activity.target_statuses, activity.target_object_statuses)
for (statusesArray in statusesMatrix) {
if (statusesArray == null) continue

View File

@ -46,7 +46,7 @@ class DestroyStatusTask(
} finally {
if (deleteStatus) {
DataStoreUtils.deleteStatus(context.contentResolver, accountKey, statusId, status)
deleteActivityStatus(context.contentResolver, accountKey, statusId, status)
context.contentResolver.deleteActivityStatus(accountKey, statusId, status)
}
}
}

View File

@ -68,7 +68,7 @@ class RetweetStatusTask(
for (uri in DataStoreUtils.STATUSES_URIS) {
resolver.update(uri, values, where.sql, whereArgs)
}
updateActivityStatus(resolver, accountKey, statusId) { activity ->
resolver.updateActivityStatus(accountKey, statusId) { activity ->
val statusesMatrix = arrayOf(activity.target_statuses, activity.target_object_statuses)
activity.status_my_retweet_id = result.my_retweet_id
for (statusesArray in statusesMatrix) {

View File

@ -86,8 +86,8 @@ abstract class GetActivitiesTask(
// We should delete old activities has intersection with new items
try {
val activities = getActivities(microBlog, credentials, paging)
val storeResult = storeActivities(cr, loadItemLimit, credentials, noItemsBefore, activities, sinceId,
maxId, false)
val storeResult = storeActivities(cr, loadItemLimit, credentials, noItemsBefore,
activities, sinceId, maxId, false)
if (saveReadPosition) {
saveReadPosition(accountKey, credentials, microBlog)
}

View File

@ -106,8 +106,8 @@ abstract class GetStatusesTask(
sinceId = null
}
val statuses = getStatuses(microBlog, paging)
val storeResult = storeStatus(accountKey, details, statuses, sinceId, maxId, sinceSortId,
maxSortId, loadItemLimit, false)
val storeResult = storeStatus(accountKey, details, statuses, sinceId, maxId,
sinceSortId, maxSortId, loadItemLimit, false)
// TODO cache related data and preload
val cacheTask = CacheUsersStatusesTask(context, accountKey, details.type, statuses)
TaskStarter.execute(cacheTask)

View File

@ -0,0 +1,76 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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.task.twitter.message
import android.accounts.AccountManager
import android.content.Context
import org.mariotaku.ktextension.forEachRow
import org.mariotaku.ktextension.useCursor
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.task.ExceptionHandlingAbstractTask
import org.mariotaku.twidere.util.TwidereQueryBuilder
import org.mariotaku.twidere.util.getUnreadMessagesEntriesCursor
/**
* Created by mariotaku on 2017/2/16.
*/
class BatchMarkMessageReadTask(
context: Context,
val accountKey: UserKey,
val markTimestampBefore: Long
) : ExceptionHandlingAbstractTask<Unit?, Boolean, MicroBlogException, Unit?>(context) {
override val exceptionClass = MicroBlogException::class.java
override fun onExecute(params: Unit?): Boolean {
val cr = context.contentResolver
val projection = Conversations.COLUMNS.map {
TwidereQueryBuilder.mapConversationsProjection(it)
}.toTypedArray()
val cur = cr.getUnreadMessagesEntriesCursor(projection, arrayOf(accountKey),
markTimestampBefore) ?: return false
val account = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true) ?:
throw MicroBlogException("No account")
val microBlog = account.newMicroBlogInstance(context, cls = MicroBlog::class.java)
cur.useCursor {
val indices = ObjectCursor.indicesFrom(cur, ParcelableMessageConversation::class.java)
cur.forEachRow { cur, _ ->
val conversation = indices.newObject(cur)
try {
MarkMessageReadTask.performMarkRead(context, microBlog, account, conversation)
return@forEachRow true
} catch (e: MicroBlogException) {
return@forEachRow false
}
}
}
return true
}
}

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.task.twitter.message
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import org.mariotaku.library.objectcursor.ObjectCursor
@ -64,7 +65,7 @@ class MarkMessageReadTask(
val microBlog = account.newMicroBlogInstance(context, cls = MicroBlog::class.java)
val conversation = DataStoreUtils.findMessageConversation(context, accountKey, conversationId)
val lastReadEvent = conversation?.let {
return@let performMarkRead(microBlog, account, conversation)
return@let performMarkRead(context, microBlog, account, conversation)
} ?: return false
val values = ContentValues()
values.put(Conversations.LAST_READ_ID, lastReadEvent.first)
@ -82,13 +83,18 @@ class MarkMessageReadTask(
bus.post(UnreadCountUpdatedEvent(-1))
}
private fun performMarkRead(microBlog: MicroBlog, account: AccountDetails,
companion object {
@Throws(MicroBlogException::class)
internal fun performMarkRead(context: Context, microBlog: MicroBlog, account: AccountDetails,
conversation: ParcelableMessageConversation): Pair<String, Long>? {
val cr = context.contentResolver
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context)) {
val event = (conversation.conversation_extras as? TwitterOfficialConversationExtras)?.maxReadEvent ?: run {
val message = findRecentMessage(accountKey, conversationId) ?: return null
val message = cr.findRecentMessage(account.key, conversation.id) ?: return null
return@run Pair(message.id, message.timestamp)
}
if (conversation.last_read_timestamp > event.second) {
@ -101,17 +107,17 @@ class MarkMessageReadTask(
}
}
}
val message = findRecentMessage(accountKey, conversationId) ?: return null
val message = cr.findRecentMessage(account.key, conversation.id) ?: return null
return Pair(message.id, message.timestamp)
}
private fun findRecentMessage(accountKey: UserKey, conversationId: String): ParcelableMessage? {
private fun ContentResolver.findRecentMessage(accountKey: UserKey, conversationId: String): ParcelableMessage? {
val where = Expression.and(Expression.equalsArgs(Messages.ACCOUNT_KEY),
Expression.equalsArgs(Messages.CONVERSATION_ID)).sql
val whereArgs = arrayOf(accountKey.toString(), conversationId)
@SuppressLint("Recycle")
val cur = context.contentResolver.query(Messages.CONTENT_URI, Messages.COLUMNS,
val cur = query(Messages.CONTENT_URI, Messages.COLUMNS,
where, whereArgs, OrderBy(Messages.LOCAL_TIMESTAMP, false).sql) ?: return null
try {
if (cur.moveToFirst()) {
@ -131,4 +137,5 @@ class MarkMessageReadTask(
return Pair(id, maxEntryTimestamp)
}
}
}

View File

@ -24,11 +24,11 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.database.Cursor
import android.media.AudioManager
import android.net.Uri
import android.support.v4.app.NotificationCompat
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.forEachRow
import org.mariotaku.ktextension.isEmpty
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.twitter.model.Activity
@ -80,10 +80,10 @@ class ContentNotificationManager(
val accountKey = pref.accountKey
val resources = context.resources
val selection = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.greaterThan(Statuses.POSITION_KEY, minPositionKey))
Expression.greaterThanArgs(Statuses.POSITION_KEY))
val selectionArgs = arrayOf(accountKey.toString(), minPositionKey.toString())
val filteredSelection = buildStatusFilterWhereClause(preferences, Statuses.TABLE_NAME,
selection)
val selectionArgs = arrayOf(accountKey.toString())
val userProjection = arrayOf(Statuses.USER_KEY, Statuses.USER_NAME, Statuses.USER_SCREEN_NAME)
val statusProjection = arrayOf(Statuses.POSITION_KEY)
@ -105,7 +105,11 @@ class ContentNotificationManager(
if (statusesCount == 0 || usersCount == 0) return
val statusIndices = ObjectCursor.indicesFrom(statusCursor, ParcelableStatus::class.java)
val userIndices = ObjectCursor.indicesFrom(userCursor, ParcelableStatus::class.java)
val positionKey = if (statusCursor.moveToFirst()) statusCursor.getLong(statusIndices[Statuses.POSITION_KEY]) else -1L
val positionKey = if (statusCursor.moveToFirst()) {
statusCursor.getLong(statusIndices[Statuses.POSITION_KEY])
} else {
-1L
}
val notificationTitle = resources.getQuantityString(R.plurals.N_new_statuses,
statusesCount, statusesCount)
val notificationContent: String
@ -184,12 +188,17 @@ class ContentNotificationManager(
style.setSummaryText(accountName)
val ci = ObjectCursor.indicesFrom(c, ParcelableActivity::class.java)
var timestamp: Long = -1
var timestamp = -1L
var newMaxPositionKey = -1L
val filteredUserIds = DataStoreUtils.getFilteredUserIds(context)
var consumed = 0
val remaining = c.forEachRow(5) { cur, _ ->
val activity = ci.newObject(cur)
if (newMaxPositionKey == -1L) {
newMaxPositionKey = activity.position_key
}
if (pref.isNotificationMentionsOnly && activity.action !in Activity.Action.MENTION_ACTIONS) {
return@forEachRow false
}
@ -239,6 +248,8 @@ class ContentNotificationManager(
builder.setNumber(displayCount)
builder.setContentIntent(getContentIntent(context, CustomTabType.NOTIFICATIONS_TIMELINE,
NotificationType.INTERACTIONS, accountKey, timestamp))
builder.setDeleteIntent(getMarkReadDeleteIntent(context, NotificationType.INTERACTIONS,
accountKey, newMaxPositionKey, false))
if (timestamp != -1L) {
builder.setDeleteIntent(getMarkReadDeleteIntent(context,
NotificationType.INTERACTIONS, accountKey, timestamp, false))
@ -267,7 +278,11 @@ class ContentNotificationManager(
val indices = ObjectCursor.indicesFrom(cur, ParcelableMessageConversation::class.java)
var messageSum: Int = 0
var newLastReadTimestamp = -1L
cur.forEachRow { cur, _ ->
if (newLastReadTimestamp != -1L) {
newLastReadTimestamp = cur.getLong(indices[Conversations.LAST_READ_TIMESTAMP])
}
messageSum += cur.getInt(indices[Conversations.UNREAD_COUNT])
return@forEachRow true
}
@ -286,6 +301,9 @@ class ContentNotificationManager(
builder.setContentTitle(notificationTitle)
builder.setContentIntent(getContentIntent(context, CustomTabType.DIRECT_MESSAGES,
NotificationType.DIRECT_MESSAGES, accountKey, 0))
builder.setDeleteIntent(getMarkReadDeleteIntent(context, NotificationType.DIRECT_MESSAGES,
accountKey, newLastReadTimestamp, false))
val remaining = cur.forEachRow(5) { cur, pos ->
val conversation = indices.newObject(cur)
if (conversation.notificationDisabled) return@forEachRow false
@ -311,22 +329,35 @@ class ContentNotificationManager(
}
/**
* @param limit -1 for no limit
* @return Remaining count, -1 if no rows present
*/
private inline fun Cursor.forEachRow(limit: Int = -1, action: (cur: Cursor, pos: Int) -> Boolean): Int {
moveToFirst()
var current = 0
while (!isAfterLast) {
@Suppress("ConvertTwoComparisonsToRangeCheck")
if (limit >= 0 && current >= limit) break
if (action(this, current)) {
current++
fun showUserNotification(accountKey: UserKey, status: Status, userKey: UserKey) {
// Build favorited user notifications
val userDisplayName = userColorNameManager.getDisplayName(status.user,
preferences[nameFirstKey])
val statusUri = LinkCreator.getTwidereStatusLink(accountKey, status.id)
val builder = NotificationCompat.Builder(context)
builder.color = userColorNameManager.getUserColor(userKey)
builder.setAutoCancel(true)
builder.setWhen(status.createdAt?.time ?: 0)
builder.setSmallIcon(R.drawable.ic_stat_twitter)
builder.setCategory(NotificationCompat.CATEGORY_SOCIAL)
if (status.isRetweetedByMe) {
builder.setContentTitle(context.getString(R.string.notification_title_new_retweet_by_user, userDisplayName))
builder.setContentText(InternalTwitterContentUtils.formatStatusTextWithIndices(status.retweetedStatus).text)
} else {
builder.setContentTitle(context.getString(R.string.notification_title_new_status_by_user, userDisplayName))
builder.setContentText(InternalTwitterContentUtils.formatStatusTextWithIndices(status).text)
}
moveToNext()
builder.setContentIntent(PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, statusUri).apply {
setClass(context, LinkHandlerActivity::class.java)
}, PendingIntent.FLAG_UPDATE_CURRENT))
val tag = "$accountKey:$userKey:${status.id}"
notificationManager.notify(tag, NOTIFICATION_ID_USER_NOTIFICATION, builder.build())
}
return count - position
fun updatePreferences() {
nameFirst = preferences[nameFirstKey]
useStarForLikes = preferences[iWantMyStarsBackKey]
}
private fun applyNotificationPreferences(builder: NotificationCompat.Builder, pref: AccountPreferences, defaultFlags: Int) {
@ -355,7 +386,6 @@ class ContentNotificationManager(
return !activityTracker.isHomeActivityStarted
}
private fun getContentIntent(context: Context, @CustomTabType type: String,
@NotificationType notificationType: String, accountKey: UserKey?, readPosition: Long): PendingIntent {
// Setup click intent
@ -376,65 +406,28 @@ class ContentNotificationManager(
return PendingIntent.getActivity(context, 0, homeIntent, 0)
}
fun updatePreferences() {
nameFirst = preferences[nameFirstKey]
useStarForLikes = preferences[iWantMyStarsBackKey]
}
private fun getMarkReadDeleteIntent(context: Context, @NotificationType type: String,
accountKey: UserKey?, position: Long,
extraUserFollowing: Boolean): PendingIntent {
return getMarkReadDeleteIntent(context, type, accountKey, position, -1, -1, extraUserFollowing)
return getMarkReadDeleteIntent(context, type, accountKey, position)
}
private fun getMarkReadDeleteIntent(context: Context, @NotificationType type: String,
accountKey: UserKey?, position: Long,
extraId: Long, extraUserId: Long,
extraUserFollowing: Boolean): PendingIntent {
accountKey: UserKey?, position: Long): PendingIntent {
// Setup delete intent
val intent = Intent(context, NotificationReceiver::class.java)
intent.action = IntentConstants.BROADCAST_NOTIFICATION_DELETED
val linkBuilder = Uri.Builder()
linkBuilder.scheme(SCHEME_TWIDERE)
linkBuilder.authority(AUTHORITY_INTERACTIONS)
linkBuilder.authority(AUTHORITY_NOTIFICATIONS)
linkBuilder.appendPath(type)
if (accountKey != null) {
linkBuilder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString())
}
linkBuilder.appendQueryParameter(QUERY_PARAM_READ_POSITION, position.toString())
linkBuilder.appendQueryParameter(QUERY_PARAM_TIMESTAMP, System.currentTimeMillis().toString())
linkBuilder.appendQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE, type)
UriExtraUtils.addExtra(linkBuilder, "item_id", extraId)
UriExtraUtils.addExtra(linkBuilder, "item_user_id", extraUserId)
UriExtraUtils.addExtra(linkBuilder, "item_user_following", extraUserFollowing)
intent.data = linkBuilder.build()
return PendingIntent.getBroadcast(context, 0, intent, 0)
}
fun showUserNotification(accountKey: UserKey, status: Status, userKey: UserKey) {
// Build favorited user notifications
val userDisplayName = userColorNameManager.getDisplayName(status.user,
preferences[nameFirstKey])
val statusUri = LinkCreator.getTwidereStatusLink(accountKey, status.id)
val builder = NotificationCompat.Builder(context)
builder.color = userColorNameManager.getUserColor(userKey)
builder.setAutoCancel(true)
builder.setWhen(status.createdAt?.time ?: 0)
builder.setSmallIcon(R.drawable.ic_stat_twitter)
builder.setCategory(NotificationCompat.CATEGORY_SOCIAL)
if (status.isRetweetedByMe) {
builder.setContentTitle(context.getString(R.string.notification_title_new_retweet_by_user, userDisplayName))
builder.setContentText(InternalTwitterContentUtils.formatStatusTextWithIndices(status.retweetedStatus).text)
} else {
builder.setContentTitle(context.getString(R.string.notification_title_new_status_by_user, userDisplayName))
builder.setContentText(InternalTwitterContentUtils.formatStatusTextWithIndices(status).text)
}
builder.setContentIntent(PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, statusUri).apply {
setClass(context, LinkHandlerActivity::class.java)
}, PendingIntent.FLAG_UPDATE_CURRENT))
val tag = "$accountKey:$userKey:${status.id}"
notificationManager.notify(tag, NOTIFICATION_ID_USER_NOTIFICATION, builder.build())
}
}

View File

@ -32,8 +32,7 @@ import java.io.IOException
* Created by mariotaku on 2016/12/24.
*/
fun buildStatusFilterWhereClause(preferences: SharedPreferences,
table: String,
fun buildStatusFilterWhereClause(preferences: SharedPreferences, table: String,
extraSelection: Expression?): Expression {
val filteredUsersQuery = SQLQueryBuilder
.select(Column(Table(Filters.Users.TABLE_NAME), Filters.Users.USER_KEY))
@ -116,20 +115,20 @@ fun deleteDrafts(context: Context, draftIds: LongArray): Int {
return context.contentResolver.delete(Drafts.CONTENT_URI, where, whereArgs)
}
fun deleteAccountData(resolver: ContentResolver, accountKey: UserKey) {
fun ContentResolver.deleteAccountData(accountKey: UserKey) {
val where = Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY).sql
val whereArgs = arrayOf(accountKey.toString())
// Also delete tweets related to the account we previously
// deleted.
resolver.delete(Statuses.CONTENT_URI, where, whereArgs)
resolver.delete(Activities.AboutMe.CONTENT_URI, where, whereArgs)
resolver.delete(Messages.CONTENT_URI, where, whereArgs)
resolver.delete(Messages.Conversations.CONTENT_URI, where, whereArgs)
delete(Statuses.CONTENT_URI, where, whereArgs)
delete(Activities.AboutMe.CONTENT_URI, where, whereArgs)
delete(Messages.CONTENT_URI, where, whereArgs)
delete(Conversations.CONTENT_URI, where, whereArgs)
}
fun deleteActivityStatus(cr: ContentResolver, accountKey: UserKey,
statusId: String, result: ParcelableStatus?) {
fun ContentResolver.deleteActivityStatus(accountKey: UserKey, statusId: String,
result: ParcelableStatus?) {
val host = accountKey.host
val deleteWhere: String
@ -159,8 +158,8 @@ fun deleteActivityStatus(cr: ContentResolver, accountKey: UserKey,
updateWhereArgs = arrayOf(statusId)
}
for (uri in ACTIVITIES_URIS) {
cr.delete(uri, deleteWhere, deleteWhereArgs)
updateActivity(cr, uri, updateWhere, updateWhereArgs) { activity ->
delete(uri, deleteWhere, deleteWhereArgs)
updateActivity(uri, updateWhere, updateWhereArgs) { activity ->
activity.status_my_retweet_id = null
arrayOf(activity.target_statuses, activity.target_object_statuses).filterNotNull().forEach {
for (status in it) {
@ -178,9 +177,7 @@ fun deleteActivityStatus(cr: ContentResolver, accountKey: UserKey,
}
}
fun updateActivityStatus(resolver: ContentResolver,
accountKey: UserKey,
statusId: String,
fun ContentResolver.updateActivityStatus(accountKey: UserKey, statusId: String,
action: (ParcelableActivity) -> Unit) {
val activityWhere = Expression.and(
Expression.equalsArgs(Activities.ACCOUNT_KEY),
@ -191,16 +188,15 @@ fun updateActivityStatus(resolver: ContentResolver,
).sql
val activityWhereArgs = arrayOf(accountKey.toString(), statusId, statusId)
for (uri in ACTIVITIES_URIS) {
updateActivity(resolver, uri, activityWhere, activityWhereArgs, action)
updateActivity(uri, activityWhere, activityWhereArgs, action)
}
}
@WorkerThread
fun updateActivity(cr: ContentResolver, uri: Uri,
where: String?, whereArgs: Array<String>?,
action: (ParcelableActivity) -> Unit) {
val c = cr.query(uri, Activities.COLUMNS, where, whereArgs, null) ?: return
fun ContentResolver.updateActivity(uri: Uri, where: String?,
whereArgs: Array<String>?, action: (ParcelableActivity) -> Unit) {
val c = query(uri, Activities.COLUMNS, where, whereArgs, null) ?: return
val values = LongSparseArray<ContentValues>()
try {
val ci = ObjectCursor.indicesFrom(c, ParcelableActivity::class.java)
@ -221,11 +217,12 @@ fun updateActivity(cr: ContentResolver, uri: Uri,
val updateWhereArgs = arrayOfNulls<String>(1)
for (i in 0 until values.size()) {
updateWhereArgs[0] = values.keyAt(i).toString()
cr.update(uri, values.valueAt(i), updateWhere, updateWhereArgs)
update(uri, values.valueAt(i), updateWhere, updateWhereArgs)
}
}
fun ContentResolver.getUnreadMessagesEntriesCursor(projection: Array<Columns.Column>, accountKeys: Array<UserKey>): Cursor? {
fun ContentResolver.getUnreadMessagesEntriesCursor(projection: Array<Columns.Column>,
accountKeys: Array<UserKey>, timestampBefore: Long = -1): Cursor? {
val qb = SQLQueryBuilder.select(Columns(*projection))
qb.from(Table(Conversations.TABLE_NAME))
qb.join(Join(false, Join.Operation.LEFT_OUTER, Table(Messages.TABLE_NAME),
@ -234,15 +231,27 @@ fun ContentResolver.getUnreadMessagesEntriesCursor(projection: Array<Columns.Col
Column(Table(Messages.TABLE_NAME), Messages.CONVERSATION_ID)
)
))
qb.where(Expression.and(
val whereConditions = arrayOf(
Expression.inArgs(Column(Table(Conversations.TABLE_NAME), Conversations.ACCOUNT_KEY),
accountKeys.size),
Expression.lesserThan(Column(Table(Conversations.TABLE_NAME), Conversations.LAST_READ_TIMESTAMP),
Column(Table(Conversations.TABLE_NAME), Conversations.LOCAL_TIMESTAMP))
))
)
if (timestampBefore >= 0) {
val beforeCondition = Expression.greaterThan(Column(Table(Conversations.TABLE_NAME),
Conversations.LAST_READ_TIMESTAMP), RawSQLLang("?"))
qb.where(Expression.and(*(whereConditions + beforeCondition)))
} else {
qb.where(Expression.and(*whereConditions))
}
qb.groupBy(Column(Table(Messages.TABLE_NAME), Messages.CONVERSATION_ID))
qb.orderBy(OrderBy(arrayOf(Column(Table(Conversations.TABLE_NAME), Conversations.LOCAL_TIMESTAMP),
Column(Table(Conversations.TABLE_NAME), Conversations.SORT_ID)), booleanArrayOf(false, false)))
val selectionArgs = accountKeys.toStringArray()
val selectionArgs = if (timestampBefore >= 0) {
accountKeys.toStringArray() + timestampBefore.toString()
} else {
accountKeys.toStringArray()
}
return rawQuery(qb.buildSQL(), selectionArgs)
}

View File

@ -0,0 +1,309 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.net
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.util.TimingLogger
import okhttp3.Dns
import org.apache.commons.lang3.StringUtils
import org.mariotaku.ktextension.toInt
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.TwidereConstants.HOST_MAPPING_PREFERENCES_NAME
import org.mariotaku.twidere.constant.SharedPreferenceConstants.*
import org.mariotaku.twidere.util.SharedPreferencesWrapper
import org.xbill.DNS.*
import java.io.IOException
import java.net.InetAddress
import java.net.UnknownHostException
import java.util.*
import javax.inject.Singleton
@Singleton
class TwidereDns(context: Context, private val preferences: SharedPreferences) : Dns {
private val hostMapping: SharedPreferences
private val systemHosts: SystemHosts
private var resolver: Resolver? = null
private var useResolver: Boolean = false
init {
hostMapping = SharedPreferencesWrapper.getInstance(context, HOST_MAPPING_PREFERENCES_NAME, Context.MODE_PRIVATE)
systemHosts = SystemHosts()
reloadDnsSettings()
}
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
try {
return resolveInternal(hostname, hostname, 0, useResolver)
} catch (e: IOException) {
if (e is UnknownHostException) throw e
throw UnknownHostException("Unable to resolve address " + e.message)
} catch (e: SecurityException) {
throw UnknownHostException("Security exception" + e.message)
}
}
@Throws(UnknownHostException::class)
fun lookupResolver(hostname: String): List<InetAddress> {
try {
return resolveInternal(hostname, hostname, 0, true)
} catch (e: IOException) {
if (e is UnknownHostException) throw e
throw UnknownHostException("Unable to resolve address " + e.message)
} catch (e: SecurityException) {
throw UnknownHostException("Security exception" + e.message)
}
}
fun reloadDnsSettings() {
this.resolver = null
useResolver = preferences.getBoolean(KEY_BUILTIN_DNS_RESOLVER, false)
}
@Throws(IOException::class, SecurityException::class)
private fun resolveInternal(originalHost: String, host: String, depth: Int,
useResolver: Boolean): List<InetAddress> {
val logger = TimingLogger(RESOLVER_LOGTAG, "resolve")
// Return if host is an address
val fromAddressString = fromAddressString(originalHost, host)
if (fromAddressString != null) {
addLogSplit(logger, host, "valid ip address", depth)
dumpLog(logger, fromAddressString)
return fromAddressString
}
// Load from custom mapping
addLogSplit(logger, host, "start custom mapping resolve", depth)
val fromMapping = getFromMapping(host)
addLogSplit(logger, host, "end custom mapping resolve", depth)
if (fromMapping != null) {
dumpLog(logger, fromMapping)
return fromMapping
}
if (useResolver) {
// Load from /etc/hosts, since Dnsjava doesn't support hosts entry lookup
addLogSplit(logger, host, "start /etc/hosts resolve", depth)
val fromSystemHosts = fromSystemHosts(host)
addLogSplit(logger, host, "end /etc/hosts resolve", depth)
if (fromSystemHosts != null) {
dumpLog(logger, fromSystemHosts)
return fromSystemHosts
}
// Use DNS resolver
addLogSplit(logger, host, "start resolver resolve", depth)
val fromResolver = fromResolver(originalHost, host)
addLogSplit(logger, host, "end resolver resolve", depth)
if (fromResolver != null) {
dumpLog(logger, fromResolver)
return fromResolver
}
}
addLogSplit(logger, host, "start system default resolve", depth)
val fromDefault = Arrays.asList(*InetAddress.getAllByName(host))
addLogSplit(logger, host, "end system default resolve", depth)
dumpLog(logger, fromDefault)
return fromDefault
}
private fun dumpLog(logger: TimingLogger, addresses: List<InetAddress>) {
if (BuildConfig.DEBUG) return
Log.v(RESOLVER_LOGTAG, "Resolved " + addresses)
logger.dumpToLog()
}
private fun addLogSplit(logger: TimingLogger, host: String, message: String, depth: Int) {
if (BuildConfig.DEBUG) return
val sb = StringBuilder()
for (i in 0..depth - 1) {
sb.append(">")
}
sb.append(" ")
sb.append(host)
sb.append(": ")
sb.append(message)
logger.addSplit(sb.toString())
}
private fun fromSystemHosts(host: String): List<InetAddress>? {
try {
return systemHosts.resolve(host)
} catch (e: IOException) {
return null
}
}
@Throws(IOException::class)
private fun fromResolver(originalHost: String, host: String): List<InetAddress>? {
val resolver = this.getResolver()
val records = lookupHostName(resolver, host, true)
val addrs = ArrayList<InetAddress>(records.size)
for (record in records) {
addrs.add(addrFromRecord(originalHost, record))
}
if (addrs.isEmpty()) return null
return addrs
}
@Throws(UnknownHostException::class)
private fun getFromMapping(host: String): List<InetAddress>? {
return getFromMappingInternal(host, host, false)
}
@Throws(UnknownHostException::class)
private fun getFromMappingInternal(host: String, origHost: String, checkRecursive: Boolean): List<InetAddress>? {
if (checkRecursive && hostMatches(host, origHost)) {
// Recursive resolution, stop this call
return null
}
for ((key, value1) in hostMapping.all) {
if (hostMatches(host, key)) {
val value = value1 as String
val resolved = getResolvedIPAddress(origHost, value) ?: // Maybe another hostname
return getFromMappingInternal(value, origHost, true)
return listOf(resolved)
}
}
return null
}
private fun getResolver(): Resolver {
return this.resolver ?: run {
val tcp = preferences.getBoolean(KEY_TCP_DNS_QUERY, false)
val resolvers = preferences.getString(KEY_DNS_SERVER, null)?.split(';', ',', ' ')?.mapNotNull {
val segs = it.split("#", limit = 2)
if (segs.isEmpty()) return@mapNotNull null
if (!isValidIpAddress(segs[0])) return@mapNotNull null
return@mapNotNull SimpleResolver(segs[0]).apply {
if (segs.size == 2) {
val port = segs[1].toInt(-1)
if (port in 0..65535) {
setPort(port)
}
}
}
}
val resolver: Resolver
if (resolvers != null && resolvers.isNotEmpty()) {
resolver = ExtendedResolver(resolvers.toTypedArray())
} else {
resolver = SimpleResolver()
}
resolver.setTCP(tcp)
this.resolver = resolver
return@run resolver
}
}
@Throws(UnknownHostException::class)
private fun fromAddressString(host: String, address: String): List<InetAddress>? {
val resolved = getResolvedIPAddress(host, address) ?: return null
return listOf(resolved)
}
companion object {
private val RESOLVER_LOGTAG = "TwidereDns"
private fun hostMatches(host: String?, rule: String?): Boolean {
if (rule == null || host == null) return false
if (rule.startsWith(".")) return StringUtils.endsWithIgnoreCase(host, rule)
return host.equals(rule, ignoreCase = true)
}
@Throws(UnknownHostException::class)
fun getResolvedIPAddress(host: String,
address: String): InetAddress? {
var bytes = Address.toByteArray(address, Address.IPv4)
if (bytes != null)
return InetAddress.getByAddress(host, bytes)
bytes = Address.toByteArray(address, Address.IPv6)
if (bytes != null)
return InetAddress.getByAddress(host, bytes)
return null
}
private fun getInetAddressType(address: String): Int {
var bytes = Address.toByteArray(address, Address.IPv4)
if (bytes != null)
return Address.IPv4
bytes = Address.toByteArray(address, Address.IPv6)
if (bytes != null)
return Address.IPv6
return 0
}
fun isValidIpAddress(address: String): Boolean {
return getInetAddressType(address) != 0
}
@Throws(UnknownHostException::class)
private fun lookupHostName(resolver: Resolver, name: String, all: Boolean): Array<Record> {
try {
val lookup = newLookup(resolver, name, Type.A)
val a = lookup.run()
if (a == null) {
if (lookup.result == Lookup.TYPE_NOT_FOUND) {
val aaaa = newLookup(resolver, name, Type.AAAA).run()
if (aaaa != null)
return aaaa
}
throw UnknownHostException("unknown host")
}
if (!all)
return a
val aaaa = newLookup(resolver, name, Type.AAAA).run() ?: return a
return a + aaaa
} catch (e: TextParseException) {
throw UnknownHostException("invalid name")
}
}
@Throws(TextParseException::class)
private fun newLookup(resolver: Resolver, name: String, type: Int): Lookup {
val lookup = Lookup(name, type)
lookup.setResolver(resolver)
return lookup
}
@Throws(UnknownHostException::class)
private fun addrFromRecord(name: String, r: Record): InetAddress {
val addr: InetAddress
if (r is ARecord) {
addr = r.address
} else {
addr = (r as AAAARecord).address
}
return InetAddress.getByAddress(name, addr.address)
}
}
}