おすすめプラグインリストの表示崩れの修正。トゥートをTLに紐付けられたアカウントでお気に入り/ブーストした後に同タンス別アカウントでブーストしようとするとうまく動作しない問題の修正

This commit is contained in:
tateisu 2017-07-04 00:43:45 +09:00
parent 4dc139b6d9
commit 7b48ebd747
11 changed files with 230 additions and 162 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 25
versionCode 86
versionName "0.8.6"
versionCode 87
versionName "0.8.7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View File

@ -54,6 +54,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -455,7 +456,7 @@ public class ActMain extends AppCompatActivity
current_column = app_state.column_list.get( vs );
}
}
if( current_column != null && !current_column.dont_close){
if( current_column != null && ! current_column.dont_close ){
final Column _column = current_column;
dialog.addAction( getString( R.string.close_column ), new Runnable() {
@Override public void run(){
@ -469,7 +470,7 @@ public class ActMain extends AppCompatActivity
openColumnList();
}
} );
dialog.addAction( getString( R.string.app_exit ), new Runnable() {
@Override public void run(){
ActMain.this.finish();
@ -591,8 +592,6 @@ public class ActMain extends AppCompatActivity
}else if( id == R.id.nav_add_domain_blocks ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_DOMAIN_BLOCKS );
}else if( id == R.id.nav_follow_requests ){
performAddTimeline( getDefaultInsertPosition(), false, Column.TYPE_FOLLOW_REQUESTS );
@ -1100,7 +1099,7 @@ public class ActMain extends AppCompatActivity
return;
}else if( uri.getPath().startsWith( "/users/" ) ){
// どうも古い形式らしいがこういうURLもあるらしい
// https://mastodon.juggler.jp/users/SubwayTooter/updates/(status_id)
Matcher m = reStatusPage2.matcher( uri.toString() );
@ -1602,7 +1601,7 @@ public class ActMain extends AppCompatActivity
}
do{
if( pref.getBoolean( Pref.KEY_PRIOR_CHROME,true )){
if( pref.getBoolean( Pref.KEY_PRIOR_CHROME, true ) ){
try{
// 初回はChrome指定で試す
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
@ -1621,9 +1620,9 @@ public class ActMain extends AppCompatActivity
builder.setToolbarColor( Styler.getAttributeColor( this, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( this, Uri.parse( url ) );
}while(false);
}while( false );
}catch( Throwable ex ){
// ex.printStackTrace();
log.e( ex, "openChromeTab failed. url=%s", url );
@ -1969,21 +1968,18 @@ public class ActMain extends AppCompatActivity
public void performFavourite(
final SavedAccount access_info
, final boolean bRemote
, final boolean bSet
, final TootStatus arg_status
, final int nCrossAccountMode
, final boolean bSet
, final RelationChangedCallback callback
){
//
final String busy_key = access_info.host + ":" + arg_status.id;
//
if( ! bRemote ){
if( app_state.map_busy_fav.contains( busy_key ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
app_state.map_busy_fav.add( busy_key );
if( app_state.isBusyFav( access_info, arg_status ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
//
app_state.setBusyFav( access_info, arg_status );
//
new AsyncTask< Void, Void, TootApiResult >() {
TootStatus new_status;
@ -2001,9 +1997,7 @@ public class ActMain extends AppCompatActivity
TootApiResult result;
TootStatus target_status;
if( ! bRemote ){
target_status = arg_status;
}else{
if( nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE ){
// 検索APIに他タンスのステータスのURLを投げると自タンスのステータスを得られる
String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) );
path = path + "&resolve=1";
@ -2026,6 +2020,8 @@ public class ActMain extends AppCompatActivity
}else if( target_status.favourited ){
return new TootApiResult( getString( R.string.already_favourited ) );
}
}else{
target_status = arg_status;
}
Request.Builder request_builder = new Request.Builder()
@ -2055,39 +2051,38 @@ public class ActMain extends AppCompatActivity
@Override
protected void onPostExecute( TootApiResult result ){
if( ! bRemote ){
app_state.map_busy_fav.remove( busy_key );
}
app_state.resetBusyFav( access_info, arg_status );
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( new_status != null ){
if( ! bRemote ){
// カウント数は遅延があるみたい
if( bSet && new_status.favourites_count <= arg_status.favourites_count ){
// 星つけたのにカウントが上がらないのは違和感あるので表示をいじる
new_status.favourites_count = arg_status.favourites_count + 1;
}else if( ! bSet && new_status.favourites_count >= arg_status.favourites_count ){
// 星外したのにカウントが下がらないのは違和感あるので表示をいじる
new_status.favourites_count = arg_status.favourites_count - 1;
if( new_status.favourites_count < 0 ){
new_status.favourites_count = 0;
}
// カウント数は遅延があるみたいなので恣意的に表示を変更する
if( bSet && new_status.favourited && new_status.favourites_count <= arg_status.favourites_count ){
// 星をつけたのにカウントが上がらないのは違和感あるので表示をいじる
new_status.favourites_count = arg_status.favourites_count + 1;
}else if( ! bSet && ! new_status.favourited && new_status.favourites_count >= arg_status.favourites_count ){
// 星を外したのにカウントが下がらないのは違和感あるので表示をいじる
new_status.favourites_count = arg_status.favourites_count - 1;
// 0未満にはならない
if( new_status.favourites_count < 0 ){
new_status.favourites_count = 0;
}
}
for( Column column : app_state.column_list ){
column.findStatus( access_info, new_status.id, new Column.StatusEntryCallback() {
column.findStatus( access_info.host, new_status.id, new Column.StatusEntryCallback() {
@Override
public void onIterate( TootStatus status ){
status.favourited = new_status.favourited;
public boolean onIterate( SavedAccount account, TootStatus status ){
status.favourites_count = new_status.favourites_count;
if( access_info.acct.equalsIgnoreCase( account.acct ) ){
status.favourited = new_status.favourited;
}
return true;
}
} );
}
if( callback != null ) callback.onRelationChanged();
}else{
@ -2107,49 +2102,52 @@ public class ActMain extends AppCompatActivity
public void performBoost(
final SavedAccount access_info
, final boolean bRemote
, final boolean bSet
, final TootStatus arg_status
, boolean bConfirmed
, final int nCrossAccountMode
, final boolean bSet
, final boolean bConfirmed
, final RelationChangedCallback callback
){
//
final String busy_key = access_info.host + ":" + arg_status.id;
if( ! bRemote ){
//
if( app_state.map_busy_boost.contains( busy_key ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
//
// アカウントからステータスにブースト操作を行っているなら何もしない
if( app_state.isBusyBoost( access_info, arg_status ) ){
Utils.showToast( this, false, R.string.wait_previous_operation );
return;
}
// クロスアカウント操作ではないならステータス内容を使ったチェックを行える
if( nCrossAccountMode == NOT_CROSS_ACCOUNT ){
if( arg_status.reblogged ){
// FAVがついているかFAV操作中はBoostを外せない
if( app_state.isBusyFav( access_info, arg_status ) || arg_status.favourited ){
// FAVがついているかFAV操作中はBoostを外せない
Utils.showToast( this, false, R.string.cant_remove_boost_while_favourited );
return;
}
}else if( ! bConfirmed ){
DlgConfirm.open( this, getString( R.string.confirm_boost_from, AcctColor.getNickname( access_info.acct ) ), new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_boost;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_boost = bv;
access_info.saveSetting();
reloadAccountSetting( access_info );
}
@Override public void onOK(){
performBoost( access_info, false, bSet, arg_status, true, callback );
}
} );
return;
}
//
app_state.map_busy_boost.add( busy_key );
}
// 操作確認が必要なら確認を出す
if( ! bConfirmed ){
DlgConfirm.open( this, getString( R.string.confirm_boost_from, AcctColor.getNickname( access_info.acct ) ), new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return access_info.confirm_boost;
}
@Override public void setConfirmEnabled( boolean bv ){
access_info.confirm_boost = bv;
access_info.saveSetting();
reloadAccountSetting( access_info );
}
@Override public void onOK(){
performBoost( access_info, arg_status, nCrossAccountMode, bSet, true, callback );
}
} );
return;
}
app_state.setBusyBoost( access_info, arg_status );
//
new AsyncTask< Void, Void, TootApiResult >() {
@ -2169,9 +2167,7 @@ public class ActMain extends AppCompatActivity
TootApiResult result;
TootStatus target_status;
if( ! bRemote ){
target_status = arg_status;
}else{
if( nCrossAccountMode == CROSS_ACCOUNT_REMOTE_INSTANCE ){
// 検索APIに他タンスのステータスのURLを投げると自タンスのステータスを得られる
String path = String.format( Locale.JAPAN, Column.PATH_SEARCH, Uri.encode( arg_status.url ) );
path = path + "&resolve=1";
@ -2192,6 +2188,9 @@ public class ActMain extends AppCompatActivity
}else if( target_status.reblogged ){
return new TootApiResult( getString( R.string.already_boosted ) );
}
}else{
// 既に自タンスのステータスがある
target_status = arg_status;
}
Request.Builder request_builder = new Request.Builder()
@ -2221,33 +2220,36 @@ public class ActMain extends AppCompatActivity
}
@Override protected void onPostExecute( TootApiResult result ){
if( ! bRemote ){
app_state.map_busy_boost.remove( busy_key );
}
app_state.resetBusyBoost( access_info, arg_status );
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( new_status != null ){
if( ! bRemote ){
// カウント数は遅延があるみたい
if( new_status.reblogged && new_status.reblogs_count <= arg_status.reblogs_count ){
// つけたのにカウントが上がらないのは違和感あるので表示をいじる
new_status.reblogs_count = arg_status.reblogs_count + 1;
}else if( ! new_status.reblogged && new_status.reblogs_count >= arg_status.reblogs_count ){
// 外したのにカウントが下がらないのは違和感あるので表示をいじる
new_status.reblogs_count = arg_status.reblogs_count - 1;
if( new_status.reblogs_count < 0 ){
new_status.reblogs_count = 0;
}
// カウント数は遅延があるみたいなので恣意的に表示を変更する
// ブーストカウント数を加工する
if( bSet && new_status.reblogged && new_status.reblogs_count <= arg_status.reblogs_count ){
// つけたのにカウントが上がらないのは違和感あるので表示をいじる
new_status.reblogs_count = arg_status.reblogs_count + 1;
}else if( ! bSet && ! new_status.reblogged && new_status.reblogs_count >= arg_status.reblogs_count ){
// 外したのにカウントが下がらないのは違和感あるので表示をいじる
new_status.reblogs_count = arg_status.reblogs_count - 1;
// 0未満にはならない
if( new_status.reblogs_count < 0 ){
new_status.reblogs_count = 0;
}
}
for( Column column : app_state.column_list ){
column.findStatus( access_info, new_status.id, new Column.StatusEntryCallback() {
@Override public void onIterate( TootStatus status ){
status.reblogged = new_status.reblogged;
column.findStatus( access_info.host, new_status.id, new Column.StatusEntryCallback() {
@Override
public boolean onIterate( SavedAccount account, TootStatus status ){
status.reblogs_count = new_status.reblogs_count;
if( access_info.acct.equalsIgnoreCase( account.acct ) ){
status.reblogged = new_status.reblogged;
}
return true;
}
} );
}
@ -2255,6 +2257,8 @@ public class ActMain extends AppCompatActivity
}else{
Utils.showToast( ActMain.this, true, result.error );
}
// 結果に関わらず更新中状態から復帰させる
showColumnMatchAccount( access_info );
}
@ -2823,7 +2827,7 @@ public class ActMain extends AppCompatActivity
}
}.executeOnExecutor( App1.task_executor );
}
void callDomainBlock( final SavedAccount access_info, final String domain, final boolean bBlock, final RelationChangedCallback callback ){
new AsyncTask< Void, Void, TootApiResult >() {
@ -2838,24 +2842,23 @@ public class ActMain extends AppCompatActivity
} );
client.setAccount( access_info );
Request.Builder request_builder = new Request.Builder();
if( bBlock ){
request_builder.post(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "domain="+ Uri.encode( domain )
, "domain=" + Uri.encode( domain )
) );
}else{
request_builder.delete(
RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, "domain="+ Uri.encode( domain )
, "domain=" + Uri.encode( domain )
) );
}
TootApiResult result = client.request( "/api/v1/domain_blocks" , request_builder );
TootApiResult result = client.request( "/api/v1/domain_blocks", request_builder );
if( result != null ){
if( result.object != null ){
@ -2882,7 +2885,7 @@ public class ActMain extends AppCompatActivity
for( Column column : app_state.column_list ){
if( bBlock ){
column.onDomainBlockChanged( access_info, domain,bBlock );
column.onDomainBlockChanged( access_info, domain, bBlock );
}
}
@ -3229,18 +3232,33 @@ public class ActMain extends AppCompatActivity
return dst;
}
void openBoostFromAnotherAccount( @NonNull final SavedAccount access_info, final TootStatus status ){
// 別アカ操作と別タンスの関係
static final int NOT_CROSS_ACCOUNT = 1;
static final int CROSS_ACCOUNT_SAME_INSTANCE = 2;
static final int CROSS_ACCOUNT_REMOTE_INSTANCE = 3;
int calcCrossAccountMode( @NonNull final SavedAccount timeline_account, @NonNull final SavedAccount action_account ){
if( ! timeline_account.host.equalsIgnoreCase( action_account.host ) ){
return CROSS_ACCOUNT_REMOTE_INSTANCE;
}else if( ! timeline_account.acct.equalsIgnoreCase( action_account.acct ) ){
return CROSS_ACCOUNT_SAME_INSTANCE;
}else{
return NOT_CROSS_ACCOUNT;
}
}
void openBoostFromAnotherAccount( @NonNull final SavedAccount timeline_account, final TootStatus status ){
if( status == null ) return;
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_boost )
, makeAccountListNonPseudo( log )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
@Override public void onAccountPicked( @NonNull SavedAccount action_account ){
performBoost(
ai
, ! ai.host.equalsIgnoreCase( access_info.host )
, true
action_account
, status
, calcCrossAccountMode( timeline_account, action_account )
, true
, false
, boost_complete_callback
);
@ -3248,19 +3266,18 @@ public class ActMain extends AppCompatActivity
} );
}
void openFavouriteFromAnotherAccount( @NonNull final SavedAccount access_info, final TootStatus status ){
void openFavouriteFromAnotherAccount( @NonNull final SavedAccount timeline_account, final TootStatus status ){
if( status == null ) return;
AccountPicker.pick( this, false, false
, getString( R.string.account_picker_favourite )
// , account_list_non_pseudo_same_instance
, makeAccountListNonPseudo( log )
, new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
@Override public void onAccountPicked( @NonNull SavedAccount action_account ){
performFavourite(
ai
, ! ai.host.equalsIgnoreCase( access_info.host )
, true
action_account
, status
, calcCrossAccountMode( timeline_account, action_account )
, true
, favourite_complete_callback
);
}
@ -3305,7 +3322,7 @@ public class ActMain extends AppCompatActivity
private boolean closeColumnSetting(){
if( pager_adapter != null ){
ColumnViewHolder vh = pager_adapter.getColumnViewHolder( pager.getCurrentItem() );
if( vh!=null && vh.isColumnSettingShown() ){
if( vh != null && vh.isColumnSettingShown() ){
vh.closeColumnSetting();
return true;
}

View File

@ -32,6 +32,7 @@ import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.CheckBox;
@ -74,6 +75,7 @@ import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.table.TagSet;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.MyClickableSpan;
import jp.juggler.subwaytooter.view.MyEditText;
@ -236,6 +238,16 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
super.onBackPressed();
}
@Override protected void onResume(){
super.onResume();
MyClickableSpan.link_callback = link_click_listener;
}
@Override protected void onPause(){
super.onPause();
MyClickableSpan.link_callback = null;
}
SharedPreferences pref;
ArrayList< PostAttachment > attachment_list;
AppState app_state;
@ -2025,7 +2037,13 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
View viewRoot = getLayoutInflater().inflate( R.layout.dlg_plugin_missing,null,false );
TextView tvText = (TextView)viewRoot. findViewById(R.id.tvText);
tvText.setText( Html.fromHtml(text));
LinkClickContext lcc = new LinkClickContext() {
@Override public AcctColor findAcctColor( String url ){
return null;
}
};
CharSequence sv = HTMLDecoder.decodeHTML( lcc,text,false,null );
tvText.setText(sv );
tvText.setMovementMethod( LinkMovementMethod.getInstance() );
TextView tvTitle = (TextView)viewRoot. findViewById(R.id.tvTitle);
@ -2051,4 +2069,16 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
}
}
final MyClickableSpan.LinkClickCallback link_click_listener = new MyClickableSpan.LinkClickCallback() {
@Override public void onClickLink( View view, LinkClickContext lcc, String url ){
if( url == null ) return;
// ブラウザで開く
try{
Intent intent = new Intent( Intent.ACTION_VIEW, Uri.parse( url ) );
startActivity( intent );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
};
}

View File

@ -6,6 +6,7 @@ import android.os.AsyncTask;
import android.os.Handler;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.support.annotation.NonNull;
import android.text.Spannable;
import android.text.TextUtils;
@ -131,22 +132,37 @@ class AppState {
//////////////////////////////////////////////////////
final HashSet< String > map_busy_fav = new HashSet<>();
private final HashSet< String > map_busy_fav = new HashSet<>();
boolean isBusyFav( SavedAccount account, TootStatus status ){
String busy_key = account.host + ":" + status.id;
return map_busy_fav.contains( busy_key );
}
boolean setBusyFav( SavedAccount account, TootStatus status ){
final String busy_key = account.acct +":" + status.uri;
return map_busy_fav.add( busy_key );
}
boolean resetBusyFav( SavedAccount account, TootStatus status ){
final String busy_key = account.acct +":" + status.uri;
return map_busy_fav.remove( busy_key );
}
//////////////////////////////////////////////////////
final HashSet< String > map_busy_boost = new HashSet<>();
private final HashSet< String > map_busy_boost = new HashSet<>();
boolean isBusyBoost( SavedAccount account, TootStatus status ){
String busy_key = account.host + ":" + status.id;
boolean isBusyBoost( @NonNull SavedAccount account, @NonNull TootStatus status ){
final String busy_key = account.acct +":" + status.uri;
return map_busy_boost.contains( busy_key );
}
boolean setBusyBoost( SavedAccount account, TootStatus status ){
final String busy_key = account.acct +":" + status.uri;
return map_busy_boost.add( busy_key );
}
boolean resetBusyBoost( SavedAccount account, TootStatus status ){
final String busy_key = account.acct +":" + status.uri;
return map_busy_boost.remove( busy_key );
}
//////////////////////////////////////////////////////
ArrayList< PostAttachment > attachment_list = null;

View File

@ -537,14 +537,14 @@ class Column implements StreamReader.Callback {
}
interface StatusEntryCallback {
void onIterate( TootStatus status );
boolean onIterate( SavedAccount account,TootStatus status );
}
// ブーストやお気に入りの更新に使うステータスを列挙する
void findStatus( SavedAccount target_account, long target_status_id, StatusEntryCallback callback ){
if( target_account.acct.equals( access_info.acct ) ){
for( int i = 0, ie = list_data.size() ; i < ie ; ++ i ){
Object data = list_data.get( i );
void findStatus( String target_instance, long target_status_id, StatusEntryCallback callback ){
if( access_info.host.equalsIgnoreCase( target_instance ) ){
boolean bChanged = false;
for( Object data: list_data ){
//
if( data instanceof TootNotification ){
data = ( (TootNotification) data ).status;
@ -554,17 +554,22 @@ class Column implements StreamReader.Callback {
//
TootStatus status = (TootStatus) data;
if( target_status_id == status.id ){
callback.onIterate( status );
if(callback.onIterate( access_info,status )){
bChanged = true;
}
}
//
TootStatus reblog = status.reblog;
if( reblog != null ){
if( target_status_id == reblog.id ){
callback.onIterate( reblog );
if( reblog != null && target_status_id == reblog.id ){
if(callback.onIterate( access_info,reblog )){
bChanged = true;
}
}
}
}
if(bChanged){
fireShowContent();
}
}
}

View File

@ -131,9 +131,9 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener {
}else{
activity.performBoost(
access_info
, false
, ! status.reblogged
, status
,ActMain.NOT_CROSS_ACCOUNT
, ! status.reblogged
, false
, bSimpleList ? activity.boost_complete_callback : null
);
@ -146,9 +146,9 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener {
}else{
activity.performFavourite(
access_info
, false
, ! status.favourited
, status
,ActMain.NOT_CROSS_ACCOUNT
, ! status.favourited
, bSimpleList ? activity.favourite_complete_callback : null
);
}

View File

@ -174,8 +174,9 @@ public class HTMLDecoder {
if( DEBUG_HTML_PARSER ) sb.append( "(end " ).append( tag ).append( ")" );
if( "br".equals( tag ) ) sb.append( '\n' );
if( "p".equals( tag ) ){
if( "br".equals( tag ) ) sb.append( '\n' );
if( "p".equals( tag ) || "li".equals( tag ) ){
if( sb.length() > 0 ){
if( sb.charAt( sb.length() - 1 ) != '\n' ) sb.append( '\n' );
sb.append( '\n' );

View File

@ -3,6 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:scrollbarStyle="outsideOverlay"
>
<LinearLayout
@ -18,6 +19,7 @@
android:id="@+id/tvTitle"
android:textStyle="bold"
android:textSize="20sp"
tools:text="tvTitle"
/>
<TextView
android:layout_width="match_parent"
@ -32,6 +34,7 @@
android:padding="12dp"
android:id="@+id/tvText"
android:textSize="16sp"
tools:text="tvText"
/>
</LinearLayout>
</ScrollView>

View File

@ -1,8 +1,7 @@
<html lang="en">
<ul>
<li>EmojionePicker <a href="https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker">https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker</a></li>
<li>SupportText Free <a href="https://play.google.com/store/apps/details?id=com.z589.supporttext">https://play.google.com/store/apps/details?id=com.z589.supporttext</a></li>
<li>ANdClip Free <a href="https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree">https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree</a></li>
<li>Contact Picker <a href="https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker">https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker</a></li>
<li>Facemark Generator Mushroom <a href="https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush">https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush</a></li>
</ul></html>
<html lang="en"><ul><li
>EmojionePicker <a href="https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker">https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker</a></li
><li>SupportText Free <a href="https://play.google.com/store/apps/details?id=com.z589.supporttext">https://play.google.com/store/apps/details?id=com.z589.supporttext</a></li
><li>ANdClip Free <a href="https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree">https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree</a></li
><li>Contact Picker <a href="https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker">https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker</a></li
><li>Facemark Generator Mushroom <a href="https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush">https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush</a></li
></ul></html>

View File

@ -1,11 +1,10 @@
<html lang="fr">
<ul>
<li>EmojionePicker <a href="https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker">https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker</a></li>
<li>SupportText Free <a href="https://play.google.com/store/apps/details?id=com.z589.supporttext">https://play.google.com/store/apps/details?id=com.z589.supporttext</a></li>
<li>ANdClip Free <a href="https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree">https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree</a></li>
<li>Contact Picker <a href="https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker">https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker</a></li>
<li>AntiSearchEngine <a href="https://play.google.com/store/apps/details?id=jp.Keshigoto.AntiSearchEngine">https://play.google.com/store/apps/details?id=jp.Keshigoto.AntiSearchEngine</a></li>
<li>Emoji Mush <a href="https://play.google.com/store/apps/details?id=jp.mstssk.emoji_mush">https://play.google.com/store/apps/details?id=jp.mstssk.emoji_mush</a></li>
<li>Facemark Generator Mushroom <a href="https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush">https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush</a></li>
<li>Tagifier <a href="https://play.google.com/store/apps/details?id=jp.wktk.tagifier">https://play.google.com/store/apps/details?id=jp.wktk.tagifier</a></li>
</ul></html>
<html lang="fr"><ul><li
>EmojionePicker <a href="https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker">https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker</a></li
><li>SupportText Free <a href="https://play.google.com/store/apps/details?id=com.z589.supporttext">https://play.google.com/store/apps/details?id=com.z589.supporttext</a></li
><li>ANdClip Free <a href="https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree">https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree</a></li
><li>Contact Picker <a href="https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker">https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker</a></li
><li>AntiSearchEngine <a href="https://play.google.com/store/apps/details?id=jp.Keshigoto.AntiSearchEngine">https://play.google.com/store/apps/details?id=jp.Keshigoto.AntiSearchEngine</a></li
><li>Emoji Mush <a href="https://play.google.com/store/apps/details?id=jp.mstssk.emoji_mush">https://play.google.com/store/apps/details?id=jp.mstssk.emoji_mush</a></li
><li>Facemark Generator Mushroom <a href="https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush">https://play.google.com/store/apps/details?id=jp.rgfx_currentdir_ozero.facegenmush</a></li
><li>Tagifier <a href="https://play.google.com/store/apps/details?id=jp.wktk.tagifier">https://play.google.com/store/apps/details?id=jp.wktk.tagifier</a></li
></ul></html>

View File

@ -1,9 +1,7 @@
<html lang="ja">
<ul>
<li>EmojionePicker <a href="https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker">https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker</a></li>
<li>SupportText Free <a href="https://play.google.com/store/apps/details?id=com.z589.supporttext">https://play.google.com/store/apps/details?id=com.z589.supporttext</a></li>
<li>ANdClip Free <a href="https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree">https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree</a></li>
<li>Contact Picker <a href="https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker">https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker</a></li>
<li>顔文字コピー <a href="https://play.google.com/store/apps/details?id=jp.neoscorp.android.kaomoji">https://play.google.com/store/apps/details?id=jp.neoscorp.android.kaomoji</a></li>
</ul>
</html>
<html lang="ja"><ul><li
>EmojionePicker <a href="https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker">https://play.google.com/store/apps/details?id=com.mastopane.emojionepicker</a></li
><li>SupportText Free <a href="https://play.google.com/store/apps/details?id=com.z589.supporttext">https://play.google.com/store/apps/details?id=com.z589.supporttext</a></li
><li>ANdClip Free <a href="https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree">https://play.google.com/store/apps/details?id=com.amazing_create.android.andclipfree</a></li
><li>Contact Picker <a href="https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker">https://play.google.com/store/apps/details?id=jp.narikitic.android.mushroom.contactPicker</a></li
><li>顔文字コピー <a href="https://play.google.com/store/apps/details?id=jp.neoscorp.android.kaomoji">https://play.google.com/store/apps/details?id=jp.neoscorp.android.kaomoji</a></li
></ul></html>