アプリ設定にアプリデータのエクスポートとインポートを追加

This commit is contained in:
tateisu 2017-05-18 14:42:14 +09:00
parent 0c7b634fd7
commit 7030d30a92
16 changed files with 943 additions and 122 deletions

View File

@ -161,6 +161,17 @@
<activity android:name="com.yasesprox.android.transcommusdk.CreateAccountActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize"></activity>
<activity android:name="com.yasesprox.android.transcommusdk.TranslateActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize"></activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="jp.juggler.subwaytooter.FileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_path" />
</provider>
</application>
</manifest>

View File

@ -1,17 +1,23 @@
package jp.juggler.subwaytooter;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.annotation.Nullable;
import android.support.v4.content.FileProvider;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.JsonWriter;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
@ -27,11 +33,13 @@ import com.jrummyapps.android.colorpicker.ColorPickerDialog;
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.FileWriterWithEncoding;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class ActAppSetting extends AppCompatActivity
@ -40,6 +48,7 @@ public class ActAppSetting extends AppCompatActivity
, View.OnClickListener
, ColorPickerDialogListener, TextWatcher
{
static final LogCategory log = new LogCategory( "ActAppSetting" );
public static void open( ActMain activity, int request_code ){
activity.startActivityForResult( new Intent( activity, ActAppSetting.class ), request_code );
@ -103,9 +112,7 @@ public class ActAppSetting extends AppCompatActivity
private void initUI(){
setContentView( R.layout.act_app_setting );
Styler.fixHorizontalPadding(findViewById( R.id.svContent ));
Styler.fixHorizontalPadding( findViewById( R.id.svContent ) );
swDontConfirmBeforeCloseColumn = (Switch) findViewById( R.id.swDontConfirmBeforeCloseColumn );
swDontConfirmBeforeCloseColumn.setOnCheckedChangeListener( this );
@ -213,6 +220,8 @@ public class ActAppSetting extends AppCompatActivity
findViewById( R.id.btnTabDividerColorReset ).setOnClickListener( this );
findViewById( R.id.btnTimelineFontEdit ).setOnClickListener( this );
findViewById( R.id.btnTimelineFontReset ).setOnClickListener( this );
findViewById( R.id.btnSettingExport ).setOnClickListener( this );
findViewById( R.id.btnSettingImport ).setOnClickListener( this );
ivFooterToot = (ImageView) findViewById( R.id.ivFooterToot );
ivFooterMenu = (ImageView) findViewById( R.id.ivFooterMenu );
@ -223,7 +232,7 @@ public class ActAppSetting extends AppCompatActivity
etColumnWidth = (EditText) findViewById( R.id.etColumnWidth );
etMediaThumbHeight = (EditText) findViewById( R.id.etMediaThumbHeight );
tvTimelineFontUrl= (TextView) findViewById( R.id.tvTimelineFontUrl );
tvTimelineFontUrl = (TextView) findViewById( R.id.tvTimelineFontUrl );
etColumnWidth.addTextChangedListener( this );
etMediaThumbHeight.addTextChangedListener( this );
@ -264,16 +273,14 @@ public class ActAppSetting extends AppCompatActivity
etColumnWidth.setText( pref.getString( Pref.KEY_COLUMN_WIDTH, "" ) );
etMediaThumbHeight.setText( pref.getString( Pref.KEY_MEDIA_THUMB_HEIGHT, "" ) );
timeline_font = pref.getString( Pref.KEY_TIMELINE_FONT, "" );
timeline_font = pref.getString( Pref.KEY_TIMELINE_FONT, "" );
load_busy = false;
showFooterColor();
showTimelineFont();
}
private void saveUIToData(){
if( load_busy ) return;
pref.edit()
@ -383,29 +390,46 @@ public class ActAppSetting extends AppCompatActivity
intent.addCategory( Intent.CATEGORY_OPENABLE );
intent.setType( "*/*" );
startActivityForResult( intent, REQUEST_CODE_TIMELINE_FONT );
}catch(Throwable ex){
Utils.showToast( this,ex,"could not open picker for font/*" );
}catch( Throwable ex ){
Utils.showToast( this, ex, "could not open picker for font/*" );
}
break;
case R.id.btnSettingExport:
exportAppData();
break;
case R.id.btnSettingImport:
importAppData();
break;
}
}
static final int REQUEST_CODE_TIMELINE_FONT = 1;
static final int REQUEST_CODE_APP_DATA_EXPORT = 2;
static final int REQUEST_CODE_APP_DATA_IMPORT = 3;
@Override protected void onActivityResult( int requestCode, int resultCode, Intent data ){
if( resultCode == RESULT_OK && requestCode == REQUEST_CODE_TIMELINE_FONT){
if( resultCode == RESULT_OK && requestCode == REQUEST_CODE_TIMELINE_FONT ){
if( data != null ){
Uri uri = data.getData();
if( uri != null ){
getContentResolver().takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION );
saveTimelineFont( uri);
saveTimelineFont( uri );
}
}
}else if( resultCode == RESULT_OK && requestCode == REQUEST_CODE_APP_DATA_IMPORT ){
if( data != null ){
Uri uri = data.getData();
if( uri != null ){
getContentResolver().takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION );
importAppData( false, uri );
}
}
}
super.onActivityResult( requestCode, resultCode, data );
}
void openColorPicker( int id, int color ){
ColorPickerDialog.Builder builder = ColorPickerDialog.newBuilder()
.setDialogType( ColorPickerDialog.TYPE_CUSTOM )
@ -501,19 +525,18 @@ public class ActAppSetting extends AppCompatActivity
saveUIToData();
}
private void showTimelineFont(){
try{
if( ! TextUtils.isEmpty( timeline_font ) ){
tvTimelineFontUrl.setTypeface( Typeface.DEFAULT );
Typeface face = Typeface.createFromFile( timeline_font );
tvTimelineFontUrl.setTypeface(face );
tvTimelineFontUrl.setTypeface( face );
tvTimelineFontUrl.setText( timeline_font );
return;
}
}catch(Throwable ex){
ex.printStackTrace( );
}catch( Throwable ex ){
ex.printStackTrace();
}
// fallback
tvTimelineFontUrl.setText( getString( R.string.not_selected ) );
@ -529,18 +552,17 @@ public class ActAppSetting extends AppCompatActivity
//noinspection ResultOfMethodCallIgnored
dir.mkdir();
File tmp_file = new File( dir, TIMELINE_FONT_FILE_NAME +".tmp" );
File tmp_file = new File( dir, TIMELINE_FONT_FILE_NAME + ".tmp" );
InputStream is = getContentResolver().openInputStream( uri );
if( is == null ){
Utils.showToast( this, false, "openInputStream returns null.");
Utils.showToast( this, false, "openInputStream returns null." );
return;
}
try{
FileOutputStream os = new FileOutputStream( tmp_file );
try{
IOUtils.copy( is,os );
IOUtils.copy( is, os );
}finally{
IOUtils.closeQuietly( os );
}
@ -549,16 +571,15 @@ public class ActAppSetting extends AppCompatActivity
IOUtils.closeQuietly( is );
}
Typeface face = Typeface.createFromFile( tmp_file );
if( face == null ){
Utils.showToast( this, false, "Typeface.createFromFile() failed.");
Utils.showToast( this, false, "Typeface.createFromFile() failed." );
return;
}
File file = new File( dir, TIMELINE_FONT_FILE_NAME );
if(!tmp_file.renameTo( file ) ){
Utils.showToast( this, false, "File operation failed.");
if( ! tmp_file.renameTo( file ) ){
Utils.showToast( this, false, "File operation failed." );
return;
}
@ -566,10 +587,113 @@ public class ActAppSetting extends AppCompatActivity
saveUIToData();
showTimelineFont();
}catch(Throwable ex){
ex.printStackTrace( );
Utils.showToast( this,ex,"saveTimelineFont failed.");
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( this, ex, "saveTimelineFont failed." );
}
}
private void exportAppData(){
final ProgressDialog progress = new ProgressDialog( this );
final AsyncTask< Void, String, File > task = new AsyncTask< Void, String, File >() {
@Override protected File doInBackground( Void... params ){
try{
File cache_dir = getCacheDir();
//noinspection ResultOfMethodCallIgnored
cache_dir.mkdir();
File file = new File( cache_dir, "SubwayTooter." + android.os.Process.myPid() + "." + android.os.Process.myTid() + ".json" );
FileWriterWithEncoding w = new FileWriterWithEncoding( file, "UTF-8" );
try{
JsonWriter jw = new JsonWriter( w );
AppDataExporter.encodeAppData( ActAppSetting.this, jw );
jw.flush();
}finally{
IOUtils.closeQuietly( w );
}
return file;
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( ActAppSetting.this, ex, "exportAppData failed." );
}
return null;
}
@Override protected void onCancelled( File result ){
super.onCancelled( result );
}
@Override protected void onPostExecute( File result ){
progress.dismiss();
if( isCancelled() || result == null ){
// cancelled.
return;
}
try{
Uri uri = FileProvider.getUriForFile( ActAppSetting.this, "jp.juggler.subwaytooter.FileProvider", result );
Intent intent = new Intent( Intent.ACTION_SEND );
intent.setType( getContentResolver().getType( uri ) );
intent.putExtra( Intent.EXTRA_SUBJECT, "SubwayTooter app data" );
intent.putExtra( Intent.EXTRA_STREAM, uri );
intent.addFlags( Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION );
startActivityForResult( intent, REQUEST_CODE_APP_DATA_EXPORT );
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( ActAppSetting.this, ex, "exportAppData failed." );
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( true );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
private void importAppData(){
try{
Intent intent = new Intent( Intent.ACTION_OPEN_DOCUMENT );
intent.addCategory( Intent.CATEGORY_OPENABLE );
intent.setType( "*/*" );
startActivityForResult( intent, REQUEST_CODE_APP_DATA_IMPORT );
}catch( Throwable ex ){
Utils.showToast( this, ex, "importAppData(1) failed." );
}
}
private void importAppData( boolean bConfirm, final Uri uri ){
String type = getContentResolver().getType( uri );
log.d( "importAppData type=%s", type );
if( ! bConfirm ){
new AlertDialog.Builder( this )
.setMessage( getString( R.string.app_data_import_confirm ) )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
importAppData( true, uri );
}
} )
.show();
return;
}
Intent data = new Intent();
data.setData( uri );
setResult( ActMain.RESULT_APP_DATA_IMPORT, data );
finish();
}
}

