Импорт/экспорт данных приложения. Практика
Предыдущая часть: https://teletype.in/@snowvolf/BJeE3fK5X
Создадим приложение. Имя пакета приложения, в моём случае, ru.SnowVolf.backupfactory
. Учтите это, при попытке воспроизвести мой код.
Откройте файл AndroidManifest.xml и добавьте внутри тега <manifest/>
:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
(Разрешение на запись и чтение памяти соответственно)
Внутри тега <application/>
добавьте обьявление новой активности:
<activity android:name=".BackupActivity"/>
Проигнорируйте ошибку IDE, мы исправим её позже.
Полный листинг AndroidManifest.xml приведён ниже:
Листинг 1. AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ru.SnowVolf.backupfactory"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:name=".App" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".BackupActivity"/> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Создайте класс App.java и напишите следующий код:
Листинг 2. Синглтон Application.
package ru.SnowVolf.backupfactory; import android.app.Application; public class App extends Application { private static App instance = null; private static SharedPreferences preferences; /** * Called when the application is starting, before any activity, service, * or receiver objects (excluding content providers) have been created. * Implementations should be as quick as possible (for example using * lazy initialization of state) since the time spent in this function * directly impacts the performance of starting the first activity, * service, or receiver in a process. * If you override this method, be sure to call super.onCreate(). */ @Override public void onCreate() { super.onCreate(); instance = this; } public static App get(){ if (instance == null){ instance = new App(); } return instance; } }
Вернитесь к манифесту. Одна ошибка должна исчезнуть.
Перейдите в каталог ресурсов вашего проекта, и откройте файл разметки главной активности (в моём случае - activity_main.xml) и сверстайте следующую разметку:
Листинг 3. Разметка activity_main.xml.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_alignParentTop="true" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="Settings test" android:textAppearance="@android:style/TextAppearance.Material.Body2"/> <EditText android:id="@+id/fieldName" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/textView" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:hint="Your name" android:maxLines="1" /> <Button android:id="@+id/buttonGet" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignEnd="@+id/fieldName" android:layout_below="@+id/fieldName" android:textAllCaps="false" android:text="Get" /> <Button android:id="@+id/buttonPut" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/fieldName" android:layout_toStartOf="@+id/buttonGet" android:textAllCaps="false" android:text="Set" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignStart="@+id/fieldName" android:layout_below="@+id/buttonPut" android:text="Database Test" android:textAppearance="@android:style/TextAppearance.Material.Body2" /> <TextView android:id="@+id/txtDB" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/textView2" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" /> <EditText android:id="@+id/fieldInsertCount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignStart="@+id/txtDB" android:layout_below="@+id/txtDB" android:layout_toStartOf="@+id/btnInsert" android:maxEms="2" android:hint="Insert count" android:inputType="phone" android:maxLines="1" /> <Button android:id="@+id/btnInsert" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@+id/fieldInsertCount" android:layout_alignEnd="@+id/buttonGet" android:text="Insert" android:textAllCaps="false"/> </RelativeLayout>
Создайте новый класс BackupFactory.java и поместите туда код из прошлой статьи. Я продублировал его здесь для удобства:
/* * Copyright (c) 2017 Deletescape. * Copyright (c) 2018 Snow Volf (Artem Zhiganov). * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ru.SnowVolf.backupfactory; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Environment; import android.util.Log; import android.widget.Toast; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; /** * Created by Snow Volf on 05.08.2018, 12:12 * * Класс экспорта/импорта БД и настроек * * Основано на: https://github.com/Deletescape/Lawnchair/app/src/main/java/ch/deletescape/lawnchair/DumbImportExportTask.java */ public class BackupFactory { // Экспорт БД public static void exportDb(Activity activity, String dbFiles) { ContextWrapper contextWrapper = new ContextWrapper(activity); String[] dbs = dbFiles.split(","); for (String db : dbs) { File expDb = contextWrapper.getDatabasePath(db); exportFile(expDb, activity); } } //Импорт БД public static void importDb(Activity activity, String dbFiles) { ContextWrapper contextWrapper = new ContextWrapper(activity); String[] dbs = dbFiles.split(","); for (String db : dbs) { File expDb = contextWrapper.getDatabasePath(db); importFile(expDb, activity); } } // Экспорт файла настроек public static void exportPrefs(Activity activity) { ApplicationInfo info = activity.getApplicationInfo(); String dir = new ContextWrapper(activity).getCacheDir().getParent(); File prefs = new File(dir, "shared_prefs/" + info.packageName + "_preferences.xml"); exportFile(prefs, activity); } // Импорт файла настроек public static void importPrefs(Activity activity) { ApplicationInfo info = activity.getApplicationInfo(); String dir = new ContextWrapper(activity).getCacheDir().getParent(); File prefs = new File(dir, "shared_prefs/" + info.packageName + "_preferences.xml"); importFile(prefs, activity); } // Экспорт файла private static void exportFile(File file, Activity activity) { // Если нет разрешения на запись if (!isExternalStorageWritable() || !canWriteStorage(activity)) { Toast.makeText(activity, "No write read/permission!", Toast.LENGTH_SHORT).show(); return; } File backup = new File(getFolder(), file.getName()); if (backup.exists()) { backup.delete(); } if (copy(file, backup)) { Toast.makeText(activity, "Successful!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(activity, "Error!", Toast.LENGTH_SHORT).show(); } } // Импорт файла private static void importFile(File file, Activity activity) { // Если нет разрешения на запись if (!isExternalStorageReadable() || !canWriteStorage(activity)) { Toast.makeText(activity, "No write read/permission!", Toast.LENGTH_SHORT).show(); return; } File backup = new File(getFolder(), file.getName()); if (!backup.exists()) { Toast.makeText(activity, String.format("Backups not found in %s", getFolder().getAbsolutePath()), Toast.LENGTH_LONG).show(); return; } if (file.exists()) { file.delete(); } if (copy(backup, file)) { Toast.makeText(activity, "Successful!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(activity, "Error!", Toast.LENGTH_SHORT).show(); } } // Получение пути к папке бэкапов // [SDCARD]/Documents/backup/ private static File getFolder() { File folder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "backup/"); if (!folder.exists()) { folder.mkdirs(); } return folder; } // Проверка на возможность записи private static boolean canWriteStorage(Activity activity) { return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } // Копирование файлов тудым-сюдым private static boolean copy(File inFile, File outFile) { FileInputStream in; FileOutputStream out; try { in = new FileInputStream(inFile); out = new FileOutputStream(outFile); byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } in.close(); // write the output file out.flush(); out.close(); return true; } catch (Exception e) { Log.e("BackupFactory", e.getMessage()); e.printStackTrace(); } return false; } /* Checks if external storage is available for read and write */ private static boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state); } /* Checks if external storage is available to at least read */ private static boolean isExternalStorageReadable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); } }
Создадим класс BackupActivity.java и напишем следующий код:
Листинг 4. Активность восстановления.
package ru.SnowVolf.backupfactory; import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Process; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ListView; public class BackupActivity extends Activity { private Activity mActivity; private String namesOfDb = "test.db"; private String[] options = {"Import databases", "Export databases", "Import preferences", "Export preferences", "Apply changes and restart"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mActivity = this; setContentView(createView()); } private View createView(){ LinearLayout content = new LinearLayout(this); LinearLayout.LayoutParams paramsMatch = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ); content.setLayoutParams(paramsMatch); return content; } @Override protected void onDestroy() { mActivity = null; namesOfDb = null; options = null; super.onDestroy(); } }
Разметка для этого экрана будет создана полностью из кода (для более удобного инжекта в другие программы при помощи smali). На данный момент мы создали только корневое представление View, т.е. пустой экран.
Заполним его списком с прикреплёнными действиями.
Допишем следующий код в метод createView()
:
// Создаём список ListView list = new ListView(this); list.setLayoutParams(paramsMatch); // Создаём адаптер для списка. В качестве разметки используем встроенный в framework ресурс. // В качестве заголовков для пунктов используем элементы массива options. ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, options); // Присваиваем адаптер list.setAdapter(adapter); // Присваиваем реакцию на нажатия в зависимости от позиции в списке list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { switch (position){ case 0: BackupFactory.importDb(mActivity, namesOfDb); break; case 1: BackupFactory.exportDb(mActivity, namesOfDb); break; case 2: BackupFactory.importPrefs(mActivity); break; case 3: BackupFactory.exportPrefs(mActivity); break; case 4: restartApp(); break; default: break; } } }); content.addView(list);
В итоге, ваш метод createView()
должен выглядеть так:
Листинг 5. Метод createView().
private View createView(){ LinearLayout content = new LinearLayout(this); LinearLayout.LayoutParams paramsMatch = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ); ListView list = new ListView(this); list.setLayoutParams(paramsMatch); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, options); list.setAdapter(adapter); list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { switch (position){ case 0: BackupFactory.importDb(mActivity, namesOfDb); break; case 1: BackupFactory.exportDb(mActivity, namesOfDb); break; case 2: BackupFactory.importPrefs(mActivity); break; case 3: BackupFactory.exportPrefs(mActivity); break; case 4: restartApp(); break; default: break; } } }); content.addView(list); return content; }
Особо наблюдательные могли заметить отсутствие метода restartApp()
. Давайте напишем его.
Листинг 6. Метод restartApp().
private void restartApp(){ Intent mStartActivity = new Intent(this, MainActivity.class); mStartActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); int mIntentPendingId = 6; PendingIntent mPendingIntent = PendingIntent.getActivity(this, mIntentPendingId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager manager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); manager.set(AlarmManager.RTC, System.currentTimeMillis() + 300, mPendingIntent); Process.killProcess(Process.myPid()); System.exit(0); }
Полный листинг активности приведён ниже:
Листинг 7. Класс BackupActivity.java
package ru.SnowVolf.backupfactory; import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Process; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ListView; public class BackupActivity extends Activity { private Activity mActivity; private String namesOfDb = "test.db"; private String[] options = {"Import databases", "Export databases", "Import preferences", "Export preferences", "Apply changes then restart"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mActivity = this; setContentView(createView()); } private View createView(){ LinearLayout content = new LinearLayout(this); LinearLayout.LayoutParams paramsMatch = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ); ListView list = new ListView(this); list.setLayoutParams(paramsMatch); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, options); list.setAdapter(adapter); list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { switch (position){ case 0: BackupFactory.importDb(mActivity, namesOfDb); break; case 1: BackupFactory.exportDb(mActivity, namesOfDb); break; case 2: BackupFactory.importPrefs(mActivity); break; case 3: BackupFactory.exportPrefs(mActivity); break; case 4: restartApp(); break; default: break; } } }); content.addView(list); return content; } private void restartApp(){ Intent mStartActivity = new Intent(this, MainActivity.class); mStartActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); int mIntentPendingId = 6; PendingIntent mPendingIntent = PendingIntent.getActivity(this, mIntentPendingId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager manager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); manager.set(AlarmManager.RTC, System.currentTimeMillis() + 300, mPendingIntent); Process.killProcess(Process.myPid()); System.exit(0); } @Override protected void onDestroy() { mActivity = null; namesOfDb = null; options = null; super.onDestroy(); } }
Всё. С этим закончили. Код можно свободно инжектить в любую программу.
Создадим пример
Вернитесь к MainActivity.java и напишите такой код:
Листинг 8. Основная активность
package ru.SnowVolf.backupfactory; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import java.util.Locale; public class MainActivity extends Activity { private TextView txtDb; private EditText mPrefValue, mNumOfItems; private Button btnGetPref, btnSetPref, btnAddItems; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); txtDb = findViewById(R.id.txtDB); mPrefValue = findViewById(R.id.fieldName); mNumOfItems = findViewById(R.id.fieldInsertCount); btnAddItems = findViewById(R.id.btnInsert); btnGetPref = findViewById(R.id.buttonGet); btnSetPref = findViewById(R.id.buttonPut); final TestDB db = new TestDB(getApplicationContext()); txtDb.setText(String.format(Locale.ENGLISH, "Number of items in abstract DB: %d", db.getAllItems().size())); btnAddItems.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int count = Integer.parseInt(mNumOfItems.getText().toString()); if ( count > 0 && count <= 99){ for (int i = 0; i < count; i++) { db.addItem(new TestDbItem(System.currentTimeMillis())); } } else { Toast.makeText(MainActivity.this, "Very big number", Toast.LENGTH_SHORT).show(); } } }); btnSetPref.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { App.get().getPreferences().edit().putString("name", mPrefValue.getText().toString()).apply(); } }); btnGetPref.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mPrefValue.setText(App.get().getPreferences().getString("name", "empty")); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_settings){ startActivity(new Intent(this, BackupActivity.class)); return true; } return super.onOptionsItemSelected(item); } }
Активность ссылается на 2 несущесуществующих класса. Создадим их.
Листинг 9. TestDbItem.java
/* * Copyright (c) 2017 Snow Volf (Artem Zhiganov). * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ru.SnowVolf.backupfactory; /** * Created by Snow Volf on 04.06.2017, 22:44 */ public class TestDbItem { private long id = -1L; public TestDbItem(long id) { this.id = id; } public long getId() { return id; } public void setId(long id) { this.id = id; } }
Последнее - класс для работы с базой данных SQLiteDatabase
Листинг 9. TestDB.java
/* * Copyright (c) 2017 Snow Volf (Artem Zhiganov). * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ru.SnowVolf.backupfactory; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import java.util.ArrayList; import java.util.List; /** * Created by Snow Volf on 04.06.2017, 22:52 */ public class TestDB extends SQLiteOpenHelper { public TestDB(Context context) { super(context, "test.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE test_db (id INTEGER)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Update this when db table will be changed } public void addItem(TestDbItem item) { if (item.getId() == -1) { item.setId(System.currentTimeMillis()); } ContentValues values = new ContentValues(); values.put("id", System.currentTimeMillis()); SQLiteDatabase db = getWritableDatabase(); db.insert("test_db", null, values); db.close(); } public List<TestDbItem> getAllItems() { List<TestDbItem> list = new ArrayList<>(); SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT * FROM " + "test_db", null); if (cursor.moveToFirst()) { do { list.add(new TestDbItem((cursor.getLong(0)))); } while (cursor.moveToNext()); } cursor.close(); db.close(); return list; } public void deleteAll() { SQLiteDatabase db = getWritableDatabase(); db.delete("test_db", "id" + ">=?", new String[]{"0"}); db.close(); } }
Проверьте код на ошибки, их не должно быть. Скомпилируйте приложение.