<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>SVolf 🇷🇺</title><generator>teletype.in</generator><description><![CDATA[SVolf 🇷🇺]]></description><image><url>https://teletype.in/files/70/70cfffb3-0e86-4f65-9cdf-b0008dda028b.png</url><title>SVolf 🇷🇺</title><link>https://teletype.in/@snowvolf</link></image><link>https://teletype.in/@snowvolf?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/snowvolf?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/snowvolf?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Wed, 27 May 2026 19:34:10 GMT</pubDate><lastBuildDate>Wed, 27 May 2026 19:34:10 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@snowvolf/rkxRTtsqm</guid><link>https://teletype.in/@snowvolf/rkxRTtsqm?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/rkxRTtsqm?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Импорт/экспорт данных приложения. Практика</title><pubDate>Wed, 10 Oct 2018 14:35:30 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/06/062c3317-9c25-456a-bb72-bb3dd3a353ba.jpeg"></media:content><description><![CDATA[Предыдущая часть: https://teletype.in/@snowvolf/BJeE3fK5X]]></description><content:encoded><![CDATA[
  <p>Предыдущая часть: https://teletype.in/@snowvolf/BJeE3fK5X</p>
  <p>Создадим приложение. Имя пакета приложения, в моём случае, <code>ru.SnowVolf.backupfactory</code>. Учтите это, при попытке воспроизвести мой код.</p>
  <p>Откройте файл <strong>AndroidManifest.xml</strong> и добавьте внутри тега <code>&lt;manifest/&gt;</code>:</p>
  <pre>&lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot;/&gt;
&lt;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot;/&gt;
</pre>
  <p>(Разрешение на запись и чтение памяти соответственно)</p>
  <p>Внутри тега <code>&lt;application/&gt;</code> добавьте обьявление новой активности:</p>
  <pre>&lt;activity android:name=&quot;.BackupActivity&quot;/&gt;
</pre>
  <p>Проигнорируйте ошибку IDE, мы исправим её позже.</p>
  <p>Полный листинг<strong> AndroidManifest.xml</strong> приведён ниже:</p>
  <p><strong>Листинг 1. AndroidManifest.xml</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  package=&quot;ru.SnowVolf.backupfactory&quot;&gt;
  &lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot;/&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot;/&gt;
  &lt;application
    android:name=&quot;.App&quot;
    android:allowBackup=&quot;true&quot;
    android:icon=&quot;@mipmap/ic_launcher&quot;
    android:label=&quot;@string/app_name&quot;
    android:roundIcon=&quot;@mipmap/ic_launcher_round&quot;
    android:supportsRtl=&quot;true&quot;
    android:theme=&quot;@style/AppTheme&quot;&gt;
    &lt;activity android:name=&quot;.BackupActivity&quot;/&gt;
    &lt;activity android:name=&quot;.MainActivity&quot;&gt;
      &lt;intent-filter&gt;
        &lt;action android:name=&quot;android.intent.action.MAIN&quot; /&gt;

        &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&gt;
      &lt;/intent-filter&gt;
    &lt;/activity&gt;
  &lt;/application&gt;

&lt;/manifest&gt;
</pre>
  <p>Создайте класс <strong>App.java</strong> и напишите следующий код:</p>
  <p><strong>Листинг 2. Синглтон Application.</strong></p>
  <pre>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;
  }
}
</pre>
  <p>Вернитесь к манифесту. Одна ошибка должна исчезнуть.</p>
  <p>Перейдите в каталог ресурсов вашего проекта, и откройте файл разметки главной активности (в моём случае - <strong>activity_main.xml</strong>) и сверстайте следующую разметку:</p>
  <p><strong>Листинг 3. Разметка activity_main.xml.</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;RelativeLayout xmlns:tools=&quot;http://schemas.android.com/tools&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;
  xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; &gt;

  &lt;TextView
    android:id=&quot;@+id/textView&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_alignParentStart=&quot;true&quot;
    android:layout_alignParentTop=&quot;true&quot;
    android:layout_marginEnd=&quot;16dp&quot;
    android:layout_marginStart=&quot;16dp&quot;
    android:layout_marginTop=&quot;8dp&quot;
    android:text=&quot;Settings test&quot;
    android:textAppearance=&quot;@android:style/TextAppearance.Material.Body2&quot;/&gt;

  &lt;EditText
    android:id=&quot;@+id/fieldName&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_below=&quot;@+id/textView&quot;
    android:layout_marginEnd=&quot;16dp&quot;
    android:layout_marginStart=&quot;16dp&quot;
    android:hint=&quot;Your name&quot;
    android:maxLines=&quot;1&quot; /&gt;

  &lt;Button
    android:id=&quot;@+id/buttonGet&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_alignEnd=&quot;@+id/fieldName&quot;
    android:layout_below=&quot;@+id/fieldName&quot;
    android:textAllCaps=&quot;false&quot;
    android:text=&quot;Get&quot; /&gt;

  &lt;Button
    android:id=&quot;@+id/buttonPut&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_below=&quot;@+id/fieldName&quot;
    android:layout_toStartOf=&quot;@+id/buttonGet&quot;
    android:textAllCaps=&quot;false&quot;
    android:text=&quot;Set&quot; /&gt;

  &lt;TextView
    android:id=&quot;@+id/textView2&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_alignStart=&quot;@+id/fieldName&quot;
    android:layout_below=&quot;@+id/buttonPut&quot;
    android:text=&quot;Database Test&quot;
    android:textAppearance=&quot;@android:style/TextAppearance.Material.Body2&quot; /&gt;

  &lt;TextView
    android:id=&quot;@+id/txtDB&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_below=&quot;@+id/textView2&quot;
    android:layout_marginEnd=&quot;16dp&quot;
    android:layout_marginStart=&quot;16dp&quot;
    android:layout_marginTop=&quot;8dp&quot; /&gt;

  &lt;EditText
    android:id=&quot;@+id/fieldInsertCount&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_alignStart=&quot;@+id/txtDB&quot;
    android:layout_below=&quot;@+id/txtDB&quot;
    android:layout_toStartOf=&quot;@+id/btnInsert&quot;
    android:maxEms=&quot;2&quot;
    android:hint=&quot;Insert count&quot;
    android:inputType=&quot;phone&quot;
    android:maxLines=&quot;1&quot; /&gt;

  &lt;Button
    android:id=&quot;@+id/btnInsert&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_alignBottom=&quot;@+id/fieldInsertCount&quot;
    android:layout_alignEnd=&quot;@+id/buttonGet&quot;
    android:text=&quot;Insert&quot;
    android:textAllCaps=&quot;false&quot;/&gt;