View File

@ -24,6 +24,7 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.JsonReader;
import android.view.Gravity;
import android.view.View;
import android.support.design.widget.NavigationView;
@ -40,9 +41,16 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import org.apache.commons.io.IOUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -306,6 +314,10 @@ public class ActMain extends AppCompatActivity
return false;
}
// リザルト
static final int RESULT_APP_DATA_IMPORT = RESULT_FIRST_USER ;
// リクエスト
static final int REQUEST_CODE_COLUMN_LIST = 1;
static final int REQUEST_CODE_ACCOUNT_SETTING = 2;
static final int REQUEST_APP_ABOUT = 3;
@ -383,11 +395,19 @@ public class ActMain extends AppCompatActivity
if( requestCode == REQUEST_APP_SETTING ){
showFooterColor();
if( resultCode == RESULT_APP_DATA_IMPORT ){
if( data != null ){
importAppData( data.getData() );
}
}
}
super.onActivityResult( requestCode, resultCode, data );
}
@Override
public void onBackPressed(){
@ -3211,4 +3231,153 @@ public class ActMain extends AppCompatActivity
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
private void importAppData( final Uri uri ){
// remove all columns
{
if( pager_adapter != null ){
pager.setAdapter( null );
}
for( Column c : app_state.column_list ){
c.dispose();
}
app_state.column_list.clear();
if( pager_adapter != null ){
pager.setAdapter( pager_adapter );
}else{
tablet_pager_adapter.notifyDataSetChanged();
}
}
final ProgressDialog progress = new ProgressDialog( this );
final AsyncTask< Void, String, ArrayList<Column> > task = new AsyncTask< Void, String, ArrayList<Column> >() {
void setProgressMessage(final String sv){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setMessage(sv);
}
} );
}
@Override protected ArrayList<Column> doInBackground( Void... params ){
try{
setProgressMessage( "import data to local storage..." );
File cache_dir = getCacheDir();
//noinspection ResultOfMethodCallIgnored
cache_dir.mkdir();
File file = new File( cache_dir, "SubwayTooter." + android.os.Process.myPid() + "." + android.os.Process.myTid() + ".json" );
// ローカルファイルにコピーする
InputStream is = getContentResolver().openInputStream( uri );
if( is == null ){
Utils.showToast( ActMain.this, true,"openInputStream failed.");
return null;
}
try{
FileOutputStream os = new FileOutputStream( file );
try{
IOUtils.copy( is,os );
}finally{
IOUtils.closeQuietly( os );
}
}finally{
IOUtils.closeQuietly( is );
}
// 通知サービスを止める
setProgressMessage( "reset Notification..." );
{
AlarmService.mBusyAppDataImportBefore.set(true);
AlarmService.mBusyAppDataImportAfter.set(true);
Intent intent = new Intent(ActMain.this,AlarmService.class);
intent.setAction( AlarmService.ACTION_APP_DATA_IMPORT_BEFORE );
startService( intent );
while( AlarmService.mBusyAppDataImportBefore.get() ){
Thread.sleep( 100L );
}
}
// JSONを読みだす
setProgressMessage( "reading app data..." );
Reader r =new InputStreamReader(new FileInputStream(file),"UTF-8");
try{
JsonReader reader = new JsonReader( r );
return AppDataExporter.decodeAppData( ActMain.this, reader );
}finally{
IOUtils.closeQuietly( r );
}
}catch( Throwable ex ){
ex.printStackTrace();
Utils.showToast( ActMain.this, ex, "importAppData failed." );
}
return null;
}
@Override protected void onCancelled( ArrayList<Column> result ){
super.onCancelled( result );
}
@Override protected void onPostExecute( ArrayList<Column> result ){
progress.dismiss();
try{
getWindow().clearFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );
}catch(Throwable ignored){
}
if( isCancelled() || result == null ){
// cancelled.
return;
}
{
if( pager_adapter != null ){
pager.setAdapter( null );
}
app_state.column_list.clear();
app_state.column_list.addAll(result );
app_state.saveColumnList();
if( pager_adapter != null ){
pager.setAdapter( pager_adapter );
}else{
tablet_pager_adapter.notifyDataSetChanged();
}
}
// 通知サービスをリスタート
{
Intent intent = new Intent(ActMain.this,AlarmService.class);
intent.setAction( AlarmService.ACTION_APP_DATA_IMPORT_AFTER );
startService( intent );
}
updateColumnStrip();
}
};
try{
getWindow().addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );
}catch(Throwable ignored){
}
progress.setIndeterminate( true );
progress.setCancelable( false );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
}

