SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java

713 lines
24 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jp.juggler.subwaytooter.table;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.provider.BaseColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootInstance;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
public class SavedAccount extends TootAccount implements LinkClickContext {
private static final LogCategory log = new LogCategory( "SavedAccount" );
public static final String table = "access_info";
private static final String COL_ID = BaseColumns._ID;
private static final String COL_HOST = "h";
private static final String COL_USER = "u";
private static final String COL_ACCOUNT = "a";
private static final String COL_TOKEN = "t";
private static final String COL_VISIBILITY = "visibility";
private static final String COL_CONFIRM_BOOST = "confirm_boost";
private static final String COL_DONT_HIDE_NSFW = "dont_hide_nsfw";
// スキーマ2から
private static final String COL_NOTIFICATION_MENTION = "notification_mention";
private static final String COL_NOTIFICATION_BOOST = "notification_boost";
private static final String COL_NOTIFICATION_FAVOURITE = "notification_favourite";
private static final String COL_NOTIFICATION_FOLLOW = "notification_follow";
// スキーマ10から
private static final String COL_CONFIRM_FOLLOW = "confirm_follow";
private static final String COL_CONFIRM_FOLLOW_LOCKED = "confirm_follow_locked";
private static final String COL_CONFIRM_UNFOLLOW = "confirm_unfollow";
private static final String COL_CONFIRM_POST = "confirm_post";
// スキーマ13から
public static final String COL_NOTIFICATION_TAG = "notification_server";
// スキーマ14から
public static final String COL_REGISTER_KEY = "register_key";
public static final String COL_REGISTER_TIME = "register_time";
// スキーマ16から
private static final String COL_SOUND_URI = "sound_uri";
// スキーマ18から
private static final String COL_DONT_SHOW_TIMEOUT = "dont_show_timeout";
/////////////////////////////////
// login information
public static final long INVALID_ID = - 1L;
public long db_id = INVALID_ID;
public String host;
public String acct; // user@host
public JSONObject token_info;
public String visibility;
public boolean confirm_boost;
public boolean dont_hide_nsfw;
public boolean dont_show_timeout;
public boolean notification_mention;
public boolean notification_boost;
public boolean notification_favourite;
public boolean notification_follow;
@NonNull public String sound_uri = "";
public boolean confirm_follow;
public boolean confirm_follow_locked;
public boolean confirm_unfollow;
public boolean confirm_post;
public String notification_tag;
public String register_key;
public long register_time;
private final AtomicReference< TootInstance > refInstance = new AtomicReference<>( null );
private static final long INSTANCE_INFORMATION_EXPIRE = 60000L * 5;
// DBには保存しない
public @Nullable TootInstance getInstance(){
TootInstance instance = refInstance.get();
if( instance != null && System.currentTimeMillis() - instance.time_parse > INSTANCE_INFORMATION_EXPIRE ){
return null;
}
return instance;
}
public void setInstance( @NonNull TootInstance instance ){
refInstance.set( instance );
}
// アプリデータのインポート時に呼ばれる
public static void onDBDelete( SQLiteDatabase db ){
try{
db.execSQL( "drop table if exists " + table );
}catch( Throwable ex ){
log.trace( ex );
}
}
public static void onDBCreate( SQLiteDatabase db ){
db.execSQL(
"create table if not exists " + table
+ "(_id INTEGER PRIMARY KEY"
+ ",u text not null"
+ ",h text not null"
+ ",a text not null"
+ ",t text not null"
+ ",visibility text"
+ ",confirm_boost integer default 1"
+ ",dont_hide_nsfw integer default 0"
// 以下はDBスキーマ2で追加
+ ",notification_mention integer default 1"
+ ",notification_boost integer default 1"
+ ",notification_favourite integer default 1"
+ ",notification_follow integer default 1"
// 以下はDBスキーマ10で更新
+ "," + COL_CONFIRM_FOLLOW + " integer default 1"
+ "," + COL_CONFIRM_FOLLOW_LOCKED + " integer default 1"
+ "," + COL_CONFIRM_UNFOLLOW + " integer default 1"
+ "," + COL_CONFIRM_POST + " integer default 1"
// 以下はDBスキーマ13で更新
+ "," + COL_NOTIFICATION_TAG + " text default ''"
// 以下はDBスキーマ14で更新
+ "," + COL_REGISTER_KEY + " text default ''"
+ "," + COL_REGISTER_TIME + " integer default 0"
// 以下はDBスキーマ16で更新
+ "," + COL_SOUND_URI + " text default ''"
// 以下はDBスキーマ18で更新
+ "," + COL_DONT_SHOW_TIMEOUT + " integer default 0"
+ ")"
);
db.execSQL( "create index if not exists " + table + "_user on " + table + "(u)" );
db.execSQL( "create index if not exists " + table + "_host on " + table + "(h,u)" );
}
public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
if( oldVersion < 2 && newVersion >= 2 ){
try{
db.execSQL( "alter table " + table + " add column notification_mention integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
try{
db.execSQL( "alter table " + table + " add column notification_boost integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
try{
db.execSQL( "alter table " + table + " add column notification_favourite integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
try{
db.execSQL( "alter table " + table + " add column notification_follow integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
}
if( oldVersion < 10 && newVersion >= 10 ){
try{
db.execSQL( "alter table " + table + " add column " + COL_CONFIRM_FOLLOW + " integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
try{
db.execSQL( "alter table " + table + " add column " + COL_CONFIRM_FOLLOW_LOCKED + " integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
try{
db.execSQL( "alter table " + table + " add column " + COL_CONFIRM_UNFOLLOW + " integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
try{
db.execSQL( "alter table " + table + " add column " + COL_CONFIRM_POST + " integer default 1" );
}catch( Throwable ex ){
log.trace( ex );
}
}
if( oldVersion < 13 && newVersion >= 13 ){
try{
db.execSQL( "alter table " + table + " add column " + COL_NOTIFICATION_TAG + " text default ''" );
}catch( Throwable ex ){
log.trace( ex );
}
}
if( oldVersion < 14 && newVersion >= 14 ){
try{
db.execSQL( "alter table " + table + " add column " + COL_REGISTER_KEY + " text default ''" );
}catch( Throwable ex ){
log.trace( ex );
}
try{
db.execSQL( "alter table " + table + " add column " + COL_REGISTER_TIME + " integer default 0" );
}catch( Throwable ex ){
log.trace( ex );
}
}
if( oldVersion < 16 && newVersion >= 16 ){
try{
db.execSQL( "alter table " + table + " add column " + COL_SOUND_URI + " text default ''" );
}catch( Throwable ex ){
log.trace( ex );
}
}
if( oldVersion < 18 && newVersion >= 18 ){
try{
db.execSQL( "alter table " + table + " add column " + COL_DONT_SHOW_TIMEOUT + " integer default 0" );
}catch( Throwable ex ){
log.trace( ex );
}
}
}
private SavedAccount(){
}
// 横断検索用の、何とも紐ついていないアカウント
// 保存しない。
private static SavedAccount na_account;
public static SavedAccount getNA(){
if( na_account == null ){
SavedAccount dst = new SavedAccount();
dst.db_id = - 1L;
dst.username = "?";
dst.acct = "?@?";
dst.host = "?";
dst.notification_follow = false;
dst.notification_favourite = false;
dst.notification_boost = false;
dst.notification_mention = false;
na_account = dst;
}
return na_account;
}
public boolean isNA(){
return acct.equals( "?@?" );
}
private static @Nullable
SavedAccount parse( Context context, Cursor cursor ) throws JSONException{
JSONObject src = new JSONObject( cursor.getString( cursor.getColumnIndex( COL_ACCOUNT ) ) );
SavedAccount dst = new SavedAccount();
dst = (SavedAccount) parse( context, dst, src, dst );
if( dst != null ){
dst.db_id = cursor.getLong( cursor.getColumnIndex( COL_ID ) );
dst.host = cursor.getString( cursor.getColumnIndex( COL_HOST ) ).toLowerCase();
dst.acct = cursor.getString( cursor.getColumnIndex( COL_USER ) );
int colIdx_visibility = cursor.getColumnIndex( COL_VISIBILITY );
dst.visibility = cursor.isNull( colIdx_visibility ) ? null : cursor.getString( colIdx_visibility );
dst.confirm_boost = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_BOOST ) ) );
dst.dont_hide_nsfw = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_DONT_HIDE_NSFW ) ) );
dst.dont_show_timeout = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_DONT_SHOW_TIMEOUT ) ) );
dst.notification_mention = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_MENTION ) ) );
dst.notification_boost = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_BOOST ) ) );
dst.notification_favourite = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_FAVOURITE ) ) );
dst.notification_follow = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_FOLLOW ) ) );
dst.confirm_follow = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_FOLLOW ) ) );
dst.confirm_follow_locked = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_FOLLOW_LOCKED ) ) );
dst.confirm_unfollow = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_UNFOLLOW ) ) );
dst.confirm_post = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_POST ) ) );
int idx_notification_tag = cursor.getColumnIndex( COL_NOTIFICATION_TAG );
dst.notification_tag = cursor.isNull( idx_notification_tag ) ? null : cursor.getString( idx_notification_tag );
int idx_register_key = cursor.getColumnIndex( COL_REGISTER_KEY );
dst.register_key = cursor.isNull( idx_register_key ) ? null : cursor.getString( idx_register_key );
dst.register_time = cursor.getLong( cursor.getColumnIndex( COL_REGISTER_TIME ) );
dst.token_info = new JSONObject( cursor.getString( cursor.getColumnIndex( COL_TOKEN ) ) );
dst.sound_uri = cursor.getString( cursor.getColumnIndex( COL_SOUND_URI ) );
}
return dst;
}
public static long insert(
@NonNull String host,
@NonNull String acct,
@NonNull JSONObject account,
@NonNull JSONObject token
){
try{
ContentValues cv = new ContentValues();
cv.put( COL_HOST, host );
cv.put( COL_USER, acct );
cv.put( COL_ACCOUNT, account.toString() );
cv.put( COL_TOKEN, token.toString() );
return App1.getDB().insert( table, null, cv );
}catch( Throwable ex ){
log.trace( ex );
throw new RuntimeException( "SavedAccount.insert failed.", ex );
}
}
public void delete(){
try{
App1.getDB().delete( table, COL_ID + "=?", new String[]{ Long.toString( db_id ) } );
}catch( Throwable ex ){
log.trace( ex );
throw new RuntimeException( "SavedAccount.delete failed.", ex );
}
}
public void updateTokenInfo( @Nullable JSONObject token_info ){
if( db_id == INVALID_ID )
throw new RuntimeException( "SavedAccount.updateTokenInfo missing db_id" );
if( token_info == null ) token_info = new JSONObject();
this.token_info = token_info;
ContentValues cv = new ContentValues();
cv.put( COL_TOKEN, token_info.toString() );
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString( db_id ) } );
}
public void saveSetting(){
if( db_id == INVALID_ID )
throw new RuntimeException( "SavedAccount.saveSetting missing db_id" );
ContentValues cv = new ContentValues();
cv.put( COL_VISIBILITY, visibility );
cv.put( COL_CONFIRM_BOOST, confirm_boost ? 1 : 0 );
cv.put( COL_DONT_HIDE_NSFW, dont_hide_nsfw ? 1 : 0 );
cv.put( COL_DONT_SHOW_TIMEOUT, dont_show_timeout ? 1 : 0 );
cv.put( COL_NOTIFICATION_MENTION, notification_mention ? 1 : 0 );
cv.put( COL_NOTIFICATION_BOOST, notification_boost ? 1 : 0 );
cv.put( COL_NOTIFICATION_FAVOURITE, notification_favourite ? 1 : 0 );
cv.put( COL_NOTIFICATION_FOLLOW, notification_follow ? 1 : 0 );
cv.put( COL_CONFIRM_FOLLOW, confirm_follow ? 1 : 0 );
cv.put( COL_CONFIRM_FOLLOW_LOCKED, confirm_follow_locked ? 1 : 0 );
cv.put( COL_CONFIRM_UNFOLLOW, confirm_unfollow ? 1 : 0 );
cv.put( COL_CONFIRM_POST, confirm_post ? 1 : 0 );
cv.put( COL_SOUND_URI, sound_uri );
// UIからは更新しない
// notification_tag
// register_key
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString( db_id ) } );
}
public void saveNotificationTag(){
if( db_id == INVALID_ID )
throw new RuntimeException( "SavedAccount.saveNotificationTag missing db_id" );
ContentValues cv = new ContentValues();
cv.put( COL_NOTIFICATION_TAG, notification_tag );
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString( db_id ) } );
}
public void saveRegisterKey(){
if( db_id == INVALID_ID )
throw new RuntimeException( "SavedAccount.saveRegisterKey missing db_id" );
ContentValues cv = new ContentValues();
cv.put( COL_REGISTER_KEY, register_key );
cv.put( COL_REGISTER_TIME, register_time );
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString( db_id ) } );
}
public static final String REGISTER_KEY_UNREGISTERED = "unregistered";
public static void clearRegistrationCache(){
ContentValues cv = new ContentValues();
cv.put( COL_REGISTER_KEY, REGISTER_KEY_UNREGISTERED );
cv.put( COL_REGISTER_TIME, 0L );
App1.getDB().update( table, cv, null, null );
}
// onResumeの時に設定を読み直す
public void reloadSetting( Context context ){
if( db_id == INVALID_ID )
throw new RuntimeException( "SavedAccount.reloadSetting missing db_id" );
SavedAccount b = loadAccount( context, log, db_id );
if( b == null ) return; // DBから削除されてる
this.visibility = b.visibility;
this.confirm_boost = b.confirm_boost;
this.dont_hide_nsfw = b.dont_hide_nsfw;
this.dont_show_timeout = b.dont_show_timeout;
this.token_info = b.token_info;
this.notification_mention = b.notification_follow;
this.notification_boost = b.notification_boost;
this.notification_favourite = b.notification_favourite;
this.notification_follow = b.notification_follow;
this.notification_tag = b.notification_tag;
this.sound_uri = b.sound_uri;
}
public static @Nullable
SavedAccount loadAccount( @NonNull Context context, @NonNull LogCategory log, long db_id ){
try{
Cursor cursor = App1.getDB().query( table, null, COL_ID + "=?", new String[]{ Long.toString( db_id ) }, null, null, null );
try{
if( cursor.moveToFirst() ){
return parse( context, cursor );
}
}finally{
cursor.close();
}
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "loadAccount failed." );
}
return null;
}
public static @NonNull
ArrayList< SavedAccount > loadAccountList( Context context, @NonNull LogCategory log ){
ArrayList< SavedAccount > result = new ArrayList<>();
try{
Cursor cursor = App1.getDB().query( table, null, null, null, null, null, null );
try{
while( cursor.moveToNext() ){
result.add( parse( context, cursor ) );
}
}finally{
cursor.close();
}
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "loadAccountList failed." );
throw new RuntimeException( "SavedAccount.loadAccountList failed.", ex );
}
return result;
}
public static @NonNull
ArrayList< SavedAccount > loadByTag( Context context, @NonNull LogCategory log, String tag ){
ArrayList< SavedAccount > result = new ArrayList<>();
try{
Cursor cursor = App1.getDB().query( table, null, COL_NOTIFICATION_TAG + "=?", new String[]{ tag }, null, null, null );
try{
while( cursor.moveToNext() ){
result.add( parse( context, cursor ) );
}
}finally{
cursor.close();
}
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "loadByTag failed." );
throw new RuntimeException( "SavedAccount.loadByTag failed.", ex );
}
return result;
}
@Nullable
public static SavedAccount loadAccountByAcct( Context context, @NonNull LogCategory log, String full_acct ){
try{
Cursor cursor = App1.getDB().query( table, null, COL_USER + "=?", new String[]{ full_acct }, null, null, null );
try{
if( cursor.moveToNext() ){
return parse( context, cursor );
}
}finally{
cursor.close();
}
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "loadAccountByAcct failed." );
}
return null;
}
public static boolean hasRealAccount( @NonNull LogCategory log ){
try{
Cursor cursor = App1.getDB().query( table, null, COL_USER + " NOT LIKE '?@%'", null, null, null, null, "1" );
try{
if( cursor.moveToNext() ){
return true;
}
}finally{
cursor.close();
}
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "hasNonPseudoAccount failed." );
}
return false;
}
@SuppressWarnings("WeakerAccess")
public @NonNull String getAccountHost( @Nullable String acct ){
if( acct != null ){
int pos = acct.indexOf( '@' );
if( pos != - 1 ) return acct.substring( pos + 1 );
}
return this.host;
}
public @NonNull String getAccountHost( @Nullable TootAccount who ){
if( who != null ) return getAccountHost( who.acct );
return this.host;
}
public @NonNull String getFullAcct( @NonNull String acct ){
return acct.indexOf( '@' ) != - 1 ? acct : acct + "@" + this.host;
}
public String getFullAcct( @Nullable TootAccount who ){
if( who != null && who.acct != null ){
return getFullAcct( who.acct );
}
return "?@?";
}
@SuppressWarnings("WeakerAccess")
public boolean isLocalUser( @NonNull TootAccount who ){
return isLocalUser( who.acct );
}
@SuppressWarnings("WeakerAccess")
public boolean isLocalUser( @NonNull String acct ){
int pos = acct.indexOf( '@' );
return pos == - 1 || host.equalsIgnoreCase( acct.substring( pos + 1 ) );
}
public boolean isRemoteUser( @NonNull TootAccount who ){
return ! isLocalUser( who );
}
@SuppressWarnings("unused")
public boolean isRemoteUser( @NonNull String acct ){
return ! isLocalUser( acct );
}
public String getUserUrl( @NonNull String who_acct ){
int p = who_acct.indexOf( '@' );
if( - 1 != p ){
return "https://" + who_acct.substring( p + 1 ) + "/@" + who_acct.substring( 0, p );
}else{
return "https://" + host + "/@" + who_acct;
}
}
public boolean isMe( @Nullable TootAccount who ){
if( who == null ) return false;
int pos = this.acct.indexOf( '@' );
String this_user = this.acct.substring( 0, pos );
//
if( ! this_user.equals( who.username ) ) return false;
//
pos = who.acct.indexOf( '@' );
return pos == - 1 || this.host.equalsIgnoreCase( who.acct.substring( pos + 1 ) );
}
public boolean isMe( @NonNull String who_acct ){
int pos = this.acct.indexOf( '@' );
String this_user = this.acct.substring( 0, pos );
//
pos = who_acct.indexOf( '@' );
if( pos == - 1 ) return this_user.equals( who_acct );
//
return this_user.equals( who_acct.substring( 0, pos ) )
&& this.host.equalsIgnoreCase( who_acct.substring( pos + 1 ) );
}
public String supplyBaseUrl( String url ){
if( TextUtils.isEmpty( url ) ) return url;
if( url.charAt( 0 ) == '/' ) return "https://" + host + url;
return url;
}
public boolean isPseudo(){
return "?".equals( username );
}
public static long getCount(){
try{
Cursor cursor = App1.getDB().query( table, new String[]{ "count(*)" }, null, null, null, null, null );
try{
if( cursor.moveToNext() ){
return cursor.getLong( 0 );
}
}finally{
cursor.close();
}
}catch( Throwable ex ){
log.trace( ex );
log.e( ex, "getCount failed." );
throw new RuntimeException( "SavedAccount.getCount failed.", ex );
}
return 0L;
}
private static final String strNicoruHost = "friends.nico";
private static final Pattern reAtNicoruHost = Pattern.compile( "@friends\\.nico\\z", Pattern.CASE_INSENSITIVE );
public static boolean isNicoru( String acct ){
return acct != null && reAtNicoruHost.matcher( acct ).find();
}
public boolean isNicoru( TootAccount account ){
String host = this.host;
int host_start = 0;
if( account != null && account.acct != null ){
int pos = account.acct.indexOf( '@' );
if( pos != - 1 ){
host = account.acct;
host_start = pos + 1;
}
}
return host_match( strNicoruHost, 0, host, host_start );
}
private static int charAtLower( @NonNull final CharSequence src, final int pos ){
final int c = src.charAt( pos );
return ( c >= 'a' && c <= 'z' ? c - ( 'a' - 'A' ) : c );
}
private static boolean host_match( @NonNull final CharSequence a, int a_start, @NonNull final CharSequence b, int b_start ){
final int a_end = a.length();
final int b_end = b.length();
int a_remain = a_end - a_start;
final int b_remain = b_end - b_start;
// 文字数が違う
if( a_remain != b_remain ) return false;
// 文字数がゼロ
if( a_remain <= 0 ) return true;
// 末尾の文字が違う
if( charAtLower( a, a_end - 1 ) != charAtLower( b, b_end - 1 ) ) return false;
// 先頭からチェック
while( a_remain-- > 0 ){
if( charAtLower( a, a_start++ ) != charAtLower( b, b_start++ ) ) return false;
}
return true;
}
private static final Comparator< SavedAccount > account_comparator = new Comparator< SavedAccount >() {
@Override public int compare( SavedAccount a, SavedAccount b ){
int i;
// NA > !NA
i = ( a.isNA() ? 1 : 0 ) - ( b.isNA() ? 1 : 0 );
if( i != 0 ) return i;
// pseudo > real
i = ( a.isPseudo() ? 1 : 0 ) - ( b.isPseudo() ? 1 : 0 );
if( i != 0 ) return i;
String sa = AcctColor.getNickname( a.acct );
String sb = AcctColor.getNickname( b.acct );
return sa.compareToIgnoreCase( sb );
}
};
public static void sort( ArrayList< SavedAccount > account_list ){
Collections.sort( account_list, account_comparator );
}
public @NonNull String getAcctHost( @NonNull TootAccount who ){
String host = who.getAcctHost();
return host != null ? host : this.host;
}
// implements LinkClickContext
@Override @Nullable public AcctColor findAcctColor( @Nullable String url ){
if( url != null ){
Matcher m = TootAccount.reAccountUrl.matcher( url );
if( m.find() ) return AcctColor.load( m.group( 2 ) + "@" + m.group( 1 ) );
}
return null;
}
}