Импорт/экспорт данных приложения. Практика
Предыдущая часть: 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();
}
}
Проверьте код на ошибки, их не должно быть. Скомпилируйте приложение.