View File

@ -55,6 +55,11 @@ public class AlarmService extends IntentService {
private static final String EXTRA_DB_ID = "db_id";
private static final String ACTION_DATA_DELETED = "data_deleted";
public static final String ACTION_APP_DATA_IMPORT_BEFORE = "app_data_import_before";
public static final AtomicBoolean mBusyAppDataImportBefore = new AtomicBoolean( false );
public static final AtomicBoolean mBusyAppDataImportAfter = new AtomicBoolean( false );
public static final String ACTION_APP_DATA_IMPORT_AFTER = "app_data_import_after";
public AlarmService(){
// name: Used to name the worker thread, important only for debugging.
super( "AlarmService" );
@ -98,11 +103,34 @@ public class AlarmService extends IntentService {
// 同期処理を行って良い
@Override protected void onHandleIntent( @Nullable Intent intent ){
if( intent != null ){
String action = intent.getAction();
log.d( "onHandleIntent action=%s", action );
if( ACTION_APP_DATA_IMPORT_BEFORE.equals( action ) ){
alarm_manager.cancel( pi_next );
for( SavedAccount a : SavedAccount.loadAccountList( log ) ){
try{
String notification_tag = Long.toString( a.db_id );
notification_manager.cancel( notification_tag, NOTIFICATION_ID );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
mBusyAppDataImportBefore.set( false );
return;
}else if( ACTION_APP_DATA_IMPORT_AFTER.equals( action ) ){
mBusyAppDataImportAfter.set( false );
NotificationTracking.resetPostAll();
}
}
if( mBusyAppDataImportAfter.get() ) return;
ArrayList< SavedAccount > account_list = SavedAccount.loadAccountList( log );
if( intent != null ){
String action = intent.getAction();
log.d( "onHandleIntent action=%s", action );
if( ACTION_DATA_DELETED.equals( action ) ){
deleteCacheData( intent.getLongExtra( EXTRA_DB_ID, - 1L ) );
@ -122,7 +150,7 @@ public class AlarmService extends IntentService {
}else if( Intent.ACTION_MY_PACKAGE_REPLACED.equals( action ) ){
NotificationTracking.resetPostAll();
}else if( ACTION_NOTIFICATION_DELETE.equals( action ) ){
log.d( "Notification deleted!" );
long db_id = received_intent.getLongExtra( EXTRA_DB_ID, 0L );
@ -147,8 +175,6 @@ public class AlarmService extends IntentService {
}
}
final AtomicBoolean bAlarmRequired = new AtomicBoolean( false );
final HashSet< String > muted_app = MutedApp.getNameSet();
final HashSet< String > muted_word = MutedWord.getNameSet();

View File

@ -0,0 +1,407 @@
package jp.juggler.subwaytooter;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.provider.BaseColumns;
import android.support.annotation.NonNull;
import android.util.JsonReader;
import android.util.JsonToken;
import android.util.JsonWriter;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.MutedWord;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
public class AppDataExporter {
static final LogCategory log = new LogCategory( "AppDataExporter" );
private static void writeJSONObject( @NonNull JsonWriter writer, @NonNull JSONObject src ) throws IOException, JSONException{
writer.beginObject();
for( Iterator< String > it = src.keys() ; it.hasNext() ; ){
String k = it.next();
if( src.isNull( k ) ){
writer.name( k );
writer.nullValue();
}else{
Object o = src.get( k );
if( o instanceof String ){
writer.name( k );
writer.value( (String) o );
}else if( o instanceof Boolean ){
writer.name( k );
writer.value( (Boolean) o );
}else if( o instanceof Number ){
writer.name( k );
writer.value( (Number) o );
}else{
throw new RuntimeException( String.format( Locale.JAPAN, "bad data type: JSONObject key =%s", k ) );
}
}
}
writer.endObject();
}
private static JSONObject readJsonObject( JsonReader reader ) throws IOException, JSONException{
JSONObject dst = new JSONObject();
reader.beginObject();
while( reader.hasNext() ){
String name = reader.nextName();
JsonToken token = reader.peek();
switch( token ){
case NULL:
reader.nextNull();
break;
case STRING:
dst.put( name, reader.nextString() );
break;
case BOOLEAN:
dst.put( name, reader.nextBoolean() );
break;
case NUMBER:
dst.put( name, reader.nextDouble() );
break;
default:
throw new RuntimeException( String.format( Locale.JAPAN, "bad data type: %s key =%s", token, name ) );
}
}
reader.endObject();
return dst;
}
private static void writeFromTable( @NonNull JsonWriter writer, String json_key, String table ) throws IOException{
writer.name( json_key );
writer.beginArray();
Cursor cursor = App1.getDB().query( table, null, null, null, null, null, null );
try{
ArrayList< String > names = new ArrayList<>();
int column_count = cursor.getColumnCount();
for( int i = 0 ; i < column_count ; ++ i ){
names.add( cursor.getColumnName( i ) );
}
while( cursor.moveToNext() ){
writer.beginObject();
for( int i = 0 ; i < column_count ; ++ i ){
switch( cursor.getType( i ) ){
case Cursor.FIELD_TYPE_NULL:
writer.name( names.get( i ) );
writer.nullValue();
break;
case Cursor.FIELD_TYPE_INTEGER:
writer.name( names.get( i ) );
writer.value( cursor.getLong( i ) );
break;
case Cursor.FIELD_TYPE_STRING:
writer.name( names.get( i ) );
writer.value( cursor.getString( i ) );
break;
case Cursor.FIELD_TYPE_FLOAT:
Double d = cursor.getDouble( i );
if( Double.isNaN( d ) || Double.isInfinite( d ) ){
log.w( "column %s is nan or infinite value.", names.get( i ) );
}else{
writer.name( names.get( i ) );
writer.value( d );
}
break;
case Cursor.FIELD_TYPE_BLOB:
log.w( "column %s is blob.", names.get( i ) );
break;
}
}
writer.endObject();
}
}finally{
cursor.close();
}
writer.endArray();
}
private static void importTable( JsonReader reader, String table, HashMap< Long, Long > id_map ) throws IOException{
SQLiteDatabase db = App1.getDB();
if( table.equals( SavedAccount.table ) ){
SavedAccount.onDBDelete( db );
SavedAccount.onDBCreate( db );
}
boolean bOK = false;
db.execSQL( "BEGIN TRANSACTION" );
try{
db.execSQL( "delete from " + table );
ContentValues cv = new ContentValues();
reader.beginArray();
while( reader.hasNext() ){
long old_id = - 1L;
cv.clear();
reader.beginObject();
while( reader.hasNext() ){
String name = reader.nextName();
if( BaseColumns._ID.equals( name ) ){
old_id = reader.nextLong();
continue;
}
JsonToken token = reader.peek();
switch( token ){
case NULL:
reader.skipValue();
cv.putNull( name );
break;
case BOOLEAN:
cv.put( name, reader.nextBoolean() ? 1 : 0 );
break;
case NUMBER:
cv.put( name, reader.nextLong() );
break;
case STRING:
cv.put( name, reader.nextString() );
break;
default:
reader.skipValue();
// 無視する
break;
}
}
reader.endObject();
long new_id = db.insert( table, null, cv );
if( id_map != null ) id_map.put( old_id, new_id );
}
reader.endArray();
bOK = true;
}catch( Throwable ex ){
ex.printStackTrace();
log.e( ex, "saveList failed." );
}
if( bOK ){
db.execSQL( "COMMIT TRANSACTION" );
}else{
db.execSQL( "ROLLBACK TRANSACTION" );
}
}
private static void writePref( JsonWriter writer, SharedPreferences pref ) throws IOException{
writer.beginObject();
for( Map.Entry< String, ? > entry : pref.getAll().entrySet() ){
String k = entry.getKey();
Object v = entry.getValue();
writer.name( k );
if( v == null ){
writer.nullValue();
}else if( v instanceof String ){
writer.value( (String) v );
}else if( v instanceof Boolean ){
writer.value( (Boolean) v );
}else if( v instanceof Number ){
writer.value( (Number) v );
}else{
throw new RuntimeException( String.format( Locale.JAPAN, "writePref. bad data type: Preference key =%s", k ) );
}
}
writer.endObject();
}
private static void importPref( JsonReader reader, SharedPreferences pref ) throws IOException{
SharedPreferences.Editor e = pref.edit();
reader.beginObject();
while( reader.hasNext() ){
String k = reader.nextName();
if( k == null ){
throw new RuntimeException( "importPref: name is null" );
}
JsonToken token = reader.peek();
if( token == JsonToken.NULL ){
reader.nextNull();
e.remove( k );
continue;
}
switch( k ){
// boolean
case Pref.KEY_DONT_CONFIRM_BEFORE_CLOSE_COLUMN:
case Pref.KEY_PRIOR_LOCAL_URL:
case Pref.KEY_DISABLE_FAST_SCROLLER:
case Pref.KEY_SIMPLE_LIST:
case Pref.KEY_NOTIFICATION_SOUND:
case Pref.KEY_NOTIFICATION_VIBRATION:
case Pref.KEY_NOTIFICATION_LED:
case Pref.KEY_EXIT_APP_WHEN_CLOSE_PROTECTED_COLUMN:
case Pref.KEY_SHOW_FOLLOW_BUTTON_IN_BUTTON_BAR:
case Pref.KEY_DONT_ROUND:
case Pref.KEY_DONT_USE_STREAMING:
case Pref.KEY_DONT_REFRESH_ON_RESUME:
case Pref.KEY_DONT_SCREEN_OFF:
case Pref.KEY_DISABLE_TABLET_MODE:
boolean bv = reader.nextBoolean();
e.putBoolean( k, bv );
break;
// int
case Pref.KEY_BACK_BUTTON_ACTION:
case Pref.KEY_UI_THEME:
case Pref.KEY_RESIZE_IMAGE:
case Pref.KEY_REFRESH_AFTER_TOOT:
case Pref.KEY_FOOTER_BUTTON_BG_COLOR:
case Pref.KEY_FOOTER_BUTTON_FG_COLOR:
case Pref.KEY_FOOTER_TAB_BG_COLOR:
case Pref.KEY_FOOTER_TAB_DIVIDER_COLOR:
int iv = reader.nextInt();
e.putInt( k, iv );
break;
// string
case Pref.KEY_COLUMN_WIDTH:
case Pref.KEY_MEDIA_THUMB_HEIGHT:
String sv = reader.nextString();
e.putString( k, sv );
break;
// force reset
default:
case Pref.KEY_TIMELINE_FONT:
reader.skipValue();
e.remove( k );
break;
}
}
reader.endObject();
e.apply();
}
private static final String KEY_PREF = "pref";
private static final String KEY_ACCOUNT = "account";
private static final String KEY_COLUMN = "column";
private static final String KEY_ACCT_COLOR = "acct_color";
private static final String KEY_MUTED_APP = "muted_app";
private static final String KEY_MUTED_WORD = "muted_word";
static void encodeAppData( Context context, JsonWriter writer )
throws IOException, JSONException{
writer.setIndent( " " );
writer.beginObject();
AppState app_state = App1.getAppState( context );
//////////////////////////////////////
{
writer.name( KEY_PREF );
writePref( writer, app_state.pref );
}
//////////////////////////////////////
writeFromTable( writer, KEY_ACCOUNT, SavedAccount.table );
writeFromTable( writer, KEY_ACCT_COLOR, AcctColor.table );
writeFromTable( writer, KEY_MUTED_APP, MutedApp.table );
writeFromTable( writer, KEY_MUTED_WORD, MutedWord.table );
//////////////////////////////////////
{
writer.name( KEY_COLUMN );
writer.beginArray();
for( Column column : app_state.column_list ){
JSONObject dst = new JSONObject();
column.encodeJSON( dst, 0 );
writeJSONObject( writer, dst );
}
writer.endArray();
}
writer.endObject();
}
static ArrayList< Column > decodeAppData( Context context, JsonReader reader )
throws IOException, JSONException{
ArrayList< Column > result = null;
AppState app_state = App1.getAppState( context );
reader.beginObject();
@SuppressLint("UseSparseArrays")
HashMap< Long, Long > account_id_map = new HashMap<>();
while( reader.hasNext() ){
String name = reader.nextName();
if( KEY_PREF.equals( name ) ){
importPref( reader, app_state.pref );
}else if( KEY_ACCOUNT.equals( name ) ){
importTable( reader, SavedAccount.table, account_id_map );
}else if( KEY_ACCT_COLOR.equals( name ) ){
importTable( reader, AcctColor.table, null );
AcctColor.clearMemoryCache();
}else if( KEY_MUTED_APP.equals( name ) ){
importTable( reader, MutedApp.table, null );
}else if( KEY_MUTED_WORD.equals( name ) ){
importTable( reader, MutedWord.table, null );
}else if( KEY_COLUMN.equals( name ) ){
result = importColumn( app_state, reader, account_id_map );
}
}
if( result == null ){
throw new RuntimeException( "import data does not includes column list!" );
}
return result;
}
private static @NonNull
ArrayList< Column > importColumn( AppState app_state, JsonReader reader, HashMap< Long, Long > id_map ) throws IOException, JSONException{
ArrayList< Column > result = new ArrayList<>();
reader.beginArray();
while( reader.hasNext() ){
JSONObject item = readJsonObject( reader );
long old_id = item.optLong( Column.KEY_ACCOUNT_ROW_ID, - 1L );
Long new_id = id_map.get( old_id );
if( new_id == null ){
throw new RuntimeException( "importColumn: can't convert account id" );
}
item.put( Column.KEY_ACCOUNT_ROW_ID, (long) new_id );
result.add( new Column( app_state, item ) );
}
reader.endArray();
return result;
}
}

View File

@ -108,7 +108,7 @@ class Column implements StreamReader.Callback {
static final String PATH_SEARCH = "/api/v1/search?q=%s"; // 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts
private static final String PATH_INSTANCE = "/api/v1/instance";
private static final String KEY_ACCOUNT_ROW_ID = "account_id";
static final String KEY_ACCOUNT_ROW_ID = "account_id";
static final String KEY_TYPE = "type";
static final String KEY_DONT_CLOSE = "dont_close";
private static final String KEY_WITH_ATTACHMENT = "with_attachment";

View File

@ -18,6 +18,7 @@ public class Pref {
static final int RAT_DONT_REFRESH = 2;
static final String KEY_DONT_CONFIRM_BEFORE_CLOSE_COLUMN = "DontConfirmBeforeCloseColumn";
static final String KEY_BACK_BUTTON_ACTION = "back_button_action";
static final String KEY_PRIOR_LOCAL_URL = "prior_local_url";
static final String KEY_DISABLE_FAST_SCROLLER = "disable_fast_scroller";

View File

@ -5,6 +5,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.AppDataExporter;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -12,14 +13,16 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.LruCache;
import android.text.TextUtils;
import android.util.JsonWriter;
import java.io.IOException;
import java.util.Locale;
public class AcctColor {
private static final LogCategory log = new LogCategory( "AcctColor" );
private static final String table = "acct_color";
public static final String table = "acct_color";
private static final String COL_TIME_SAVE = "time_save";
private static final String COL_ACCT = "ac"; //@who@host ascii文字の大文字小文字は(sqliteにより)同一視される
private static final String COL_COLOR_FG = "cf"; // 未設定なら0それ以外は色
@ -69,9 +72,9 @@ public class AcctColor {
}
public void save( long now ){
acct = acct.toLowerCase( Locale.ENGLISH );
try{
ContentValues cv = new ContentValues();
cv.put( COL_TIME_SAVE, now );
@ -112,7 +115,7 @@ public class AcctColor {
if( cursor != null ){
try{
if( cursor.moveToNext() ){
dst = new AcctColor(acct);
dst = new AcctColor( acct );
int idx;
idx = cursor.getColumnIndex( COL_COLOR_FG );
@ -135,7 +138,7 @@ public class AcctColor {
ex.printStackTrace();
log.e( ex, "load failed." );
}
log.d("lruCache size=%s,hit=%s,miss=%s",mMemoryCache.size(),mMemoryCache.hitCount(),mMemoryCache.missCount() );
log.d( "lruCache size=%s,hit=%s,miss=%s", mMemoryCache.size(), mMemoryCache.hitCount(), mMemoryCache.missCount() );
dst = new AcctColor( acct );
mMemoryCache.put( acct, dst );
return dst;
@ -157,4 +160,8 @@ public class AcctColor {
public static boolean hasColorBackground( @Nullable AcctColor ac ){
return ac != null && ac.color_bg != 0;
}
public static void clearMemoryCache(){
mMemoryCache.evictAll ();
}
}

View File

@ -3,22 +3,25 @@ package jp.juggler.subwaytooter.table;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.JsonWriter;
import java.io.IOException;
import java.util.HashSet;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.AppDataExporter;
import jp.juggler.subwaytooter.util.LogCategory;
public class MutedApp {
private static final LogCategory log = new LogCategory( "MutedApp" );
private static final String table = "app_mute";
public static final String table = "app_mute";
public static final String COL_NAME = "name";
private static final String COL_TIME_SAVE = "time_save";
public static void onDBCreate( SQLiteDatabase db ){
log.d("onDBCreate!");
log.d( "onDBCreate!" );
db.execSQL(
"create table if not exists " + table
+ "(_id INTEGER PRIMARY KEY"
@ -32,7 +35,7 @@ public class MutedApp {
}
public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
if(oldVersion < 6 && newVersion >= 6){
if( oldVersion < 6 && newVersion >= 6 ){
onDBCreate( db );
}
}
@ -53,62 +56,62 @@ public class MutedApp {
}
public static Cursor createCursor(){
return App1.getDB().query( table, null,null,null, null, null, COL_NAME+" asc" );
return App1.getDB().query( table, null, null, null, null, null, COL_NAME + " asc" );
}
public static void delete( String name ){
try{
App1.getDB().delete( table, COL_NAME+"=?",new String[]{ name });
App1.getDB().delete( table, COL_NAME + "=?", new String[]{ name } );
}catch( Throwable ex ){
log.e( ex, "delete failed." );
}
}
public static HashSet<String> getNameSet(){
HashSet<String> dst = new HashSet<>();
public static HashSet< String > getNameSet(){
HashSet< String > dst = new HashSet<>();
try{
Cursor cursor = App1.getDB().query( table, null,null,null, null, null, null);
Cursor cursor = App1.getDB().query( table, null, null, null, null, null, null );
if( cursor != null ){
try{
int idx_name = cursor.getColumnIndex( COL_NAME );
while(cursor.moveToNext()){
String s = cursor.getString(idx_name);
dst.add( s);
while( cursor.moveToNext() ){
String s = cursor.getString( idx_name );
dst.add( s );
}
}finally{
cursor.close();
}
}
}catch(Throwable ex){
ex.printStackTrace( );
}catch( Throwable ex ){
ex.printStackTrace();
}
return dst;
}
// private static final String[] isMuted_projection = new String[]{COL_NAME};
// private static final String isMuted_where = COL_NAME+"=?";
// private static final ThreadLocal<String[]> isMuted_where_arg = new ThreadLocal<String[]>() {
// @Override protected String[] initialValue() {
// return new String[1];
// }
// };
// public static boolean isMuted( String app_name ){
// if( app_name == null ) return false;
// try{
// String[] where_arg = isMuted_where_arg.get();
// where_arg[0] = app_name;
// Cursor cursor = App1.getDB().query( table, isMuted_projection,isMuted_where , where_arg, null, null, null );
// try{
// if( cursor.moveToFirst() ){
// return true;
// }
// }finally{
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// }
// return false;
// }
// private static final String[] isMuted_projection = new String[]{COL_NAME};
// private static final String isMuted_where = COL_NAME+"=?";
// private static final ThreadLocal<String[]> isMuted_where_arg = new ThreadLocal<String[]>() {
// @Override protected String[] initialValue() {
// return new String[1];
// }
// };
// public static boolean isMuted( String app_name ){
// if( app_name == null ) return false;
// try{
// String[] where_arg = isMuted_where_arg.get();
// where_arg[0] = app_name;
// Cursor cursor = App1.getDB().query( table, isMuted_projection,isMuted_where , where_arg, null, null, null );
// try{
// if( cursor.moveToFirst() ){
// return true;
// }
// }finally{
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// }
// return false;
// }
}

View File

@ -3,22 +3,25 @@ package jp.juggler.subwaytooter.table;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.JsonWriter;
import java.io.IOException;
import java.util.HashSet;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.AppDataExporter;
import jp.juggler.subwaytooter.util.LogCategory;
public class MutedWord {
private static final LogCategory log = new LogCategory( "MutedWord" );
private static final String table = "word_mute";
public static final String table = "word_mute";
public static final String COL_NAME = "name";
private static final String COL_TIME_SAVE = "time_save";
public static void onDBCreate( SQLiteDatabase db ){
log.d("onDBCreate!");
log.d( "onDBCreate!" );
db.execSQL(
"create table if not exists " + table
+ "(_id INTEGER PRIMARY KEY"
@ -32,7 +35,7 @@ public class MutedWord {
}
public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
if(oldVersion < 11 && newVersion >= 11){
if( oldVersion < 11 && newVersion >= 11 ){
onDBCreate( db );
}
}
@ -53,62 +56,62 @@ public class MutedWord {
}
public static Cursor createCursor(){
return App1.getDB().query( table, null,null,null, null, null, COL_NAME+" asc" );
return App1.getDB().query( table, null, null, null, null, null, COL_NAME + " asc" );
}
public static void delete( String name ){
try{
App1.getDB().delete( table, COL_NAME+"=?",new String[]{ name });
App1.getDB().delete( table, COL_NAME + "=?", new String[]{ name } );
}catch( Throwable ex ){
log.e( ex, "delete failed." );
}
}
public static HashSet<String> getNameSet(){
HashSet<String> dst = new HashSet<>();
public static HashSet< String > getNameSet(){
HashSet< String > dst = new HashSet<>();
try{
Cursor cursor = App1.getDB().query( table, null,null,null, null, null, null);
Cursor cursor = App1.getDB().query( table, null, null, null, null, null, null );
if( cursor != null ){
try{
int idx_name = cursor.getColumnIndex( COL_NAME );
while(cursor.moveToNext()){
String s = cursor.getString(idx_name);
dst.add( s);
while( cursor.moveToNext() ){
String s = cursor.getString( idx_name );
dst.add( s );
}
}finally{
cursor.close();
}
}
}catch(Throwable ex){
ex.printStackTrace( );
}catch( Throwable ex ){
ex.printStackTrace();
}
return dst;
}
// private static final String[] isMuted_projection = new String[]{COL_NAME};
// private static final String isMuted_where = COL_NAME+"=?";
// private static final ThreadLocal<String[]> isMuted_where_arg = new ThreadLocal<String[]>() {
// @Override protected String[] initialValue() {
// return new String[1];
// }
// };
// public static boolean isMuted( String app_name ){
// if( app_name == null ) return false;
// try{
// String[] where_arg = isMuted_where_arg.get();
// where_arg[0] = app_name;
// Cursor cursor = App1.getDB().query( table, isMuted_projection,isMuted_where , where_arg, null, null, null );
// try{
// if( cursor.moveToFirst() ){
// return true;
// }
// }finally{
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// }
// return false;
// }
// private static final String[] isMuted_projection = new String[]{COL_NAME};
// private static final String isMuted_where = COL_NAME+"=?";
// private static final ThreadLocal<String[]> isMuted_where_arg = new ThreadLocal<String[]>() {
// @Override protected String[] initialValue() {
// return new String[1];
// }
// };
// public static boolean isMuted( String app_name ){
// if( app_name == null ) return false;
// try{
// String[] where_arg = isMuted_where_arg.get();
// where_arg[0] = app_name;
// Cursor cursor = App1.getDB().query( table, isMuted_projection,isMuted_where , where_arg, null, null, null );
// try{
// if( cursor.moveToFirst() ){
// return true;
// }
// }finally{
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// }
// return false;
// }
}

View File

@ -23,9 +23,9 @@ import jp.juggler.subwaytooter.util.LogCategory;
public class SavedAccount extends TootAccount implements LinkClickContext {
private static final LogCategory log = new LogCategory( "SavedAccount" );
private static final String table = "access_info";
public static final String table = "access_info";
private static final String COL_ID = BaseColumns._ID;
public 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";
@ -65,6 +65,15 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
public boolean confirm_unfollow;
public boolean confirm_post;
// アプリデータのインポート時に呼ばれる
public static void onDBDelete( SQLiteDatabase db ){
try{
db.execSQL( "drop table if exists " + table );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
public static void onDBCreate( SQLiteDatabase db ){
db.execSQL(
"create table if not exists " + table
@ -298,7 +307,7 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
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);
return "https://" + who_acct.substring( p + 1 ) + "/@" + who_acct.substring( 0, p );
}else{
return "https://" + host + "/@" + who_acct;
}
@ -345,10 +354,10 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
public static long getCount(){
try{
Cursor cursor = App1.getDB().query( table,new String[]{ "count(*)"} , null, null, null, null, null );
Cursor cursor = App1.getDB().query( table, new String[]{ "count(*)" }, null, null, null, null, null );
try{
if( cursor.moveToNext() ){
return cursor.getLong(0);
return cursor.getLong( 0 );
}
}finally{
cursor.close();
@ -360,4 +369,5 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
}
return 0L;
}
}

View File

@ -585,6 +585,45 @@
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/actions"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnSettingExport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_data_export"
android:textAllCaps="false"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnSettingImport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_data_import"
android:textAllCaps="false"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_data_import_desc"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<!--<TextView-->
<!--style="@style/setting_row_label"-->

View File

@ -322,9 +322,13 @@
<string name="reply_to_in_draft_is_lost">The reply origin referred by draft has been lost. The following context is removed. in_reply_to</string>
<string name="restore_draft">Restore from draft</string>
<string name="select_draft">Which draft to restore ?</string>
<string name="app_data_export">Export app data</string>
<string name="app_data_import">Import app data</string>
<string name="app_data_import_confirm">existing data ( before import ) will be cleared. Are you sure?</string>
<string name="app_data_import_desc">You can export data to any app that can receive data, But maybe you can import data only from device\'s storage or Google drive.</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->
<!--<string name="abc_action_bar_up_description">Revenir en haut de la page</string>-->

View File

@ -610,4 +610,8 @@
<string name="reply_to_in_draft_is_lost">下書きから参照されている返信元の投稿は失われました。以下の文脈を再現できません。in_reply_to</string>
<string name="restore_draft">下書きから復元</string>
<string name="select_draft">どの下書きから復元しますか?</string>
<string name="app_data_export">アプリデータのエクスポート</string>
<string name="app_data_import">アプリデータのインポート</string>
<string name="app_data_import_confirm">アプリに今あるデータは消えてしまいます。よろしいですか?</string>
<string name="app_data_import_desc">エクスポート先はいくつか選べますが、インポートするには端末のストレージかGoogleドライブにデータを置く必要があります。カラム背景画像とタイムラインのフォントはエクスポート/インポートの対象外です。</string>
</resources>

View File

@ -319,4 +319,8 @@
<string name="account_in_draft_is_lost">The account used for this draft has been removed from app. The following context can not be restored. in_reply_to, media attachment.</string>
<string name="reply_to_in_draft_is_lost">The reply origin referred by draft has been lost. The following context is removed. in_reply_to</string>
<string name="attachment_in_draft_is_lost">Some of media attachment in the draft have been lost.</string>
<string name="app_data_export">Export app data</string>
<string name="app_data_import">Import app data</string>
<string name="app_data_import_desc">You can export data to any app that can receive data, But maybe you can import data only from device\'s storage or Google drive.\nColumn background image may be not exported/imported over device.\nTimeline font is not exported/imported.</string>
<string name="app_data_import_confirm">existing data ( before import ) will be cleared. Are you sure?</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--<files-path name="pathFiles" path="."/>-->
<cache-path name="pathCache" path="." />
<!--<external-path name="pathExternal" path="." />-->
<!--<external-files-path name="pathExternalFile" path="." />-->
<!--<external-cache-path name="pathExternalCache" path="." />-->
<!--<root-path name="pathRoot" path="." />-->
</paths>