&lt;/RelativeLayout&gt;
</pre>
  <p>Создайте новый класс <strong>BackupFactory.java</strong> и поместите туда код из прошлой статьи. Я продублировал его здесь для удобства:</p>
  <pre>/*
 * Copyright (c) 2017 Deletescape.
 * Copyright (c) 2018 Snow Volf (Artem Zhiganov).
 * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
 * 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 &quot;AS IS&quot; 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(&quot;,&quot;);
    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(&quot;,&quot;);
    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, &quot;shared_prefs/&quot; + info.packageName + &quot;_preferences.xml&quot;);
    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, &quot;shared_prefs/&quot; + info.packageName + &quot;_preferences.xml&quot;);

    importFile(prefs, activity);
  }

  // Экспорт файла
  private static void exportFile(File file, Activity activity) {
    // Если нет разрешения на запись
    if (!isExternalStorageWritable() || !canWriteStorage(activity)) {
      Toast.makeText(activity, &quot;No write read/permission!&quot;, Toast.LENGTH_SHORT).show();
      return;
    }
    File backup = new File(getFolder(), file.getName());
    if (backup.exists()) {
      backup.delete();
    }
    if (copy(file, backup)) {
      Toast.makeText(activity, &quot;Successful!&quot;, Toast.LENGTH_SHORT).show();
    } else {
      Toast.makeText(activity, &quot;Error!&quot;, Toast.LENGTH_SHORT).show();
    }
  }

  // Импорт файла
  private static void importFile(File file, Activity activity) {
    // Если нет разрешения на запись
    if (!isExternalStorageReadable() || !canWriteStorage(activity)) {
      Toast.makeText(activity, &quot;No write read/permission!&quot;, Toast.LENGTH_SHORT).show();
      return;
    }
    File backup = new File(getFolder(), file.getName());
    if (!backup.exists()) {
      Toast.makeText(activity, String.format(&quot;Backups not found in %s&quot;, getFolder().getAbsolutePath()), Toast.LENGTH_LONG).show();
      return;
    }
    if (file.exists()) {
      file.delete();
    }
    if (copy(backup, file)) {
      Toast.makeText(activity, &quot;Successful!&quot;, Toast.LENGTH_SHORT).show();
    } else {
      Toast.makeText(activity, &quot;Error!&quot;, Toast.LENGTH_SHORT).show();
    }
  }

  // Получение пути к папке бэкапов
  // [SDCARD]/Documents/backup/

  private static File getFolder() {
    File folder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), &quot;backup/&quot;);
    if (!folder.exists()) {
      folder.mkdirs();
    }
    return folder;
  }

  // Проверка на возможность записи
  private static boolean canWriteStorage(Activity activity) {
    return Build.VERSION.SDK_INT &lt; 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(&quot;BackupFactory&quot;, 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);
  }
}
</pre>
  <p>Создадим класс <strong>BackupActivity.java</strong> и напишем следующий код:</p>
  <p><strong>Листинг 4. Активность восстановления.</strong></p>
  <pre>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 = &quot;test.db&quot;;
  private String[] options = {&quot;Import databases&quot;, &quot;Export databases&quot;, &quot;Import preferences&quot;,
 &quot;Export preferences&quot;, &quot;Apply changes and restart&quot;};


  @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();
  }
}
</pre>
  <p>Разметка для этого экрана будет создана полностью из кода (для более удобного инжекта в другие программы при помощи smali). На данный момент мы создали только корневое представление View, т.е. пустой экран.</p>
  <p>Заполним его списком с прикреплёнными действиями.</p>
  <p>Допишем следующий код в метод <code>createView()</code>:</p>
  <pre>		// Создаём список
		ListView list = new ListView(this);
    list.setLayoutParams(paramsMatch);
		// Создаём адаптер для списка. В качестве разметки используем встроенный в framework ресурс.
		// В качестве заголовков для пунктов используем элементы массива options.
    ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;&gt;(this, android.R.layout.simple_list_item_1, options);
		// Присваиваем адаптер
    list.setAdapter(adapter);
		// Присваиваем реакцию на нажатия в зависимости от позиции в списке
    list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView&lt;?&gt; 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);
</pre>
  <p>В итоге, ваш метод <code>createView()</code> должен выглядеть так:</p>
  <p><strong>Листинг 5. Метод createView().</strong></p>
  <pre>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&lt;String&gt; adapter = new ArrayAdapter&lt;&gt;(this, android.R.layout.simple_list_item_1, options);
    list.setAdapter(adapter);
    list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView&lt;?&gt; 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;
  }
</pre>
  <p>Особо наблюдательные могли заметить отсутствие метода <code>restartApp()</code>. Давайте напишем его.</p>
  <p><strong>Листинг 6. Метод restartApp().</strong></p>
  <pre>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);
  }
</pre>
  <p>Полный листинг активности приведён ниже:</p>
  <p><strong>Листинг 7. Класс BackupActivity.java</strong></p>
  <pre>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 = &quot;test.db&quot;;
  private String[] options = {&quot;Import databases&quot;, &quot;Export databases&quot;, &quot;Import preferences&quot;,
 &quot;Export preferences&quot;, &quot;Apply changes then restart&quot;};

  @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&lt;String&gt; adapter = new ArrayAdapter&lt;&gt;(this, android.R.layout.simple_list_item_1, options);
    list.setAdapter(adapter);
    list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView&lt;?&gt; 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();
  }
}
</pre>
  <p>Всё. С этим закончили. Код можно свободно инжектить в любую программу.</p>
  <h2>Создадим пример</h2>
  <p>Вернитесь к <strong>MainActivity.java</strong> и напишите такой код:</p>
  <p><strong>Листинг 8. Основная активность</strong></p>
  <pre>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, &quot;Number of items in abstract DB: %d&quot;, db.getAllItems().size()));

    btnAddItems.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        int count = Integer.parseInt(mNumOfItems.getText().toString());
        if ( count &gt; 0 &amp;&amp; count &lt;= 99){

          for (int i = 0; i &lt; count; i++) {
            db.addItem(new TestDbItem(System.currentTimeMillis()));
          }
        } else {
          Toast.makeText(MainActivity.this, &quot;Very big number&quot;, Toast.LENGTH_SHORT).show();
        }
      }
    });
    btnSetPref.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        App.get().getPreferences().edit().putString(&quot;name&quot;, mPrefValue.getText().toString()).apply();
      }
    });

    btnGetPref.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        mPrefValue.setText(App.get().getPreferences().getString(&quot;name&quot;, &quot;empty&quot;));
      }
    });
  }

  @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);
  }
}
</pre>
  <p>Активность ссылается на 2 несущесуществующих класса. Создадим их.</p>
  <p><strong>Листинг 9. TestDbItem.java</strong></p>
  <pre>/*
 * Copyright (c) 2017 Snow Volf (Artem Zhiganov).
 * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
 * 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 &quot;AS IS&quot; 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;
  }
}
</pre>
  <p>Последнее - класс для работы с базой данных SQLiteDatabase</p>
  <p><strong>Листинг 9. TestDB.java</strong></p>
  <pre>/*
 * Copyright (c) 2017 Snow Volf (Artem Zhiganov).
 * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
 * 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 &quot;AS IS&quot; 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, &quot;test.db&quot;, null, 1);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL(&quot;CREATE TABLE test_db (id INTEGER)&quot;);
  }

  @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(&quot;id&quot;, System.currentTimeMillis());

    SQLiteDatabase db = getWritableDatabase();
    db.insert(&quot;test_db&quot;, null, values);
    db.close();
  }

  public List&lt;TestDbItem&gt; getAllItems() {
    List&lt;TestDbItem&gt; list = new ArrayList&lt;&gt;();
    SQLiteDatabase db = getReadableDatabase();
    Cursor cursor = db.rawQuery(&quot;SELECT * FROM &quot; + &quot;test_db&quot;, 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(&quot;test_db&quot;, &quot;id&quot; + &quot;&gt;=?&quot;, new String[]{&quot;0&quot;});
    db.close();
  }
}
</pre>
  <p>Проверьте код на ошибки, их не должно быть. Скомпилируйте приложение.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/BJeE3fK5X</guid><link>https://teletype.in/@snowvolf/BJeE3fK5X?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/BJeE3fK5X?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Импорт и экспорт данных приложения. Теория.</title><pubDate>Mon, 08 Oct 2018 18:04:23 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/94/94e87190-36bf-4b0c-8a84-75f309b44c0a.png"></media:content><description><![CDATA[@VolfsChannel]]></description><content:encoded><![CDATA[
  <p><a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>
  <p>Порой нам бывает необходимо восстановить некоторые настройки приложения. Это просто делается вручную, но что если в приложении десятки, а то и сотни независимых настроек? На многих устройствах нет рута, с adb не каждый разберется. Так что же делать? Сейчас узнаем.</p>
  <p>Данный код будет работать при 2-х условиях:</p>
  <ul>
    <li>Наличие разрешения на запись</li>
  </ul>
  <pre>&lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot;/&gt;
&lt;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot;/&gt;

</pre>
  <ul>
    <li>В настройках системы тоже должен быть предоставлен доступ</li>
  </ul>
  <p>Создадим класс <code>BackupFactory.java:</code></p>
  <pre>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;


public class BackupFactory {
    // Экспорт БД
    public static void exportDb(Activity activity, String dbFiles) {
        ContextWrapper contextWrapper = new ContextWrapper(activity);
		// Преобразование строки в массив
        String[] dbs = dbFiles.split(&quot;,&quot;);
		// Экспорт каждого отдельного файла
        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(&quot;,&quot;);
		// Импорт каждого отдельного файла
        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, &quot;shared_prefs/&quot; + info.packageName + &quot;_preferences.xml&quot;);
        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, &quot;shared_prefs/&quot; + info.packageName + &quot;_preferences.xml&quot;);
        importFile(prefs, activity);
    }

    // Экспорт файла
    private static void exportFile(File file, Activity activity) {
        // Если нет разрешения на запись
        if (!isExternalStorageWritable() || !canWriteStorage(activity)) {
            Toast.makeText(activity, &quot;Нет разрешения на чтение/запись!&quot;, Toast.LENGTH_SHORT).show();
            return;
        }
        File backup = new File(getFolder(), file.getName());
		// Перезаписываем существующий файл
        if (backup.exists()) {
            backup.delete();
        }
        if (copy(file, backup)) {
            Toast.makeText(activity, &quot;Успешно!&quot;, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(activity, &quot;Ошибка!&quot;, Toast.LENGTH_SHORT).show();
        }
    }

    // Импорт файла
    private static void importFile(File file, Activity activity) {
        // Если нет разрешения на запись
        if (!isExternalStorageReadable() || !canWriteStorage(activity)) {
            Toast.makeText(activity, &quot;Нет разрешения на чтение/запись!&quot;, Toast.LENGTH_SHORT).show();
            return;
        }
        File backup = new File(getFolder(), file.getName());
        if (!backup.exists()) {
            Toast.makeText(activity, &quot;Файлы бэкапа отсутствуют!&quot;, Toast.LENGTH_LONG).show();
            return;
        }
        if (file.exists()) {
            file.delete();
        }
		// Всё хорошо
        if (copy(backup, file)) {
            Toast.makeText(activity, &quot;Успешно!&quot;, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(activity, &quot;Ошибка!&quot;, Toast.LENGTH_SHORT).show();
        }
    }

    // Получение пути к папке бэкапов
    // [SDCARD]/Documents/backup/

    private static File getFolder() {
        File folder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), &quot;backup&quot;);
        if (!folder.exists()) {
            folder.mkdirs();
        }
        return folder;
    }

    // Проверка на возможность записи
    private static boolean canWriteStorage(Activity activity) {
        return Build.VERSION.SDK_INT &lt; 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(&quot;BackupFactory&quot;, e.getMessage());
        }
        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);
    }
	
</pre>
  <p>Я добавил комментарии к коду. Основных метода 4 - импорт/экспорт БД, импорт/экспорт настроек. Файлов БД может быть несколько, указывать их надо через запятую <code>&quot;test.db,sec.db&quot;</code> etc.</p>
  <p>Теперь попробуем написать...[Продолжение следует]</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/B1NXMSW5Q</guid><link>https://teletype.in/@snowvolf/B1NXMSW5Q?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/B1NXMSW5Q?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>С чего начать изучение smali?</title><pubDate>Tue, 02 Oct 2018 19:08:11 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/8c/8c6ac84d-2dbc-496a-a785-efdade929502.png"></media:content><description><![CDATA[@VolfsChannel]]></description><content:encoded><![CDATA[
  <p><a href="http://t.me/VolfsChannel" target="_blank">@VolfsChanne</a>l</p>
  <p>Я часто слышу что-то типа:</p>
  <blockquote>Я нуб, как мне познакомиться со smali?</blockquote>
  <p>Общих инструкций не существует. Я лишь могу подтолкнуть в верном направлении.</p>
  <ol>
    <li>Желательно чтобы вы уже знали какой-либо язык программирования или имели дело с Web разработкой. Имея необходимую усидчивость и навык поиска информации, вы сможете легко пойти по пути модификации.</li>
    <li>Сравнивайте моды. Берите оригинал и мод, Декомпилируйте и сравните через <a href="https://www.google.com/url?sa=t&source=web&rct=j&url=https://www.scootersoftware.com/download.php&ved=2ahUKEwjInf36uejdAhWmp4sKHZ2aAXEQFjAAegQIBBAD&usg=AOvVaw3AnJcSlT29pPt_ALRqnLrn" target="_blank">Beyound Compare</a> для Windows.</li>
    <li>Читайте мануалы. Большое количество мануалов находится в открытом доступе на форуме 4PDA. Если вы не знаете русский - используйте Google Translate</li>
    <li>Не прыгайте выше головы. Сперва научитесь изменять простые (boolean) значения. Станьте спецом в const/4 v0, 0x1.</li>
    <li>Используйте автоматические патчи, только когда научитесь делать то же самое вручную. Помните: патчи - это лишь автоматизация рутинной работы. Сравнивайте оригинал и патченный Apk, и поймите что сделал патч в каждом конкретном случае.</li>
    <li>Google - ваш лучший друг. При возникновении вопросов используйте Google. Очень маловероятно, что вы первый кто столкнулся с проблемой. Используйте информацию во благо.</li>
  </ol>
  <hr />
  <h2>Полезные ссылки</h2>
  <p><strong>Комплект инструкций на 4PDA:</strong></p>
  <p><strong>The biggest Mod Apk Glossary (use google translate if don&#x27;t understand)</strong></p>
  <p><a href="https://4pda.ru/forum/index.php?s=&showtopic=461675&view=findpost&p=44979438" target="_blank">https://4pda.ru/forum/index.php?s=&amp;showtopic=461675&amp;view=findpost&amp;p=44979438</a></p>
  <p><strong>Another</strong>:</p>
  <p><a href="https://4pda.ru/forum/index.php?s=&showtopic=461675&view=findpost&p=21970396" target="_blank">https://4pda.ru/forum/index.php?s=&amp;showtopic=461675&amp;view=findpost&amp;p=21970396</a></p>
  <p><strong>XDA Apk Modding:</strong></p>
  <p><a href="https://forum.xda-developers.com/showthread.php?t=2295002" target="_blank">https://forum.xda-developers.com/showthread.php?t=2295002</a></p>
  <p><strong>and another link:</strong></p>
  <p><a href="https://forum.xda-developers.com/showthread.php?t=1624757" target="_blank">https://forum.xda-developers.com/showthread.php?t=1624757</a></p>
  <p><strong>Other links</strong>: <a href="https://hackerbot.net/tutorials/330-mod-apk" target="_blank">https://hackerbot.net/tutorials/330-mod-apk</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/rkACyQKYm</guid><link>https://teletype.in/@snowvolf/rkACyQKYm?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/rkACyQKYm?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>shell команды на Android за ~100 строк кода</title><pubDate>Wed, 26 Sep 2018 15:13:29 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/ae/ae160e66-83ad-4014-ba2c-80a1aea5de29.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/b6/b67bfebf-642c-45ee-9069-2bd94f96e0ab.png"></img>Мой канал: @VolfsChannel]]></description><content:encoded><![CDATA[
  <p>Мой канал: <a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>
  <p>Сегодня мы попробуем создать мини-приложение терминал, рассмотрим основные методы работы с shell-командами на Android.</p>
  <p>Android основан на ядре Linux. Также как и на Linux в нем можно выполнять команды через терминал. Терминал в Android по своей сути просто является нескучной обоиной для выполнения функций рантайма. Чтож, заглянем глубже. Создадим простейшее приложение которое будет отдавать shell-команды и получать результат в виде строки. Ко всему прочему наше приложение сможет использовать Root.</p>
  <h2>100 строчек кода...</h2>
  <p>Создадим новый класс, я назвал его CommandExecutor.java и напишите следующий код:</p>
  <pre>import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;

public class CommandExecutor {

		// Модель для получения результата
		public static class ResultData {
				// Код результата
				private int resultCode;
				// Результат отработки команды
				private String resultData;
				// Поле для записи ошибок
				private String resultError;

				// Конструктор
				ResultData(int code, String info, String err) {
						resultCode = code;
						resultData = info == null ? &quot;&quot; : info;
						resultError = err == null ? &quot;&quot; : err;
					}
				// Геттеры полей
				public int getResultCode() {
						return resultCode;
					}
				public String getResultData() {
						return resultData;
					}
				public String getResultError() {
						return resultError;
					}
				public String getResult() {
						return toString();
					}
				public String toString() {
						return resultData + &quot;\n&quot; + resultError;
					}
			}
		// Выполнение простой sh-команды
		public static ResultData execute(String command) {
				return execute(false, command);
			}
		// То же что и выше, но из под рута
		public static ResultData executeSu(String command) {
				return execute(true, command);
			}
		// Если нужно изменять значение su динамически
		public static ResultData execute(boolean su, String command) {
				return execPool(su, rmSlashN(command).split(&quot;\n&quot;));
			}
		// Выполняем массив команд
		private static ResultData execPool(boolean su, String[] commands) {
				// Обьявление переменных
				Process exec = null;
				InputStream execIn = null;
				InputStream execErr = null;
				OutputStream execOs = null;

				ResultData resultData;
				try {
						// В зависимости от значения su меняем параметр запуска команды
						// От рута или же от юзера
						exec = Runtime.getRuntime().exec(su ? &quot;su&quot; : &quot;sh&quot;);
						execIn = exec.getInputStream();
						execErr = exec.getErrorStream();
						execOs = exec.getOutputStream();
						DataOutputStream dos = new DataOutputStream(execOs);
						// В цикле проходимся по всем отосланным командам
						for (String com : commands)
							if (!com.isEmpty()) {
									dos.writeBytes(com + &quot;\n&quot;);
									dos.flush();
								}
						dos.close();
						// Посылаем команду и ждём ответа от оболочки
						resultData = new ResultData(exec.waitFor(), inputStream2String(execIn, &quot;utf-8&quot;), inputStream2String(execErr, &quot;utf-8&quot;));
					} catch (Exception e) {
						resultData = new ResultData(-1, &quot;&quot;, e.toString());
					} finally {
						try {
								// Не забываем закрыть все стримы
								if (execIn != null) execIn.close();
								if (execErr != null) execErr.close();
								if (execOs != null) execOs.close();
								// Завершаем процесс выполнения
								if (exec != null) exec.destroy();
							} catch (Exception ignored) {}
					}
				return resultData;
			}

		// Преобразование стрима в строку
		public static String inputStream2String(InputStream in, String encoding) throws Exception {
				StringBuilder out = new StringBuilder();
				InputStreamReader inread = new InputStreamReader(in, encoding);
				char[] b = new char[1024];
				int n;
				while ((n = inread.read(b)) != -1) {
						String s = new String(b, 0, n);
						out.append(s);
					}
				return out.toString();
			}

		// Чтение стрима в строку
		public static String read(InputStream is) {
				try {
						ByteArrayOutputStream baos = new ByteArrayOutputStream();
						byte[] buffer = new byte[1024];
						int length;
						while ((length = is.read(buffer)) != -1)
							baos.write(buffer, 0, length);
						return baos.toString(&quot;UTF-8&quot;);
					} catch (Exception e) { e.printStackTrace(); return e.toString(); }
			}

		// Удаляем лишние переносы строки
		public static String rmSlashN(String text) {
				while (text.contains(&quot;\n\n&quot;)) text = text.replace(&quot;\n\n&quot;, &quot;\n&quot;);
				if (text.startsWith(&quot;\n&quot;))
					text = text.substring(1);
				if (text.endsWith(&quot;\n&quot;))
					text = text.substring(0, text.length() - 1);

				return text;
			}
}
</pre>
  <p>Я добавил комментарии к коду. Если у вас возникнут вопросы - пишите их в бота.</p>
  <p>Грубо говоря: мы шлём команду, ждём пока система ответит, и выполняем другие команды дальше.</p>
  <p>В примере приложения я использовал метод <code>execute(boolean su, String command)</code> т.к. значение <code>su</code> у меня зависит от чекбокса.</p>
  <h2>Пример вызова</h2>
  <pre>String out = CommandExecutor.execute(true, &quot;magiskhide --ls&quot;).getResult();
</pre>
  <p>Выведет список приложений от которых скрывается рут в <code>MagiskHide</code>.</p>
  <pre>Log.d(&quot;SU-TEST&quot;, out);
</pre>
  <h2>Создаем приложение</h2>
  <p>Создайте новый проект в IDE. Откройте MainActivity.java и напишите следующий код:</p>
  <pre>import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.CheckBox;
import android.widget.TextView;
import android.view.View.OnClickListener;
import android.view.View;

public class MainActivity extends Activity {
	private Button btnExec;
	private EditText fieldCommand;
	private CheckBox boxSu;
	private TextView textOut;
	
		@Override
		protected void onCreate(Bundle savedInstanceState) {
				super.onCreate(savedInstanceState);
				setContentView(R.layout.main);
				
				fieldCommand = findViewById(R.id.field_exec);
				boxSu = findViewById(R.id.checbox_su);
				btnExec = findViewById(R.id.btn_execute);
				textOut = findViewById(R.id.command_output);
				
				btnExec.setOnClickListener(new OnClickListener(){

							@Override
							public void onClick(View p1) {
									String out = CommandExecutor.execute(boxSu.isChecked(), fieldCommand.getText().toString()).getResult();
									textOut.setText(out);
								}
							
					
				});
			}
	}
</pre>
  <p>Измените разметку главного экрана (в моем случае <strong>main.xml</strong>)</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout
	xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
	android:layout_width=&quot;match_parent&quot;
	android:layout_height=&quot;match_parent&quot;
	android:orientation=&quot;vertical&quot;
	android:padding=&quot;16dp&quot;&gt;

	&lt;EditText
		android:layout_width=&quot;match_parent&quot;
		android:maxLines=&quot;2&quot;
		android:layout_height=&quot;wrap_content&quot;
		android:hint=&quot;command&quot;
		android:id=&quot;@+id/field_exec&quot;/&gt;

	&lt;CheckBox
		android:layout_marginTop=&quot;16dp&quot;
		android:text=&quot;Super User&quot;
		android:layout_width=&quot;wrap_content&quot;
		android:layout_height=&quot;wrap_content&quot;
		android:id=&quot;@+id/checbox_su&quot;/&gt;

	&lt;Button
		android:layout_width=&quot;wrap_content&quot;
		android:layout_height=&quot;wrap_content&quot;
		android:text=&quot;Execute&quot;
		android:textAllCaps=&quot;false&quot;
		android:layout_gravity=&quot;end&quot;
		android:id=&quot;@+id/btn_execute&quot;/&gt;

	&lt;ScrollView
		android:layout_width=&quot;wrap_content&quot;
		android:layout_height=&quot;wrap_content&quot;&gt;

		&lt;TextView
			android:layout_width=&quot;wrap_content&quot;
			android:layout_height=&quot;wrap_content&quot;
			android:typeface=&quot;monospace&quot;
			android:textSize=&quot;12sp&quot;
			android:id=&quot;@+id/command_output&quot;/&gt;

	&lt;/ScrollView&gt;

&lt;/LinearLayout&gt;
</pre>
  <p>Скопируйте класс <strong>CommandExecutor.java</strong> к себе в проект.</p>
  <p>Все ошибки должны пропасть. Запустите приложение и попробуйте выполнить любую shell-команду.</p>
  <h2>Результат</h2>
  <p>Выполнение команды без рута</p>
  <figure class="m_custom">
    <img src="https://teletype.in/files/b6/b67bfebf-642c-45ee-9069-2bd94f96e0ab.png" width="1080" />
  </figure>
  <p>То же самое с рутом</p>
  <figure class="m_custom">
    <img src="https://teletype.in/files/20/204b0f77-655f-4e60-8b8f-5be2559f2a4a.png" width="1080" />
  </figure>
  <p>Приложение и его исходный код можно найти на канале: <a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/HkMgNWxYQ</guid><link>https://teletype.in/@snowvolf/HkMgNWxYQ?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/HkMgNWxYQ?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Аутентификация с помощью отпечатков пальцев в Android</title><pubDate>Wed, 19 Sep 2018 17:14:17 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/13/13ecede5-73f6-4692-b6fd-8feae7c32c2d.jpeg"></media:content><description><![CDATA[<img src="https://teletype.in/files/1c/1c38b1b9-a435-4470-945b-c6a713918e8c.png"></img>Мой канал: @VolfsChannel]]></description><content:encoded><![CDATA[
  <p>Мой канал: <a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>
  <p>Биометрия... Ваши отпечатки пальцев в смартфонах... Ещё 5-7 лет назад данный вид аутентификации казался баловством. Я что, храню коды запуска ракет? Зачем мне этот сканер? Да графический ключ удобнее и быстрее! Знакомые фразы? Может быть вы и сами так думали, но сейчас глупо отрицать - такого рода биометрическая аутентификация является лидирующей на портативных устройствах. Сказывается дешевизна ($3-5 за модуль сканера) и относительная безопасность.</p>
  <p>Сканеры отпечатков пальцев в смартфонах стали появляться ещё во времена Android 4.4. Только тогда, из-за отсутствия API, данный сканер мог работать только в приложениях от производителя, и был скорее забавной игрушкой, чем методом авторизации. Всё изменилось с релизом Android 6.0. Было представлено Fingerprint Manager API, с помощью которого разработчики могут реализовать авторизацию по фингерпринту.</p>
  <p>Почитав пару статей на Хабре, я так ничего и не понял. Что? Как? Зачем? Путаные инструкции и нуль конкретики.</p>
  <p>Сегодня я хочу разобраться в API, а поможет мне в этом специальный класс-хелпер который является модификацией одноименного класса из Magisk Manager.</p>
  <p>Подготовка</p>
  <p>Для использования сканера отпечатков ваше приложение должно удовлетворять следующим требованиям:</p>
  <ol>
    <li>Целевая версия SDK 23 или выше</li>
    <li>Разрешение &lt;uses-permission android:name=&quot;android.permission.USE_FINGERPRINT&quot; /&gt; в манифесте</li>
  </ol>
  <h2>Реализация</h2>
  <p>Желательно завести объект-наследник класса android.app.Application, чтобы без труда получить контекст.</p>
  <pre>import android.app.Application;

public class Girl extends Application {
	private static Girl instance;
	
	public Girl(){
		instance = this;
		}

	public static Girl get() {
			return instance;
		}

	@Override
	public void onCreate() {
			// TODO: Implement this method
			super.onCreate();
		}
	
	
}
</pre>
  <p>Зарегистрируйте класс в файле AndroidManifest.xml</p>
  <pre>&lt;application
    android:name=&quot;.Girl&quot;
    ...
    ...
    ...&gt;
...
...
...
&lt;/application&gt;
</pre>
  <p>Создайте класс <strong>FingerprintHelper.java</strong>, который и будет отвечать за работу со сканером:</p>
  <pre>import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;

import java.security.KeyStore;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

@TargetApi(Build.VERSION_CODES.M)
public abstract class FingerprintHelper {

  private FingerprintManager manager;
  private Cipher cipher;
  private CancellationSignal cancel;
	
	private String keystoreTag = &quot;GirlTag&quot;;
	
	// Проверяем наличие сканера
  public static boolean canUseFingerprint() {
    if (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.M)
      return false;
    Girl girl = Girl.get();
    KeyguardManager km = girl.getSystemService(KeyguardManager.class);
    FingerprintManager fm = girl.getSystemService(FingerprintManager.class);
    return km.isKeyguardSecure() &amp;&amp; fm != null &amp;&amp; fm.isHardwareDetected() &amp;&amp; fm.hasEnrolledFingerprints();
  }
	
	// Мы не можем просто так взять и использовать отпечаток без пароля
	// Нам необходимо создать хранилище ключей, по которому будет
	// будет производиться авторизация
  protected FingerprintHelper() throws Exception {
		// Получение контекста
		Girl girl = Girl.get();
		// Создание экземпляра хранилища ключей
    KeyStore keyStore = KeyStore.getInstance(&quot;AndroidKeyStore&quot;);
		// Стучимся к сервису распознавания отпечатков
    manager = girl.getSystemService(FingerprintManager.class);
		// Создание экземпляра шифровальщика
    cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + &quot;/&quot;
        + KeyProperties.BLOCK_MODE_CBC + &quot;/&quot;
        + KeyProperties.ENCRYPTION_PADDING_PKCS7);
		// Загрузка хранилища ключей
    keyStore.load(null);
		// Получаем секретный ключ из загруженного хранилища
		// если отсутствует - создаём
    SecretKey key = (SecretKey) keyStore.getKey(keystoreTag, null);
    if (key == null) {
      key = generateKey();
    }
		// Расшифровка
    try {
      cipher.init(Cipher.ENCRYPT_MODE, key);
    } catch (KeyPermanentlyInvalidatedException e) {
      // Фикс бага Android Marshmallow
      key = generateKey();
      cipher.init(Cipher.ENCRYPT_MODE, key);
    }
  }
	
	// Если в процессе авторизации произошла ошибка
  public abstract void onAuthenticationError(int errorCode, CharSequence errString);
	
	// Получение статуса авторизации
  public abstract void onAuthenticationHelp(int helpCode, CharSequence helpString);
	
	// Успешная авторизация
  public abstract void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result);
	// Ошибка
  public abstract void onAuthenticationFailed();
	
	// Запуск авторизации
  public void startAuth() {
    cancel = new CancellationSignal();
    FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
    manager.authenticate(cryptoObject, cancel, 0, new FingerprintManager.AuthenticationCallback() {
      @Override
      public void onAuthenticationError(int errorCode, CharSequence errString) {
        FingerprintHelper.this.onAuthenticationError(errorCode, errString);
      }

      @Override
      public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
        FingerprintHelper.this.onAuthenticationHelp(helpCode, helpString);
      }

      @Override
      public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
        FingerprintHelper.this.onAuthenticationSucceeded(result);
      }

      @Override
      public void onAuthenticationFailed() {
        FingerprintHelper.this.onAuthenticationFailed();
      }
    }, null);
  }
	
	// Если пользователь отменил операцию
  public void cancel() {
    if (cancel != null)
      cancel.cancel();
  }
	
	// Создание ключа авторизации
  private SecretKey generateKey() throws Exception {
    KeyGenerator keygen = KeyGenerator
        .getInstance(KeyProperties.KEY_ALGORITHM_AES, &quot;AndroidKeyStore&quot;);
    KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
        keystoreTag,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setUserAuthenticationRequired(true)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.N) {
      builder.setInvalidatedByBiometricEnrollment(false);
    }
    keygen.init(builder.build());
    return keygen.generateKey();
  }
}
</pre>
  <p>Я добавил комментарии к коду, так что вопросов возникнуть не должно.</p>
  <p>Пример реализации в Activity:</p>
  <pre>private FingerprintHelper fingerprint;
private TextView warning;

private void init() {
				if (FingerprintHelper.canUseFingerprint()) {
						try {
								fingerprint = new FingerprintHelper() {
										@Override
										public void onAuthenticationError(int errorCode, CharSequence errString) {
												warning.setText(errString);
											}

										@Override
										public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
												warning.setText(helpString);
											}

										@Override
										public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
												warning.setText(&quot;Success!&quot;);
											}

										@Override
										public void onAuthenticationFailed() {
												warning.setText(&quot;Failed!&quot;);
											}
									};
								fingerprint.startAuth();
							} catch (Exception e) {
								e.printStackTrace();

							}
					}
			}

		@Override
		public void finish() {

				if (fingerprint != null)
					fingerprint.cancel();
				super.finish();
			}

</pre>
  <h2>А наглядно?</h2>
  <p>Конечно. Пример приложения и его исходный код вы найдете на канале.</p>
  <figure class="m_custom">
    <img src="https://teletype.in/files/1c/1c38b1b9-a435-4470-945b-c6a713918e8c.png" width="1080" />
  </figure>
  <hr />
  <figure class="m_custom">
    <img src="https://teletype.in/files/7d/7d5bb02a-6637-435d-9db0-0a5c8aa02924.png" width="1080" />
  </figure>
  <p><a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/BkTpAQTOX</guid><link>https://teletype.in/@snowvolf/BkTpAQTOX?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/BkTpAQTOX?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Простейшая проверка подписи на Java</title><pubDate>Mon, 17 Sep 2018 13:40:21 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/bc/bc6aa5c7-b713-4970-9f62-37d134913590.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/59/5946107f-ed07-43b0-949c-ddd5d8d4bcf4.png"></img>Мой канал: @VolfsChannel]]></description><content:encoded><![CDATA[
  <p>Мой канал: <a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>
  <p>Данная проверка является т.н. &quot;классикой жанра&quot; и первым (иногда и последним) рубежом защиты. Данный пример до безобразия прост, но тем и привлекателен. Никаких заумных и запутанных действий, только рабочий код.</p>
  <p>Весь код проверки умещается в одном методе. Имена переменных изменены специально.</p>
  <h2>Реализация</h2>
  <p>Объявляем поля:</p>
  <pre>private static PackageManager pm;
private static String name;
private static String s;
private static byte[] bs;
private static Signature[] sa;
</pre>
  <p>Проверяем подпись:</p>
  <pre>@SuppressLint(&quot;PackageManagerGetSignatures&quot;)
		public boolean b(String st) {
				try {
						pm = getApplicationContext().getPackageManager();
						name = getApplicationContext().getPackageName();
						sa = pm.getPackageInfo(name, PackageManager.GET_SIGNATURES).signatures;
						//Log.w(Constants.TAG, &quot;Signature[] :: &quot; + Arrays.toString(sa));
						for (Signature a$a : sa) {
								//Log.w(Constants.TAG, &quot;byte[] :: &quot; + Arrays.toString(bs));
								bs = a$a.toByteArray();
								//Log.w(Constants.TAG, &quot;new byte[] :: &quot; + Arrays.toString(bs));
								bs = CertificateFactory.getInstance(&quot;X509&quot;).generateCertificate(
									new ByteArrayInputStream(bs)).getEncoded();
								//Log.w(Constants.TAG, &quot;new byte[] encoded :: &quot; + Arrays.toString(bs));
								s = new String(Base64.encode(MessageDigest.getInstance(&quot;MD5&quot;).digest(bs), 19));
								//Log.w(Constants.TAG, &quot;result string :: &quot; + s);
								return Objects.equals(s, st);
							}
					} catch (Exception e) {
						return false;
					}
				return false;
			}
</pre>
  <p>Теперь пройдемся по коду, хотя, те кому надо и так уже поняли...</p>
  <p>На вход поступает <code>String <strong>st</strong>.</code></p>
  <p>Следующие 3 строчки - инициализация объектa <code>PackageManager</code>, у которого мы узнаем наше имя пакета. С помощью этого имени мы получаем массив сигнатур - ту самую подпись.</p>
  <p>Преобразуем нашу подпись в массив байт. Из полученного массива мы создаём сертификат <code>X509</code>. Шифруем MD5 нашего сертификата с помощью <code>Base64</code>, и сравниваем получившийся String с переменной <code><strong>st</strong></code>.</p>
  <p>Что такое <code><strong>st</strong></code>? Как его получить? Создать новый метод с возвращением String, и вернуть <code><strong>s</strong></code></p>
  <h2>А наглядно можно?</h2>
  <p>Конечно. Чтобы продемонстрировать работу кода, я создал простое приложение. Чтобы узнать хэш вашей подписи, просто переподпишите его вашей подписью.</p>
  <p>Подписи совпадают:</p>
  <figure class="m_custom">
    <img src="https://teletype.in/files/59/5946107f-ed07-43b0-949c-ddd5d8d4bcf4.png" width="1080" />
  </figure>
  <p>Подписи не совпадают:</p>
  <figure class="m_custom">
    <img src="https://teletype.in/files/5c/5c8ab5a2-371c-4612-a056-bce2ddb97951.png" width="1080" />
  </figure>
  <p>Скачать данное приложение и его исходный код можно на канале.</p>
  <p><a href="http://t me/VolfsChannel" target="_blank">@VolfsChannel</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/HJ5gODr_X</guid><link>https://teletype.in/@snowvolf/HJ5gODr_X?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/HJ5gODr_X?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Обход запрета на использование reflection в Android Pie</title><pubDate>Tue, 11 Sep 2018 16:06:10 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/f0/f07ee61a-30fe-4926-a4d0-6ffe34b702d3.png"></media:content><description><![CDATA[Мой канал @VolfsChannel]]></description><content:encoded><![CDATA[
  <p>Мой канал <a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>
  <p>Для того чтобы понять, что вообще тут происходит, прочитайте предыдущую статью на канале.</p>
  <h2>Как я могу отключить данные ограничения?</h2>
  <p>Данные ограничения можно отключить на 1-м конкретном устройстве с помощью команд ADB:</p>
  <pre>adb shell settings put global hidden_api_policy_pre_p_apps 1
 adb shell settings put global hidden_api_policy_p_apps 1
</pre>
  <p>Команды принимают следующие входные параметры:</p>
  <ul>
    <li><strong>0</strong>: отключение обнаружения использования non-SDK API. Это также отключит ведение журнала, а также нарушит строгий режим API, detectNonSdkApiUsage(). Не рекомендуется для повседневного использования.</li>
    <li><strong>1</strong>: &quot;только предупреждение&quot; - разрешить доступ ко всем API, не связанным с SDK, но сохранять предупреждения в журнале. Строгий режим API будет продолжать работать.</li>
    <li><strong>2</strong>: запретить использование темно-серого и черного списка API.</li>
    <li><strong>3</strong>: запретить использование черного списка API, но разрешить использование темно-серого списка</li>
  </ul>
  <p>Если у вас нет ПК, то вы можете прописать эти команды в эмуляторе терминала (потребуются права Root):</p>
  <pre>su
settings put global hidden_api_policy_pre_p_apps 1
settings put global hidden_api_policy_p_apps 1
</pre>
  <p>Со стороны ПО вы можете использовать вариант выше через <code>Runtime.getRuntime().exec(&quot;command&quot;)</code></p>
  <p>Или же создать класс Application, и поместить вызов метода в onCreate():</p>
  <pre>public void hackVmPolicy() {
 
		if (Build.VERSION.SDK_INT &gt;= 28) {
 
			StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
 
			StrictMode.setVmPolicy(builder.build());
 
		} else {
 
			Log.i(&quot;Lower android version &quot; + Build.VERSION.SDK_INT);
 
		}
 
	}
</pre>
  <p>Суть этого метода заключается в переопределении политики VM для конкретного приложения. Мы создаём новый (пустой) инстанс политики, и применяем его. Т.к. за рефлексию отвечает метод detectNonSdkApiUsage(), мы можем играть не по правилам.</p>
  <p><a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/BJKIcr0P7</guid><link>https://teletype.in/@snowvolf/BJKIcr0P7?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/BJKIcr0P7?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Reflection в Android Pie</title><pubDate>Mon, 10 Sep 2018 01:55:05 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/f0/f07ee61a-30fe-4926-a4d0-6ffe34b702d3.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/61/61f73adb-b3e9-48de-ad63-bdedbc6e4b47.png"></img>Мой канал: VolfsChannel]]></description><content:encoded><![CDATA[
  <p>Мой канал: <a href="http://t.me/VolfsChannel" target="_blank">VolfsChannel</a></p>
  <p>С каждым годом Android становится всё более закрытой системой. Хотя платформа всё также имеет открытый исходный код, играть не по правилам становится всё труднее. Но у разработчиков есть один полезный инструмент, доставшийся благодаря Java. Его имя - reflection (отражение). Или по просту говоря - прямой доступ даже к приватным методам и классам. Рефлексия позволяет добраться до скрытых API, и извлечь выгоду.</p>
  <hr />
  <h2>Беда не приходит одна...</h2>
  <p>Вероятно у вас возникли следующие вопросы:</p>
  <blockquote>Так что же всё таки произошло?</blockquote>
  <blockquote>Ничего непонятно. Я использую рефлексию, но моё приложение прекрасно работает на Android Pie, где подвох?</blockquote>
  <p>Не так быстро, сейчас мы во всём разберемся...</p>
  <p>Гугл давно стал беспокоиться о совместимости приложений с различными версиями Android. Были созданы библиотеки android-support, которые приносят более новые API в старые версии Android. Помимо поддержки, библиотеки несут в себе расширение функциональных возможностей приложения. Но одних библиотек мало... Даже вместе с SDK и различными сторонними фреймворками, некоторым разработчикам приходится использовать скрытые возможности Android, которые не заявлены в документации. Это представляет 2 потенциальных угрозы:</p>
  <ol>
    <li>Приложение может навредить функциональности устройства, путем использования скрытых &quot;хуков&quot; ОС</li>
    <li>Приложение может полностью или частично перестать работать в результате изменения алгоритма работы скрытого API в новой версии Android, либо же полного его удаления.</li>
  </ol>
  <p>В версии 7.0 Android стал ограничивать использование скрытых интерфейсов NDK с помощью <strong>dlopen</strong>. Запрещается использование скрытых API библиотек находящиеся в <code>/system/lib/</code> (что, впрочем, не мешает скопировать и загрузить библиотеку из другого места). В Android 9.0 Гугл ввёл похожие ограничения на использование скрытых API интерфейсов SDK (т.н. <code>non-SDK interfaces</code>). Так, с этим разобрались. Что же дальше?</p>
  <hr />
  <h2>Быстрее, выше, сильнее</h2>
  <p>Само собой, изменение в использовании такого важного компонента не могут пройти в одночасье. Для этого требуется время, и, что не менее важно - альтернативны скрытым API. Android 9 вводит следующую классификацию методов и полей:</p>
  <ul>
    <li>Белый список - все методы и поля публичного SDK <strong>[74 000 методов и полей]</strong></li>
    <li>Светло-серый список - поля и методы не являющиеся частью SDK, но тем не менее остающиеся доступными <strong>[11 000 методов и полей]</strong></li>
    <li>Тёмно-серый список - для приложений с целевым SDK меньше 28, разрешает использование скрытых методов и полей из этого списка. Для приложений с целевым SDK равным или выше 28 действует аналогично чёрному списку <strong>[121 000 методов и полей]</strong></li>
    <li>Чёрный список - ограничен независимо от целевого SDK. Платформа будет вести себят так как будто интерфейс отсутствует. Например, будет бросать NoSuchMethodError/NoSuchFieldException всякий раз, когда приложение пытается использовать его, и не включать его, когда приложение хочет знать список полей/методов определенного класса. <strong>[9 000 методов и полей]</strong></li>
  </ul>
  <p>Всё это сделано ради нашего же блага, а как же ещё...</p>
  <hr />
  <h2>Результат использования интерфейсов не из пакета SDK (black, dark-gray lists)</h2>
  <p>Согласно таблице на официальном сайте, использование скрытых API SDK повлечёт за собой выброс следующих исключений:</p>
  <figure class="m_custom">
    <img src="https://teletype.in/files/61/61f73adb-b3e9-48de-ad63-bdedbc6e4b47.png" width="2048" />
  </figure>
  <h2>Что же делать мне?</h2>
  <p>Прежде всего, если ваше приложение использует скрытые API, вам необходимо найти замену в лице открытого интерфейса SDK. Если это невозможно, то вам необходимо создать запрос здесь:</p>
  <p><a href="https://issuetracker.google.com/issues/new?component=328403&template=1027267" target="_blank">https://issuetracker.google.com/issues/new?component=328403&amp;template=1027267</a> и подробно описать ваш случай использования конкретного поля/метода.</p>
  <p>Если вы используете сторонние библиотеки, и не уверены в том, что какая-либо из них не использует запрещённые API, то вы можете проверить ваш DEX с помощью утилиты veridex:</p>
  <p><a href="https://android.googlesource.com/platform/prebuilts/runtime/+/master/appcompat" target="_blank">https://android.googlesource.com/platform/prebuilts/runtime/+/master/appcompat</a></p>
  <hr />
  <h2>FAQ</h2>
  <blockquote><strong>Как мне определить, что приложение использует скрытые API?</strong></blockquote>
  <p>При использовании скрытых API, в системный журнал будет печататься сообщение следующего вида:</p>
  <pre>Accessing hidden field|method Lname/of/Class;-&gt;nameOfFieldOrMethod: returnType (method list, source-caller (Java/JNI))
</pre>
  <p>Например:</p>
  <pre>Accessing hidden field Landroid/os/Message;-&gt;flags:I (light greylist, JNI)
</pre>
  <blockquote><strong>Где я могу получить список API из черного/серого списка?</strong></blockquote>
  <p>Они являются частью платформы Вы можете найти готовые записи здесь:</p>
  <p><code>platform/prebuilts/runtime/appcompat/ hiddenapi-light-greylist.txt</code>: список светло-серых API</p>
  <p><code>platform/prebuilts/runtime/appcompat/ hiddenapi-dark-greylist.txt</code>: список темно-серых API</p>
  <p>Дополнительно, это список содержит список светло-серых API.</p>
  <p>Мы добавили правило, создающее списки на AOSP. Это не то же самое, что и черный список в P, но перекрытие достаточно хорошее. Разработчик может скачать исходный код AOSP, а затем сгенерировать черный список с помощью следующей команды:</p>
  <p><code>make hiddenapi-aosp-blacklist</code></p>
  <p>Файл можно будет просмотреть по пути:</p>
  <p><code>out/target/common/obj/PACKAGING/hiddenapi-aosp-blacklist.txt</code></p>
  <blockquote><strong>Каковы примерные размеры этих списков?</strong></blockquote>
  <ul>
    <li>белый список (также известный как SDK) ~= 74 000 методов и полей</li>
    <li>светло-серый ~= 11 000 методов и полей</li>
    <li>тёмно-серый ~= 121 000 методов и полей</li>
    <li>черный список ~= 9 000 методов и полей</li>
  </ul>
  <blockquote><strong>Где я могу найти серый/чёрный список в образе системы?</strong></blockquote>
  <p>Они кодируются в поле и битах флага доступа к методу, в файлах платформы dex. В образе системы, содержащем эти списки, нет отдельного файла.</p>
  <blockquote><strong>Серый/чёрный список является одинаковым на разных устройствах OEM с одинаковыми версиями Android?</strong></blockquote>
  <p>Да, OEM-производители могут добавить свои собственные API в черный список, но не могут удалить методы из оригинального черного/серого списка. CDD предотвращает такие изменения, а CTS тесты гарантируют, что рантайм Android применяет данный список.</p>
  <blockquote><strong>Применяются ли ограничения интерфейсов, отличных от SDK, ко всем приложениям, включая системные и сторонние приложения, а не только к сторонним приложениям?</strong></blockquote>
  <p>Да, однако, мы освобождаем приложения, подписанные ключом платформы, и у нас также есть белый список пакетов для некоторых приложений с системными ресурсами. Обратите внимание, что эти исключения применимы только к приложениям, которые находятся в образе системы (или обновленных приложениях образа системы). Этот список предназначен только для приложений, которые создаются на основе API-интерфейсов частной платформы, а не в SDK API (т. е. <code>LOCAL_PRIVATE_PLATFORM_APIS := true</code>).</p>
  <hr />
  <h2>Как я могу отключить данные ограничения?</h2>
  <p>Данные ограничения можно отключить на 1 конкретном устройстве с помощью... [Продолжение следует]</p>
  <p><a href="http://t.me/VolfsChannel" target="_blank">@VolfsChannel</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/S1mAs-hw7</guid><link>https://teletype.in/@snowvolf/S1mAs-hw7?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/S1mAs-hw7?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Переключение тем в Android приложении</title><pubDate>Tue, 04 Sep 2018 13:42:34 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/55/55709271-1893-4ec9-a13a-eaaee493f0df.jpeg"></media:content><description><![CDATA[Наверное каждому в этом мире охота иметь право выбора. Даже если это лишь выбор цветовой схемы приложения. Тёмная тема легче воспринимается в вечернее время, а также положительно влияет на энергосбережение (в частности на AMOLED дисплеях)]]></description><content:encoded><![CDATA[
  <p>Наверное каждому в этом мире охота иметь право выбора. Даже если это лишь выбор цветовой схемы приложения. Тёмная тема легче воспринимается в вечернее время, а также положительно влияет на энергосбережение (в частности на AMOLED дисплеях)</p>
  <p>Когда-то я хотел сделать изменение темы в приложении &quot;на лету&quot;, но не мог из-за своих малых знаний (от силы 2 месяца программирования). Всё приходит со временем, и, возможно, это тот самый момент, когда вы овладеете полезным навыком.</p>
  <p><strong>Хочу сразу предупредить:</strong></p>
  <p>Пример будет предназначен для новичков, имеющих малый (менее 1 года) опыт разработки под Андроид. В проекте не будет никакой сложной архитектуры, типо MVP, которая только лишь собьёт новичка с толку.</p>
  <p><strong>Подразумевается, что:</strong></p>
  <ol>
    <li>Вы уже знакомы с методами жизненного цикла Activity, умеете запускать и управлять их состоянием</li>
    <li>Вы уже знакомы с классом Fragment, умеете помещать несколько фрагментов внутри одной Activity, и управлять ими</li>
  </ol>
  <h2>Подготовка</h2>
  <p>Перейдите в каталог ресурсов вашего проекта, найдите файл <strong>styles.xml</strong>, и добавьте туда новые темы для приложения.</p>
  <p>Создайте класс <strong>App</strong>, который будет наследоваться от <code>android.app.Application</code>, и напишите следующий код:</p>
  <pre>import android.app.Application;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

public class App extends Application {
  private static App instance = null;
  private SharedPreferences preferences;

  public App() {
    super();
  }

  @Override
  public void onCreate() {
    super.onCreate();
    instance = this;
    // Если в вашем приложении есть экран настроек, замените preferences на нужное имя файла
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
  }

  public static App getInstance(){
    if (instance == null)
      instance = new App();
    return instance;
  }

  public SharedPreferences getPreferences(){
    if (preferences == null)
      preferences = PreferenceManager.getDefaultSharedPreferences(this);
    return preferences;
  }
}
</pre>
  <p>Зарегистрируйте класс в файле <strong>AndroidManifest.xml</strong></p>
  <pre>&lt;application
    android:name=&quot;.App&quot;
    ...
    ...
    .../&gt;
...
...
...
&lt;/application&gt;
</pre>
  <p>Теперь пройдемся по методам. Мы создали экземпляр класса Application, который сам по себе является синглтоном. Переопределив метод <code>onCreate()</code>, присвоили значение переменной <code>instance</code>.</p>
  <p>В методе <code>getInstance()</code> мы просто возвращаем переменную instance, которая является экземпляром класса App.</p>
  <p>В методе <code>getPreferences()</code> мы возвращаем экземпляр <code>SharedPreferences</code>, необходимый для хранения настроек.</p>
  <p>Создайте новый класс, который будет отвечать за смену темы. Я назову его <strong>ThemeWrapper.java</strong>. напишите какой код:</p>
  <pre>import android.app.Activity;

public abstract class ThemeWrapper {
  public enum Theme {
    LIGHT,
    DARK
  }

  public static void applyTheme(Activity ctx) {
    int theme;
    switch (Theme.values()[getThemeIndex()]) {
      case LIGHT:
        theme = R.style.AppTheme;
        break;
      case DARK:
        theme = R.style.AppTheme_Dark;
        break;
      default:
        theme = R.style.AppTheme;
        break;
    }
    ctx.setTheme(theme);
  }

  private static int getThemeIndex() {
    return Integer.parseInt(App.getInstance().getPreferences().getString(&quot;ui.theme&quot;, String.valueOf(ThemeWrapper.Theme.LIGHT.ordinal())));
  }
}
</pre>
  <p>Самостоятельно импортируйте классы <strong>R</strong> и <strong>App</strong>.</p>
  <h2>Внедрение кода</h2>
  <p>Если ваше приложение использует более чем одно Activity, то лучше всего будет создать некий базовый класс, от которого всё будет наследоваться. Я назвал такой класс <strong>BaseActivity.java</strong></p>
  <pre>import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;



public class BaseActivity extends AppCompatActivity {
  //Theme
  private final BroadcastReceiver mThemeReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      if (SettingsActivity.class.equals(BaseActivity.this.getClass())){
        finish();
        startActivity(getIntent());
        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
      } else recreate();
    }
  };

  public BaseActivity(){

  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    //Приёмник сигналов от SettingsActivity
 LocalBroadcastManager.getInstance(this).registerReceiver(mThemeReceiver, new IntentFilter(&quot;ru.svolf.action.REFRESH_THEME&quot;));
    ThemeWrapper.applyTheme(this);
    super.onCreate(savedInstanceState);
  }

  @Override
  public void onResume() {
    super.onResume();
  }

  @Override
  protected void onDestroy() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mThemeReceiver);
    super.onDestroy();
  }
}
</pre>
  <p>Импортируйте класс ThemeWrapper. Если у вас уже есть Activity настроек, то импортируйте и её, в противном случае создайте с нуля.</p>
  <p><strong>/res/layout/activity_settings.xml</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;
  xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
  android:orientation=&quot;vertical&quot;&gt;
  &lt;android.support.design.widget.AppBarLayout
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:theme=&quot;@style/AppTheme.AppBarOverlay&quot;&gt;

    &lt;android.support.v7.widget.Toolbar
      android:id=&quot;@+id/toolbar&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;?attr/actionBarSize&quot;
      android:background=&quot;?attr/colorPrimary&quot;
      app:popupTheme=&quot;?popupTheme&quot; /&gt;
  &lt;/android.support.design.widget.AppBarLayout&gt;
  &lt;FrameLayout
    android:id=&quot;@+id/frame_container&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;/&gt;
&lt;/LinearLayout&gt;
</pre>
  <blockquote>Возможно вам придется добавить библиотеки <code>AppCompat</code> и <code>Design</code>, для корректной работы</blockquote>
  <p><strong>SettingsActivity.java</strong></p>
  <pre>import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;

import ru.SnowVolf.devtheme.R;



public class SettingsActivity extends BaseActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_settings);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    getFragmentManager()
        .beginTransaction()
        .replace(R.id.frame_container, new SettingsFragment())
        .commit();
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()){
      case android.R.id.home:{
        finish();
      }
    }
    return super.onOptionsItemSelected(item);
  }
}
</pre>
  <p>Создадим класс <strong>SettingsFragment.java</strong> с содержимым настроек</p>
  <pre>import android.content.Intent;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.PreferenceFragment;
import android.content.SharedPreferences;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;


public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {

  @Override
  public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.preferences);
    //Регистрация слушателя настроек
 getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    //Ставим текущее значение как подзаголовок настройки

setCurrentValue((ListPreference) findPreference(&quot;ui.theme&quot;));
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    //Отключение слушателя настроек
 getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
  }

  private void setCurrentValue(ListPreference listPreference){
    listPreference.setSummary(listPreference.getEntry());
  }

  @Override
  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    switch (key) {
      case &quot;ui.theme&quot;: {
        setCurrentValue((ListPreference) findPreference(key));
        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(new Intent(&quot;ru.svolf.action.REFRESH_THEME&quot;));
        break;
      }
    }
  }
}
</pre>
  <p>В onCreate мы регистрируем слушателя настроек, для того чтобы данные на экране обновлялись сразу же.</p>
  <p>В методе <code>onSharedPreferenceChanged</code> посылаем сигнал для того, чтобы activity пересоздалась. Чтобы это было менее заметно пользователю, мы добавили метод переопределения системных анимаций в <strong>BaseActivity</strong> (<code>overridePendingTransaction</code>)</p>
  <p><strong>/res/xml/preferences.xml разметка для экрана настроек</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;PreferenceScreen xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

  &lt;ListPreference
    android:defaultValue=&quot;0&quot;
    android:entries=&quot;@array/theme_names&quot;
    android:entryValues=&quot;@array/theme_values&quot;
    android:key=&quot;ui.theme&quot;
    android:title=&quot;App theme&quot; /&gt;
&lt;/PreferenceScreen&gt;
</pre>
  <p><strong>/res/values/arrays.xml Файл с названиями настроек</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;resources&gt;
  &lt;string-array name=&quot;theme_values&quot; translatable=&quot;false&quot;&gt;
    &lt;item&gt;0&lt;/item&gt;
    &lt;item&gt;1&lt;/item&gt;
  &lt;/string-array&gt;
  &lt;string-array name=&quot;theme_names&quot;&gt;
    &lt;item&gt;Light&lt;/item&gt;
    &lt;item&gt;Dark&lt;/item&gt;
  &lt;/string-array&gt;
&lt;/resources&gt;
</pre>
  <blockquote>Ещё раз проверьте, всё ли Activity вы зарегистрировали в манифесте. Сделайте это, если забыли.</blockquote>
  <p>Всё. Теперь вы можете просмотреть на плоды своих трудов, просто скомпилировав проект. Если вы всё сделали правильно, то приложение будет работать как надо.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@snowvolf/rJuyiWnwQ</guid><link>https://teletype.in/@snowvolf/rJuyiWnwQ?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf</link><comments>https://teletype.in/@snowvolf/rJuyiWnwQ?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=snowvolf#comments</comments><dc:creator>snowvolf</dc:creator><title>Переключение тем в Android приложении</title><pubDate>Tue, 04 Sep 2018 13:38:40 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/55/55709271-1893-4ec9-a13a-eaaee493f0df.jpeg"></media:content><description><![CDATA[Наверное каждому в этом мире охота иметь право выбора. Даже если это лишь выбор цветовой схемы приложения. Тёмная тема легче воспринимается в вечернее время, а также положительно влияет на энергосбережение (в частности на AMOLED дисплеях)]]></description><content:encoded><![CDATA[
  <p>Наверное каждому в этом мире охота иметь право выбора. Даже если это лишь выбор цветовой схемы приложения. Тёмная тема легче воспринимается в вечернее время, а также положительно влияет на энергосбережение (в частности на AMOLED дисплеях)</p>
  <p>Когда-то я хотел сделать изменение темы в приложении &quot;на лету&quot;, но не мог из-за своих малых знаний (от силы 2 месяца программирования). Всё приходит со временем, и, возможно, это тот самый момент, когда вы овладеете полезным навыком.</p>
  <p><strong>Хочу сразу предупредить:</strong></p>
  <p>Пример будет предназначен для новичков, имеющих малый (менее 1 года) опыт разработки под Андроид. В проекте не будет никакой сложной архитектуры, типо MVP, которая только лишь собьёт новичка с толку.</p>
  <p><strong>Подразумевается, что:</strong></p>
  <ol>
    <li>Вы уже знакомы с методами жизненного цикла Activity, умеете запускать и управлять их состоянием</li>
    <li>Вы уже знакомы с классом Fragment, умеете помещать несколько фрагментов внутри одной Activity, и управлять ими</li>
  </ol>
  <h2>Подготовка</h2>
  <p>Перейдите в каталог ресурсов вашего проекта, найдите файл <strong>styles.xml</strong>, и добавьте туда новые темы для приложения.</p>
  <p>Создайте класс <strong>App</strong>, который будет наследоваться от <code>android.app.Application</code>, и напишите следующий код:</p>
  <pre>import android.app.Application;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

public class App extends Application {
  private static App instance = null;
  private SharedPreferences preferences;

  public App() {
    super();
  }

  @Override
  public void onCreate() {
    super.onCreate();
    instance = this;
    // Если в вашем приложении есть экран настроек, замените preferences на нужное имя файла
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
  }

  public static App getInstance(){
    if (instance == null)
      instance = new App();
    return instance;
  }

  public SharedPreferences getPreferences(){
    if (preferences == null)
      preferences = PreferenceManager.getDefaultSharedPreferences(this);
    return preferences;
  }
}
</pre>
  <p>Зарегистрируйте класс в файле <strong>AndroidManifest.xml</strong></p>
  <pre>&lt;application
    android:name=&quot;.App&quot;
    ...
    ...
    .../&gt;
...
...
...
&lt;/application&gt;
</pre>
  <p>Теперь пройдемся по методам. Мы создали экземпляр класса Application, который сам по себе является синглтоном. Переопределив метод <code>onCreate()</code>, присвоили значение переменной <code>instance</code>.</p>
  <p>В методе <code>getInstance()</code> мы просто возвращаем переменную instance, которая является экземпляром класса App.</p>
  <p>В методе <code>getPreferences()</code> мы возвращаем экземпляр <code>SharedPreferences</code>, необходимый для хранения настроек.</p>
  <p>Создайте новый класс, который будет отвечать за смену темы. Я назову его <strong>ThemeWrapper.java</strong>. напишите какой код:</p>
  <pre>import android.app.Activity;

public abstract class ThemeWrapper {
  public enum Theme {
    LIGHT,
    DARK
  }

  public static void applyTheme(Activity ctx) {
    int theme;
    switch (Theme.values()[getThemeIndex()]) {
      case LIGHT:
        theme = R.style.AppTheme;
        break;
      case DARK:
        theme = R.style.AppTheme_Dark;
        break;
      default:
        theme = R.style.AppTheme;
        break;
    }
    ctx.setTheme(theme);
  }

  private static int getThemeIndex() {
    return Integer.parseInt(App.getInstance().getPreferences().getString(&quot;ui.theme&quot;, String.valueOf(ThemeWrapper.Theme.LIGHT.ordinal())));
  }
}
</pre>
  <p>Самостоятельно импортируйте классы <strong>R</strong> и <strong>App</strong>.</p>
  <h2>Внедрение кода</h2>
  <p>Если ваше приложение использует более чем одно Activity, то лучше всего будет создать некий базовый класс, от которого всё будет наследоваться. Я назвал такой класс <strong>BaseActivity.java</strong></p>
  <pre>import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;



public class BaseActivity extends AppCompatActivity {
  //Theme
  private final BroadcastReceiver mThemeReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      if (SettingsActivity.class.equals(BaseActivity.this.getClass())){
        finish();
        startActivity(getIntent());
        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
      } else recreate();
    }
  };

  public BaseActivity(){

  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    //Приёмник сигналов от SettingsActivity
 LocalBroadcastManager.getInstance(this).registerReceiver(mThemeReceiver, new IntentFilter(&quot;ru.svolf.action.REFRESH_THEME&quot;));
    ThemeWrapper.applyTheme(this);
    super.onCreate(savedInstanceState);
  }

  @Override
  public void onResume() {
    super.onResume();
  }

  @Override
  protected void onDestroy() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mThemeReceiver);
    super.onDestroy();
  }
}
</pre>
  <p>Импортируйте класс ThemeWrapper. Если у вас уже есть Activity настроек, то импортируйте и её, в противном случае создайте с нуля.</p>
  <p><strong>/res/layout/activity_settings.xml</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;
  xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
  android:orientation=&quot;vertical&quot;&gt;
  &lt;android.support.design.widget.AppBarLayout
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:theme=&quot;@style/AppTheme.AppBarOverlay&quot;&gt;

    &lt;android.support.v7.widget.Toolbar
      android:id=&quot;@+id/toolbar&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;?attr/actionBarSize&quot;
      android:background=&quot;?attr/colorPrimary&quot;
      app:popupTheme=&quot;?popupTheme&quot; /&gt;
  &lt;/android.support.design.widget.AppBarLayout&gt;
  &lt;FrameLayout
    android:id=&quot;@+id/frame_container&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;/&gt;
&lt;/LinearLayout&gt;
</pre>
  <blockquote>Возможно вам придется добавить библиотеки <code>AppCompat</code> и <code>Design</code>, для корректной работы</blockquote>
  <p><strong>SettingsActivity.java</strong></p>
  <pre>import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;

import ru.SnowVolf.devtheme.R;



public class SettingsActivity extends BaseActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_settings);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    getFragmentManager()
        .beginTransaction()
        .replace(R.id.frame_container, new SettingsFragment())
        .commit();
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()){
      case android.R.id.home:{
        finish();
      }
    }
    return super.onOptionsItemSelected(item);
  }
}
</pre>
  <p>Создадим класс <strong>SettingsFragment.java</strong> с содержимым настроек</p>
  <pre>import android.content.Intent;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.PreferenceFragment;
import android.content.SharedPreferences;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;


public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {

  @Override
  public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.preferences);
    //Регистрация слушателя настроек
 getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    //Ставим текущее значение как подзаголовок настройки

setCurrentValue((ListPreference) findPreference(&quot;ui.theme&quot;));
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    //Отключение слушателя настроек
 getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
  }

  private void setCurrentValue(ListPreference listPreference){
    listPreference.setSummary(listPreference.getEntry());
  }

  @Override
  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    switch (key) {
      case &quot;ui.theme&quot;: {
        setCurrentValue((ListPreference) findPreference(key));
        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(new Intent(&quot;ru.svolf.action.REFRESH_THEME&quot;));
        break;
      }
    }
  }
}
</pre>
  <p>В onCreate мы регистрируем слушателя настроек, для того чтобы данные на экране обновлялись сразу же.</p>
  <p>В методе <code>onSharedPreferenceChanged</code> посылаем сигнал для того, чтобы activity пересоздалась. Чтобы это было менее заметно пользователю, мы добавили метод переопределения системных анимаций в <strong>BaseActivity</strong> (<code>overridePendingTransaction</code>)</p>
  <p><strong>/res/xml/preferences.xml разметка для экрана настроек</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;PreferenceScreen xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

  &lt;ListPreference
    android:defaultValue=&quot;0&quot;
    android:entries=&quot;@array/theme_names&quot;
    android:entryValues=&quot;@array/theme_values&quot;
    android:key=&quot;ui.theme&quot;
    android:title=&quot;App theme&quot; /&gt;
&lt;/PreferenceScreen&gt;
</pre>
  <p><strong>/res/values/arrays.xml Файл с названиями настроек</strong></p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;resources&gt;
  &lt;string-array name=&quot;theme_values&quot; translatable=&quot;false&quot;&gt;
    &lt;item&gt;0&lt;/item&gt;
    &lt;item&gt;1&lt;/item&gt;
  &lt;/string-array&gt;
  &lt;string-array name=&quot;theme_names&quot;&gt;
    &lt;item&gt;Light&lt;/item&gt;
    &lt;item&gt;Dark&lt;/item&gt;
  &lt;/string-array&gt;
&lt;/resources&gt;
</pre>
  <blockquote>Ещё раз проверьте, всё ли Activity вы зарегистрировали в манифесте. Сделайте это, если забыли.</blockquote>
  <p>Всё. Теперь вы можете просмотреть на плоды своих трудов, просто скомпилировав проект. Если вы всё сделали правильно, то приложение будет работать как надо.</p>

]]></content:encoded></item></channel></rss>