投稿画面でメンションの入力補完

This commit is contained in:
tateisu 2017-05-01 03:31:03 +09:00
parent a4ef01503e
commit 92fa93fedd
13 changed files with 512 additions and 40 deletions

View File

@ -37,7 +37,7 @@
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -9,8 +9,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 25
versionCode 25
versionName "0.2.5"
versionCode 26
versionName "0.2.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View File

@ -140,7 +140,17 @@ public class ActMain extends AppCompatActivity
}
}
ColumnViewHolder.ListItemPopup list_item_popup;
void closeListItemPopup(){
if( list_item_popup != null ){
list_item_popup.dismiss();
list_item_popup = null;
}
}
@Override protected void onPause(){
closeListItemPopup();
HTMLDecoder.link_callback = null;
super.onPause();
}

View File

@ -4,24 +4,34 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.provider.OpenableColumns;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.os.AsyncTaskCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.Layout;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CheckedTextView;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import com.android.volley.toolbox.NetworkImageView;
@ -41,9 +51,11 @@ import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.api.entity.TootMention;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.MyEditText;
import jp.juggler.subwaytooter.util.Utils;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
@ -141,10 +153,13 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
super.onActivityResult( requestCode, resultCode, data );
}
SharedPreferences pref;
@Override
protected void onCreate( @Nullable Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
App1.setActivityTheme( this, true );
pref = Pref.pref( this );
initUI();
if( account_list.isEmpty() ){
@ -296,6 +311,12 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
showReplyTo();
}
@Override protected void onDestroy(){
handler.removeCallbacks( proc_text_changed );
closeAcctPopup();
super.onDestroy();
}
@Override
protected void onSaveInstanceState( Bundle outState ){
if( account != null ){
@ -326,12 +347,13 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
View btnAttachment;
View btnPost;
View llAttachment;
final NetworkImageView[] ivMedia = new NetworkImageView[4];
final NetworkImageView[] ivMedia = new NetworkImageView[ 4 ];
CheckBox cbNSFW;
CheckBox cbContentWarning;
EditText etContentWarning;
EditText etContent;
MyEditText etContentWarning;
MyEditText etContent;
TextView tvCharCount;
Handler handler;
ArrayList< SavedAccount > account_list;
@ -342,20 +364,21 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
private void initUI(){
setContentView( R.layout.act_post );
handler = new Handler();
btnAccount = (Button) findViewById( R.id.btnAccount );
btnVisibility = (ImageButton) findViewById( R.id.btnVisibility );
btnAttachment = findViewById( R.id.btnAttachment );
btnPost = findViewById( R.id.btnPost );
llAttachment = findViewById( R.id.llAttachment );
ivMedia[0] = (NetworkImageView) findViewById( R.id.ivMedia1 );
ivMedia[1] = (NetworkImageView) findViewById( R.id.ivMedia2 );
ivMedia[2] = (NetworkImageView) findViewById( R.id.ivMedia3 );
ivMedia[3] = (NetworkImageView) findViewById( R.id.ivMedia4 );
ivMedia[ 0 ] = (NetworkImageView) findViewById( R.id.ivMedia1 );
ivMedia[ 1 ] = (NetworkImageView) findViewById( R.id.ivMedia2 );
ivMedia[ 2 ] = (NetworkImageView) findViewById( R.id.ivMedia3 );
ivMedia[ 3 ] = (NetworkImageView) findViewById( R.id.ivMedia4 );
cbNSFW = (CheckBox) findViewById( R.id.cbNSFW );
cbContentWarning = (CheckBox) findViewById( R.id.cbContentWarning );
etContentWarning = (EditText) findViewById( R.id.etContentWarning );
etContent = (EditText) findViewById( R.id.etContent );
etContentWarning = (MyEditText) findViewById( R.id.etContentWarning );
etContent = (MyEditText) findViewById( R.id.etContent );
tvCharCount = (TextView) findViewById( R.id.tvCharCount );
llReply = findViewById( R.id.llReply );
@ -377,12 +400,12 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
btnPost.setOnClickListener( this );
btnRemoveReply.setOnClickListener( this );
for( NetworkImageView iv :ivMedia){
for( NetworkImageView iv : ivMedia ){
iv.setOnClickListener( this );
iv.setDefaultImageResId( Styler.getAttributeResourceId( this,R.attr.btn_refresh ));
// iv.setErrorImageResId( Styler.getAttributeResourceId( this,R.attr.btn_refresh ));
iv.setDefaultImageResId( Styler.getAttributeResourceId( this, R.attr.btn_refresh ) );
// iv.setErrorImageResId( Styler.getAttributeResourceId( this,R.attr.btn_refresh ));
}
cbContentWarning.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged( CompoundButton buttonView, boolean isChecked ){
@ -398,7 +421,11 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
@Override
public void onTextChanged( CharSequence s, int start, int before, int count ){
if( count > 0 ){
log.d( "onTextChanged" );
handler.removeCallbacks( proc_text_changed );
handler.postDelayed( proc_text_changed, 1500L );
}
}
@Override
@ -406,13 +433,176 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
updateTextCount();
}
} );
etContent.setOnSelectionChangeListener( new MyEditText.OnSelectionChangeListener() {
int last_selection = - 1;
@Override public void onSelectionChanged( int selStart, int selEnd ){
if( selStart != selEnd ){
// 範囲選択されてるならポップアップは閉じる
log.d( "onSelectionChanged: range selected" );
closeAcctPopup();
}else if( selStart > last_selection ){
// 文字挿入の直後かもしれないので何もしない
log.d( "onSelectionChanged: may after text input? " );
}else{
// 前方への移動ではないならポップアップは閉じる
log.d( "onSelectionChanged: not forward change" );
closeAcctPopup();
}
last_selection = selStart;
}
} );
}
final Runnable proc_text_changed = new Runnable() {
@Override public void run(){
int ss = etContent.getSelectionStart();
int se = etContent.getSelectionEnd();
if( ss != se ){
closeAcctPopup();
return;
}
int end = ss;
String src = etContent.getText().toString();
int start = ss;
int count_atMark = 0;
int[] pos_atMark = new int[ 2 ];
for( ; ; ){
if( start == 0 ) break;
if( count_atMark >= 2 ) break;
char c = src.charAt( start - 1 );
if( ( '0' <= c && c <= '9' )
|| ( 'A' <= c && c <= 'Z' )
|| ( 'a' <= c && c <= 'z' )
|| c == '_'
){
-- start;
continue;
}else if( c == '@' ){
-- start;
pos_atMark[ count_atMark++ ] = start;
continue;
}
break;
}
if( count_atMark == 0 ){
closeAcctPopup();
return;
}else if( count_atMark == 1 ){
start = pos_atMark[ 0 ];
}else if( count_atMark == 2 ){
start = pos_atMark[ 1 ];
}
if( end - start < 2 ){
closeAcctPopup();
return;
}
int limit = 10;
String s = src.substring( start, end );
ArrayList< String > acct_list = AcctSet.searchPrefix( s, limit );
log.d( "search for %s, result=%d", s, acct_list.size() );
if( acct_list.isEmpty() || acct_list.size() >= limit ){
closeAcctPopup();
return;
}
openAcctPopup( acct_list, start, end );
}
};
PopupWindow acct_popup;
private void closeAcctPopup(){
if( acct_popup != null ){
acct_popup.dismiss();
acct_popup = null;
}
}
private void openAcctPopup( ArrayList< String > acct_list, final int start, final int end ){
closeAcctPopup();
View viewRoot = getLayoutInflater().inflate( R.layout.acct_complete_popup, null, false );
LinearLayout llItems = (LinearLayout) viewRoot.findViewById( R.id.llItems );
{
CheckedTextView v = (CheckedTextView) getLayoutInflater().inflate( R.layout.lv_spinner_dropdown, llItems, false );
v.setTextColor( Styler.getAttributeColor( this, android.R.attr.textColorPrimary ) );
v.setText( R.string.close );
v.setOnClickListener( new View.OnClickListener() {
@Override public void onClick( View v ){
closeAcctPopup();
}
} );
llItems.addView( v );
}
for( int i = 0 ; ; ++ i ){
if( i >= acct_list.size() ) break;
final String acct = acct_list.get( i );
CheckedTextView v = (CheckedTextView) getLayoutInflater().inflate( R.layout.lv_spinner_dropdown, llItems, false );
v.setTextColor( Styler.getAttributeColor( this, android.R.attr.textColorPrimary ) );
v.setText( acct );
v.setOnClickListener( new View.OnClickListener() {
@Override public void onClick( View v ){
String s = etContent.getText().toString();
s = s.substring( 0, start ) + acct + " " + ( end >= s.length() ? "" : s.substring( end ) );
etContent.setText( s );
etContent.setSelection( start + acct.length() + 1 );
closeAcctPopup();
}
} );
llItems.addView( v );
}
//
acct_popup = new PopupWindow( this );
acct_popup.setBackgroundDrawable( ContextCompat.getDrawable( this, R.drawable.acct_popup_bg ) );
// Resources.Theme popupTheme = getResources().newTheme();
//
// int theme_idx = pref.getInt(Pref.KEY_UI_THEME,0);
// switch(theme_idx){
//
// default:
// case 0:
// popupTheme.applyStyle( R.style.Theme_AppCompat_Light_Dialog, true);
// break;
//
// case 1:
// popupTheme.applyStyle( R.style.Theme_AppCompat_Dialog, true);
// break;
//
// }
acct_popup.setWidth( WindowManager.LayoutParams.WRAP_CONTENT );
acct_popup.setHeight( WindowManager.LayoutParams.WRAP_CONTENT );
acct_popup.setContentView( viewRoot );
acct_popup.setTouchable( true );
int[] location = new int[ 2 ];
etContent.getLocationOnScreen( location );
int y = location[ 1 ];
y += etContent.getTotalPaddingTop();
y -= etContent.getScrollY();
Layout layout = etContent.getLayout();
y += layout.getLineBottom( layout.getLineCount() - 1 );
acct_popup.showAtLocation(
etContent
, Gravity.CENTER_HORIZONTAL | Gravity.TOP
, 0
, y
);
}
private void updateTextCount(){
String s = etContent.getText().toString();
int count = s.codePointCount( 0,s.length() );
int remain = 500 - count;
tvCharCount.setText( Integer.toString( remain ) );
int count_content = s.codePointCount( 0, s.length() );
s = cbContentWarning.isChecked() ? etContentWarning.getText().toString() : "";
int count_spoiler = s.codePointCount( 0, s.length() );
int remain = 500 - count_content - count_spoiler;
tvCharCount.setText( Integer.toString( remain ) );
int color = Styler.getAttributeColor( this, remain < 0 ? R.attr.colorRegexFilterError : android.R.attr.textColorPrimary );
tvCharCount.setTextColor( color );
}
@ -451,7 +641,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
}else{
tmp_account_list.addAll( account_list );
}
String[] caption_list = new String[ tmp_account_list.size() ];
for( int i = 0, ie = tmp_account_list.size() ; i < ie ; ++ i ){
caption_list[ i ] = tmp_account_list.get( i ).acct;
@ -463,7 +653,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
@Override
public void onClick( DialogInterface dialog, int which ){
if( which >= 0 && which < tmp_account_list.size() ){
SavedAccount account =tmp_account_list.get( which );
SavedAccount account = tmp_account_list.get( which );
setAccount( account );
try{
if( account.visibility != null && TootStatus.compareVisibility( visibility, account.visibility ) > 0 ){
@ -471,8 +661,8 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
visibility = account.visibility;
showVisibility();
}
}catch(Throwable ex){
ex.printStackTrace( );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
}
@ -499,8 +689,8 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
llAttachment.setVisibility( View.GONE );
}else{
llAttachment.setVisibility( View.VISIBLE );
for(int i=0,ie=ivMedia.length;i<ie;++i){
showAttachment_sub( ivMedia[i], i );
for( int i = 0, ie = ivMedia.length ; i < ie ; ++ i ){
showAttachment_sub( ivMedia[ i ], i );
}
}
}
@ -624,7 +814,8 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener {
@Override
public void writeTo( BufferedSink sink ) throws IOException{
InputStream is = getContentResolver().openInputStream( uri );
if( is == null ) throw new IOException( "openInputStream() failed. uri="+uri );
if( is == null )
throw new IOException( "openInputStream() failed. uri=" + uri );
try{
byte[] tmp = new byte[ 4096 ];
for( ; ; ){

View File

@ -17,6 +17,7 @@ import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.ClientInfo;
import jp.juggler.subwaytooter.table.ContentWarning;
@ -33,11 +34,12 @@ public class App1 extends Application {
static final String DB_NAME = "app_db";
static final int DB_VERSION = 6;
static final int DB_VERSION = 7;
// 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加
// 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加
// 2017/4/29 v20 2=>5 MediaShown,ContentWarningのインデクスが間違っていたので貼り直す
// 2017/4/29 v23 5=>6 MutedAppテーブルの追加UserRelationテーブルの追加
// 2017/5/01 v26 6=>7 AcctSetテーブルの追加
static DBOpenHelper db_open_helper;
public static SQLiteDatabase getDB(){
@ -77,6 +79,7 @@ public class App1 extends Application {
NotificationTracking.onDBCreate(db);
MutedApp.onDBCreate(db);
UserRelation.onDBCreate(db);
AcctSet.onDBCreate( db );
}
@Override
@ -90,6 +93,7 @@ public class App1 extends Application {
NotificationTracking.onDBUpgrade( db, oldVersion, newVersion );
MutedApp.onDBUpgrade( db, oldVersion, newVersion );
UserRelation.onDBUpgrade( db, oldVersion, newVersion );
AcctSet.onDBUpgrade( db, oldVersion, newVersion );
}
}
@ -182,6 +186,8 @@ public class App1 extends Application {
// SQLiteDatabase db = db_open_helper.getWritableDatabase();
// db_open_helper.onCreate( db );
// }
UserRelation.deleteOld(System.currentTimeMillis());
AcctSet.deleteOld(System.currentTimeMillis());
}
if( image_loader == null ){

View File

@ -31,6 +31,7 @@ import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.api.entity.TootReport;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.UserRelation;
@ -51,6 +52,7 @@ class Column {
private static final long LOOP_TIMEOUT = 10000L;
private static final int LOOP_READ_ENOUGH = 30; // フィルタ後のデータ数がコレ以上ならループを諦めます
private static final int RELATIONSHIP_LOAD_STEP = 40;
private static final int ACCT_DB_STEP = 100;
// ステータスのリストを返すAPI
private static final String PATH_HOME = "/api/v1/timelines/home?limit=" + READ_LIMIT;
@ -1879,6 +1881,7 @@ class Column {
private void updateRelation( TootApiClient client, ArrayList< Object > list_tmp ){
if( list_tmp == null || list_tmp.isEmpty() ) return;
HashSet< Long > who_set = new HashSet<>();
HashSet<String> acct_set = new HashSet<>();
{
TootAccount a;
TootStatus s;
@ -1887,29 +1890,45 @@ class Column {
if( o instanceof TootAccount ){
a = (TootAccount) o;
who_set.add( a.id );
acct_set.add( "@" + access_info.getFullAcct( a ));
}else if( o instanceof TootStatus ){
s = (TootStatus) o;
a = s.account;
if( a != null ) who_set.add( a.id );
if( a != null ){
who_set.add( a.id );
acct_set.add( "@" + access_info.getFullAcct( a ));
}
s = s.reblog;
if( s != null ){
a = s.account;
if( a != null ) who_set.add( a.id );
if( a != null ){
who_set.add( a.id );
acct_set.add( "@" + access_info.getFullAcct( a ));
}
}
}else if( o instanceof TootNotification ){
n = (TootNotification) o;
//
a = n.account;
if( a != null ) who_set.add( a.id );
if( a != null ){
who_set.add( a.id );
acct_set.add( "@" + access_info.getFullAcct( a ));
}
//
s = n.status;
if( s != null ){
a = s.account;
if( a != null ) who_set.add( a.id );
if( a != null ){
who_set.add( a.id );
acct_set.add( "@" + access_info.getFullAcct( a ));
}
s = s.reblog;
if( s != null ){
a = s.account;
if( a != null ) who_set.add( a.id );
if( a != null ){
who_set.add( a.id );
acct_set.add( "@" + access_info.getFullAcct( a ));
}
}
}
}
@ -1925,6 +1944,7 @@ class Column {
}
}
long now = System.currentTimeMillis();
int n = 0;
while( n < size ){
StringBuilder sb = new StringBuilder();
@ -1941,12 +1961,34 @@ class Column {
break;
}else if( result.array != null ){
TootRelationShip.List list = TootRelationShip.parseList( log, result.array );
long now = System.currentTimeMillis();
UserRelation.saveList( now, access_info.db_id, list );
}
}
log.d( "updateRelation: update %d relations.", n );
}
size = acct_set.size();
if( size > 0 ){
String[] acct_list = new String[ size ];
{
int n = 0;
for( String l : acct_set ){
acct_list[ n++ ] = l;
}
}
long now = System.currentTimeMillis();
int n = 0;
while( n < size ){
int length = size-n;
if( length > ACCT_DB_STEP ) length = ACCT_DB_STEP;
AcctSet.saveList( now, acct_list, n,length );
n += length;
}
log.d( "updateRelation: update %d acct.", n );
}
}
}

View File

@ -79,6 +79,8 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S
saveScrollPosition();
log.d( "onPageDestroy:%s", column.getColumnName( true ) );
column.removeVisualListener( this );
activity.closeListItemPopup();
}
private TextView tvLoading;
@ -1047,13 +1049,15 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S
void onItemClick( View anchor ){
if( status != null ){
activity.closeListItemPopup();
// ポップアップを表示する
ListItemPopup popup = new ListItemPopup();
popup.show( anchor, status );
activity.list_item_popup = new ListItemPopup();
activity.list_item_popup.show( anchor, status );
}
}
}
private final ActMain.RelationChangedCallback favourite_complete_callback = new ActMain.RelationChangedCallback() {
@Override public void onRelationChanged(){
Utils.showToast( activity, false, R.string.favourite_succeeded );
@ -1147,7 +1151,7 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S
}
}
private class ListItemPopup {
class ListItemPopup {
final View viewRoot;
final ButtonsForStatus buttons_for_status;
@ -1215,5 +1219,9 @@ class ColumnViewHolder implements View.OnClickListener, Column.VisualCallback, S
, popup_y
);
}
public void dismiss(){
}
}
}

View File

@ -0,0 +1,145 @@
package jp.juggler.subwaytooter.table;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.util.LogCategory;
public class AcctSet {
private static final LogCategory log = new LogCategory( "AcctSet" );
private static final String table = "acct_set";
private static final String COL_TIME_SAVE = "time_save";
private static final String COL_ACCT = "acct"; //@who@host ascii文字の大文字小文字は(sqliteにより)同一視される
public static void onDBCreate( SQLiteDatabase db ){
log.d( "onDBCreate!" );
db.execSQL(
"create table if not exists " + table
+ "(_id INTEGER PRIMARY KEY"
+ "," + COL_TIME_SAVE + " integer not null"
+ "," + COL_ACCT + " text not null"
+ ")"
);
db.execSQL(
"create unique index if not exists " + table + "_acct on " + table + "(" + COL_ACCT + ")"
);
db.execSQL(
"create index if not exists " + table + "_time on " + table + "(" + COL_TIME_SAVE + ")"
);
}
public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
if( oldVersion < 7 && newVersion >= 7 ){
onDBCreate( db );
}
}
public static void deleteOld( long now ){
try{
// 古いデータを掃除する
long expire = now - 86400000L * 365;
App1.getDB().delete( table, COL_TIME_SAVE + "<?", new String[]{ Long.toString( expire ) } );
}catch( Throwable ex ){
log.e( ex, "deleteOld failed." );
}
}
// public static void save1( long now, String acct ){
// try{
//
// ContentValues cv = new ContentValues();
// cv.put( COL_TIME_SAVE, now );
// cv.put( COL_ACCT, acct );
// App1.getDB().replace( table, null, cv );
// }catch( Throwable ex ){
// log.e( ex, "save failed." );
// }
// }
public static void saveList( long now, String[] src_list, int offset, int length ){
try{
ContentValues cv = new ContentValues();
cv.put( COL_TIME_SAVE, now );
boolean bOK = false;
SQLiteDatabase db = App1.getDB();
db.execSQL( "BEGIN TRANSACTION" );
try{
for( int i = 0 ; i < length ; ++ i ){
String acct = src_list[ i + offset ];
cv.put( COL_ACCT, acct );
db.replace( table, null, cv );
}
bOK = true;
}catch( Throwable ex ){
ex.printStackTrace();
log.e( ex, "saveList failed." );
}
if( bOK ){
db.execSQL( "COMMIT TRANSACTION" );
}else{
db.execSQL( "ROLLBACK TRANSACTION" );
}
}catch( Throwable ex ){
ex.printStackTrace();
log.e( ex, "saveList failed." );
}
}
private static final String prefix_search_where = COL_ACCT + " like ? escape '$'";
private static final ThreadLocal< String[] > prefix_search_where_arg = new ThreadLocal< String[] >() {
@Override protected String[] initialValue(){
return new String[ 1 ];
}
};
private static String makePattern( String src ){
StringBuilder sb = new StringBuilder();
for( int i = 0, ie = src.length() ; i < ie ; ++ i ){
char c = src.charAt( i );
if( c == '%' || c == '_' || c == '$' ){
sb.append( '$' );
}
sb.append( c );
}
// 前方一致検索にするため末尾に%をつける
sb.append( '%' );
return sb.toString();
}
@NonNull public static ArrayList< String > searchPrefix( @NonNull String prefix ,int limit){
try{
String[] where_arg = prefix_search_where_arg.get();
where_arg[ 0 ] = makePattern( prefix );
Cursor cursor = App1.getDB().query( table, null, prefix_search_where, where_arg, null, null, COL_ACCT + " asc limit "+limit );
if( cursor != null ){
try{
ArrayList< String > dst = new ArrayList<>( cursor.getCount() );
int idx_acct = cursor.getColumnIndex( COL_ACCT );
while( cursor.moveToNext() ){
dst.add( cursor.getString( idx_acct ) );
}
return dst;
}finally{
cursor.close();
}
}
}catch( Throwable ex ){
ex.printStackTrace();
log.e( ex, "searchPrefix failed." );
}
return new ArrayList<>();
}
}

View File

@ -0,0 +1,42 @@
package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.support.v7.widget.AppCompatEditText;
import android.util.AttributeSet;
import android.widget.EditText;
/**
* Created by tateisu on 2017/05/01.
*/
public class MyEditText extends AppCompatEditText {
public MyEditText( Context context ){
super( context );
}
public MyEditText( Context context, AttributeSet attrs ){
super( context, attrs );
}
public MyEditText( Context context, AttributeSet attrs, int defStyleAttr ){
super( context, attrs, defStyleAttr );
}
public interface OnSelectionChangeListener {
void onSelectionChanged( int selStart, int selEnd );
}
OnSelectionChangeListener mOnSelectionChangeListener;
public void setOnSelectionChangeListener( OnSelectionChangeListener listener ){
mOnSelectionChangeListener = listener;
}
@Override
protected void onSelectionChanged( int selStart, int selEnd ){
super.onSelectionChanged( selStart, selEnd );
if( mOnSelectionChangeListener != null ){
mOnSelectionChangeListener.onSelectionChanged( selStart, selEnd );
}
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
>
<solid android:color="?android:attr/colorBackground" />
<stroke
android:color="?android:attr/colorForeground"
android:width="1dp"
/>
</shape>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<LinearLayout
android:id="@+id/llItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
/>
</ScrollView>

View File

@ -174,7 +174,7 @@
android:background="?attr/colorPostFormBackground"
>
<EditText
<jp.juggler.subwaytooter.util.MyEditText
android:id="@+id/etContentWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -196,7 +196,7 @@
android:background="?attr/colorPostFormBackground"
>
<EditText
<jp.juggler.subwaytooter.util.MyEditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -202,4 +202,5 @@
<string name="follow_request_ng">Reject for follow request</string>
<string name="follow_request_rejected">%1$s\'s follow request is rejected.</string>
<string name="follow_request_authorized">%1$s\'s follow request is authorized.</string>
<string name="close">Close</string>
</resources>