[tor-commits] [orbot/master] First stab at modularizing shared code between the app and app mini projects

n8fr8 at torproject.org n8fr8 at torproject.org
Wed Oct 7 12:37:35 UTC 2020


commit 17178d23d1578a7586cc441065728d73c5e11eb9
Author: bim <dsnake at protonmail.com>
Date:   Tue Sep 8 13:01:19 2020 -0400

    First stab at modularizing shared code between the app and app mini projects
---
 app-mini/build.gradle                              |   6 +-
 app-mini/src/debug/AndroidManifest.xml             |   2 +-
 app-mini/src/main/AndroidManifest.xml              |  29 +-
 .../integration/android/IntentIntegrator.java      | 506 -----------
 .../org/torproject/android/mini/MainConstants.java |  15 -
 .../torproject/android/mini/MiniMainActivity.java  | 995 +++++++++------------
 .../torproject/android/mini/OnBootReceiver.java    |  39 -
 .../org/torproject/android/mini/OrbotMiniApp.java  |  79 +-
 .../android/mini/settings/Languages.java           | 228 -----
 .../android/mini/settings/LocaleHelper.java        |  86 --
 .../android/mini/settings/SettingsPreferences.java |  72 --
 .../mini/ui/NoPersonalizedLearningEditText.java    |  13 -
 .../android/mini/ui/Rotate3dAnimation.java         |  76 --
 .../mini/ui/onboarding/OnboardingActivity.java     |   3 +-
 .../src/main/res/layout/content_app_config.xml     |   1 -
 app/build.gradle                                   |   2 +
 app/src/main/AndroidManifest.xml                   |   4 +-
 .../zxing/integration/android/IntentResult.java    |  95 --
 .../org/torproject/android/OnBootReceiver.java     |  42 -
 .../main/java/org/torproject/android/OrbotApp.java |   6 +-
 .../org/torproject/android/OrbotMainActivity.java  |  12 +-
 .../org/torproject/android/settings/Languages.java | 195 ----
 .../torproject/android/settings/LocaleHelper.java  |  80 --
 .../android/settings/SettingsPreferences.java      |  68 --
 .../android/ui/NoPersonalizedLearningEditText.java |  13 -
 .../torproject/android/ui/Rotate3dAnimation.java   |  76 --
 .../ui/hiddenservices/ClientCookiesActivity.java   |   2 +-
 .../ui/hiddenservices/HiddenServicesActivity.java  |   2 +-
 .../ui/hiddenservices/backup/BackupUtils.java      |   2 +-
 .../dialogs/SelectCookieBackupDialog.java          |   2 +-
 .../dialogs/SelectHSBackupDialog.java              |   2 +-
 .../ui/hiddenservices/storage/ExternalStorage.java |  28 -
 .../ui/onboarding/BridgeWizardActivity.java        |   4 +-
 .../android/ui/onboarding/OnboardingActivity.java  |   2 +-
 .../res/layout/layout_add_client_cookie_dialog.xml |   4 +-
 app/src/main/res/layout/layout_hs_data_dialog.xml  |   6 +-
 appcore/.gitignore                                 |   1 +
 appcore/build.gradle                               |  41 +
 appcore/consumer-rules.pro                         |   0
 appcore/proguard-rules.pro                         |  21 +
 .../android/core/ExampleInstrumentedTest.kt        |  24 +
 appcore/src/main/AndroidManifest.xml               |   3 +
 .../org/torproject/android/core/ExternalStorage.kt |  15 +
 .../java/org/torproject/android/core/Languages.kt  | 174 ++++
 .../org/torproject/android/core/LocaleHelper.kt    |  55 ++
 .../org/torproject/android/core/OnBootReceiver.kt  |  31 +
 .../core/ui/NoPersonalizedLearningEditText.kt      |  12 +
 .../android/core/ui/Rotate3dAnimation.kt           |  48 +
 .../android/core/ui/SettingsPreferencesActivity.kt |  65 ++
 .../org/torproject/android/core/ExampleUnitTest.kt |  17 +
 build.gradle                                       |   4 +
 intentintegrator/.gitignore                        |   1 +
 intentintegrator/build.gradle                      |  30 +
 intentintegrator/consumer-rules.pro                |   0
 intentintegrator/proguard-rules.pro                |  21 +
 intentintegrator/src/main/AndroidManifest.xml      |   5 +
 .../integration/android/IntentIntegrator.java      |   0
 .../zxing/integration/android/IntentResult.java    |   0
 settings.gradle                                    |   2 +
 59 files changed, 1017 insertions(+), 2350 deletions(-)

diff --git a/app-mini/build.gradle b/app-mini/build.gradle
index 140011cc..6cf3d7f3 100644
--- a/app-mini/build.gradle
+++ b/app-mini/build.gradle
@@ -109,8 +109,12 @@ dependencies {
     implementation project(':orbotservice')
     implementation 'com.github.apl-devs:appintro:v4.2.2'
     implementation 'androidx.palette:palette:1.0.0'
-    implementation 'com.github.javiersantos:AppUpdater:2.7'
     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    implementation project(path: ':appcore')
+    implementation 'androidx.recyclerview:recyclerview:1.1.0'
+    implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
+    implementation 'com.google.android.material:material:1.2.1'
+    implementation project(path: ':intentintegrator')
     androidTestImplementation "tools.fastlane:screengrab:1.2.0"
 }
 
diff --git a/app-mini/src/debug/AndroidManifest.xml b/app-mini/src/debug/AndroidManifest.xml
index a04afa83..a06b3fe8 100644
--- a/app-mini/src/debug/AndroidManifest.xml
+++ b/app-mini/src/debug/AndroidManifest.xml
@@ -72,7 +72,7 @@
             android:exported="false"
             android:label="@string/app_name" />
         <activity
-            android:name=".settings.SettingsPreferences"
+            android:name=".settings.SettingsPreferencesActivity"
             android:label="@string/app_name" />
         <activity
             android:name=".ui.AppManagerActivity"
diff --git a/app-mini/src/main/AndroidManifest.xml b/app-mini/src/main/AndroidManifest.xml
index 6e73081a..dda8d0bf 100644
--- a/app-mini/src/main/AndroidManifest.xml
+++ b/app-mini/src/main/AndroidManifest.xml
@@ -51,7 +51,7 @@
             android:stateNotNeeded="true"
             android:theme="@android:style/Theme.Translucent" />
         <activity
-            android:name=".settings.SettingsPreferences"
+            android:name="org.torproject.android.core.ui.SettingsPreferencesActivity"
             android:label="@string/app_name" />
         <activity
             android:name=".ui.AppManagerActivity"
@@ -61,36 +61,11 @@
         <activity android:name=".ui.onboarding.OnboardingActivity" />
 
 
-        <receiver
-            android:name=".OnBootReceiver"
-            android:enabled="true"
-            android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-
-                <category android:name="android.intent.category.HOME" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.intent.action.QUICKBOOT_POWERON" />
-
-                <category android:name="android.intent.category.HOME" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.intent.action.MEDIA_MOUNTED" />
-
-                <category android:name="android.intent.category.HOME" />
-            </intent-filter>
-        </receiver>
-
         <service
             android:name="org.torproject.android.service.OrbotService"
             android:enabled="true"
             android:permission="android.permission.BIND_VPN_SERVICE"
-            android:stopWithTask="false" />
-        <service
-            android:name="org.torproject.android.service.vpn.TorVpnService"
-            android:enabled="true"
-            android:permission="android.permission.BIND_VPN_SERVICE">
+            android:stopWithTask="false">
             <intent-filter>
                 <action android:name="android.net.VpnService" />
             </intent-filter>
diff --git a/app-mini/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java b/app-mini/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java
deleted file mode 100644
index d5628e87..00000000
--- a/app-mini/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java
+++ /dev/null
@@ -1,506 +0,0 @@
-/*
- * Copyright 2009 ZXing authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.zxing.integration.android;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Fragment;
-import android.content.ActivityNotFoundException;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.Uri;
-import android.os.Bundle;
-import android.util.Log;
-
-/**
- * <p>A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple
- * way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the
- * project's source code.</p>
- *
- * <h2>Initiating a barcode scan</h2>
- *
- * <p>To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait
- * for the result in your app.</p>
- *
- * <p>It does require that the Barcode Scanner (or work-alike) application is installed. The
- * {@link #initiateScan()} method will prompt the user to download the application, if needed.</p>
- *
- * <p>There are a few steps to using this integration. First, your {@link Activity} must implement
- * the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:</p>
- *
- * <pre>{@code
- * public void onActivityResult(int requestCode, int resultCode, Intent intent) {
- *   IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
- *   if (scanResult != null) {
- *     // handle scan result
- *   }
- *   // else continue with any other code you need in the method
- *   ...
- * }
- * }</pre>
- *
- * <p>This is where you will handle a scan result.</p>
- *
- * <p>Second, just call this in response to a user action somewhere to begin the scan process:</p>
- *
- * <pre>{@code
- * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
- * integrator.initiateScan();
- * }</pre>
- *
- * <p>Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the
- * user was prompted to download the application. This lets the calling app potentially manage the dialog.
- * In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()}
- * method.</p>
- * 
- * <p>You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use
- * {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and
- * yes/no button labels can be changed.</p>
- *
- * <p>Finally, you can use {@link #addExtra(String, Object)} to add more parameters to the Intent used
- * to invoke the scanner. This can be used to set additional options not directly exposed by this
- * simplified API.</p>
- * 
- * <p>By default, this will only allow applications that are known to respond to this intent correctly
- * do so. The apps that are allowed to response can be set with {@link #setTargetApplications(List)}.
- * For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.</p>
- *
- * <h2>Sharing text via barcode</h2>
- *
- * <p>To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.</p>
- *
- * <p>Some code, particularly download integration, was contributed from the Anobiit application.</p>
- *
- * <h2>Enabling experimental barcode formats</h2>
- *
- * <p>Some formats are not enabled by default even when scanning with {@link #ALL_CODE_TYPES}, such as
- * PDF417. Use {@link #initiateScan(java.util.Collection)} with
- * a collection containing the names of formats to scan for explicitly, like "PDF_417", to use such
- * formats.</p>
- *
- * @author Sean Owen
- * @author Fred Lin
- * @author Isaac Potoczny-Jones
- * @author Brad Drehmer
- * @author gcstang
- */
-public class IntentIntegrator {
-
-  public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits
-  private static final String TAG = IntentIntegrator.class.getSimpleName();
-
-  public static final String DEFAULT_TITLE = "Install Barcode Scanner?";
-  public static final String DEFAULT_MESSAGE =
-      "This application requires Barcode Scanner. Would you like to install it?";
-  public static final String DEFAULT_YES = "Yes";
-  public static final String DEFAULT_NO = "No";
-
-  private static final String BS_PACKAGE = "com.google.zxing.client.android";
-  private static final String BSPLUS_PACKAGE = "com.srowen.bs.android";
-
-  // supported barcode formats
-  public static final Collection<String> PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14");
-  public static final Collection<String> ONE_D_CODE_TYPES =
-      list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128",
-           "ITF", "RSS_14", "RSS_EXPANDED");
-  public static final Collection<String> QR_CODE_TYPES = Collections.singleton("QR_CODE");
-  public static final Collection<String> DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX");
-
-  public static final Collection<String> ALL_CODE_TYPES = null;
-  
-  public static final List<String> TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE);
-  public static final List<String> TARGET_ALL_KNOWN = list(
-          BSPLUS_PACKAGE,             // Barcode Scanner+
-          BSPLUS_PACKAGE + ".simple", // Barcode Scanner+ Simple
-          BS_PACKAGE                  // Barcode Scanner          
-          // What else supports this intent?
-      );
-  
-  private final Activity activity;
-  private final Fragment fragment;
-
-  private String title;
-  private String message;
-  private String buttonYes;
-  private String buttonNo;
-  private List<String> targetApplications;
-  private final Map<String,Object> moreExtras = new HashMap<String,Object>(3);
-
-  /**
-   * @param activity {@link Activity} invoking the integration
-   */
-  public IntentIntegrator(Activity activity) {
-    this.activity = activity;
-    this.fragment = null;
-    initializeConfiguration();
-  }
-
-  /**
-   * @param fragment {@link Fragment} invoking the integration.
-   *  {@link #startActivityForResult(Intent, int)} will be called on the {@link Fragment} instead
-   *  of an {@link Activity}
-   */
-  public IntentIntegrator(Fragment fragment) {
-    this.activity = fragment.getActivity();
-    this.fragment = fragment;
-    initializeConfiguration();
-  }
-
-  private void initializeConfiguration() {
-    title = DEFAULT_TITLE;
-    message = DEFAULT_MESSAGE;
-    buttonYes = DEFAULT_YES;
-    buttonNo = DEFAULT_NO;
-    targetApplications = TARGET_ALL_KNOWN;
-  }
-  
-  public String getTitle() {
-    return title;
-  }
-  
-  public void setTitle(String title) {
-    this.title = title;
-  }
-
-  public void setTitleByID(int titleID) {
-    title = activity.getString(titleID);
-  }
-
-  public String getMessage() {
-    return message;
-  }
-
-  public void setMessage(String message) {
-    this.message = message;
-  }
-
-  public void setMessageByID(int messageID) {
-    message = activity.getString(messageID);
-  }
-
-  public String getButtonYes() {
-    return buttonYes;
-  }
-
-  public void setButtonYes(String buttonYes) {
-    this.buttonYes = buttonYes;
-  }
-
-  public void setButtonYesByID(int buttonYesID) {
-    buttonYes = activity.getString(buttonYesID);
-  }
-
-  public String getButtonNo() {
-    return buttonNo;
-  }
-
-  public void setButtonNo(String buttonNo) {
-    this.buttonNo = buttonNo;
-  }
-
-  public void setButtonNoByID(int buttonNoID) {
-    buttonNo = activity.getString(buttonNoID);
-  }
-  
-  public Collection<String> getTargetApplications() {
-    return targetApplications;
-  }
-  
-  public final void setTargetApplications(List<String> targetApplications) {
-    if (targetApplications.isEmpty()) {
-      throw new IllegalArgumentException("No target applications");
-    }
-    this.targetApplications = targetApplications;
-  }
-  
-  public void setSingleTargetApplication(String targetApplication) {
-    this.targetApplications = Collections.singletonList(targetApplication);
-  }
-
-  public Map<String,?> getMoreExtras() {
-    return moreExtras;
-  }
-
-  public final void addExtra(String key, Object value) {
-    moreExtras.put(key, value);
-  }
-
-  /**
-   * Initiates a scan for all known barcode types with the default camera.
-   *
-   * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
-   *   if a prompt was needed, or null otherwise.
-   */
-  public final AlertDialog initiateScan() {
-    return initiateScan(ALL_CODE_TYPES, -1);
-  }
-  
-  /**
-   * Initiates a scan for all known barcode types with the specified camera.
-   *
-   * @param cameraId camera ID of the camera to use. A negative value means "no preference".
-   * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
-   *   if a prompt was needed, or null otherwise.
-   */
-  public final AlertDialog initiateScan(int cameraId) {
-    return initiateScan(ALL_CODE_TYPES, cameraId);
-  }
-
-  /**
-   * Initiates a scan, using the default camera, only for a certain set of barcode types, given as strings corresponding
-   * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
-   * like {@link #PRODUCT_CODE_TYPES} for example.
-   *
-   * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
-   * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
-   *   if a prompt was needed, or null otherwise.
-   */
-  public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats) {
-    return initiateScan(desiredBarcodeFormats, -1);
-  }
-  
-  /**
-   * Initiates a scan, using the specified camera, only for a certain set of barcode types, given as strings corresponding
-   * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
-   * like {@link #PRODUCT_CODE_TYPES} for example.
-   *
-   * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
-   * @param cameraId camera ID of the camera to use. A negative value means "no preference".
-   * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
-   *   if a prompt was needed, or null otherwise
-   */
-  public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats, int cameraId) {
-    Intent intentScan = new Intent(BS_PACKAGE + ".SCAN");
-    intentScan.addCategory(Intent.CATEGORY_DEFAULT);
-
-    // check which types of codes to scan for
-    if (desiredBarcodeFormats != null) {
-      // set the desired barcode types
-      StringBuilder joinedByComma = new StringBuilder();
-      for (String format : desiredBarcodeFormats) {
-        if (joinedByComma.length() > 0) {
-          joinedByComma.append(',');
-        }
-        joinedByComma.append(format);
-      }
-      intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString());
-    }
-
-    // check requested camera ID
-    if (cameraId >= 0) {
-      intentScan.putExtra("SCAN_CAMERA_ID", cameraId);
-    }
-
-    String targetAppPackage = findTargetAppPackage(intentScan);
-    if (targetAppPackage == null) {
-      return showDownloadDialog();
-    }
-    intentScan.setPackage(targetAppPackage);
-    intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-    intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
-    attachMoreExtras(intentScan);
-    startActivityForResult(intentScan, REQUEST_CODE);
-    return null;
-  }
-
-  /**
-   * Start an activity. This method is defined to allow different methods of activity starting for
-   * newer versions of Android and for compatibility library.
-   *
-   * @param intent Intent to start.
-   * @param code Request code for the activity
-   * @see android.app.Activity#startActivityForResult(Intent, int)
-   * @see android.app.Fragment#startActivityForResult(Intent, int)
-   */
-  protected void startActivityForResult(Intent intent, int code) {
-    if (fragment == null) {
-      activity.startActivityForResult(intent, code);
-    } else {
-      fragment.startActivityForResult(intent, code);
-    }
-  }
-  
-  private String findTargetAppPackage(Intent intent) {
-    PackageManager pm = activity.getPackageManager();
-    List<ResolveInfo> availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
-    if (availableApps != null) {
-      for (String targetApp : targetApplications) {
-        if (contains(availableApps, targetApp)) {
-          return targetApp;
-        }
-      }
-    }
-    return null;
-  }
-  
-  private static boolean contains(Iterable<ResolveInfo> availableApps, String targetApp) {
-    for (ResolveInfo availableApp : availableApps) {
-      String packageName = availableApp.activityInfo.packageName;
-      if (targetApp.equals(packageName)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private AlertDialog showDownloadDialog() {
-    AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
-    downloadDialog.setTitle(title);
-    downloadDialog.setMessage(message);
-    downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() {
-      @Override
-      public void onClick(DialogInterface dialogInterface, int i) {
-        String packageName;
-        if (targetApplications.contains(BS_PACKAGE)) {
-          // Prefer to suggest download of BS if it's anywhere in the list
-          packageName = BS_PACKAGE;
-        } else {
-          // Otherwise, first option:
-          packageName = targetApplications.get(0);
-        }
-        Uri uri = Uri.parse("market://details?id=" + packageName);
-        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-        try {
-          if (fragment == null) {
-            activity.startActivity(intent);
-          } else {
-            fragment.startActivity(intent);
-          }
-        } catch (ActivityNotFoundException anfe) {
-          // Hmm, market is not installed
-          Log.w(TAG, "Google Play is not installed; cannot install " + packageName);
-        }
-      }
-    });
-    downloadDialog.setNegativeButton(buttonNo, null);
-    downloadDialog.setCancelable(true);
-    return downloadDialog.show();
-  }
-
-
-  /**
-   * <p>Call this from your {@link Activity}'s
-   * {@link Activity#onActivityResult(int, int, Intent)} method.</p>
-   *
-   * @param requestCode request code from {@code onActivityResult()}
-   * @param resultCode result code from {@code onActivityResult()}
-   * @param intent {@link Intent} from {@code onActivityResult()}
-   * @return null if the event handled here was not related to this class, or
-   *  else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning,
-   *  the fields will be null.
-   */
-  public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) {
-    if (requestCode == REQUEST_CODE) {
-      if (resultCode == Activity.RESULT_OK) {
-        String contents = intent.getStringExtra("SCAN_RESULT");
-        String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT");
-        byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES");
-        int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE);
-        Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation;
-        String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL");
-        return new IntentResult(contents,
-                                formatName,
-                                rawBytes,
-                                orientation,
-                                errorCorrectionLevel);
-      }
-      return new IntentResult();
-    }
-    return null;
-  }
-
-
-  /**
-   * Defaults to type "TEXT_TYPE".
-   *
-   * @param text the text string to encode as a barcode
-   * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
-   *   if a prompt was needed, or null otherwise
-   * @see #shareText(CharSequence, CharSequence)
-   */
-  public final AlertDialog shareText(CharSequence text) {
-    return shareText(text, "TEXT_TYPE");
-  }
-
-  /**
-   * Shares the given text by encoding it as a barcode, such that another user can
-   * scan the text off the screen of the device.
-   *
-   * @param text the text string to encode as a barcode
-   * @param type type of data to encode. See {@code com.google.zxing.client.android.Contents.Type} constants.
-   * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
-   *   if a prompt was needed, or null otherwise
-   */
-  public final AlertDialog shareText(CharSequence text, CharSequence type) {
-    Intent intent = new Intent();
-    intent.addCategory(Intent.CATEGORY_DEFAULT);
-    intent.setAction(BS_PACKAGE + ".ENCODE");
-    intent.putExtra("ENCODE_TYPE", type);
-    intent.putExtra("ENCODE_DATA", text);
-    String targetAppPackage = findTargetAppPackage(intent);
-    if (targetAppPackage == null) {
-      return showDownloadDialog();
-    }
-    intent.setPackage(targetAppPackage);
-    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
-    attachMoreExtras(intent);
-    if (fragment == null) {
-      activity.startActivity(intent);
-    } else {
-      fragment.startActivity(intent);
-    }
-    return null;
-  }
-  
-  private static List<String> list(String... values) {
-    return Collections.unmodifiableList(Arrays.asList(values));
-  }
-
-  private void attachMoreExtras(Intent intent) {
-    for (Map.Entry<String,Object> entry : moreExtras.entrySet()) {
-      String key = entry.getKey();
-      Object value = entry.getValue();
-      // Kind of hacky
-      if (value instanceof Integer) {
-        intent.putExtra(key, (Integer) value);
-      } else if (value instanceof Long) {
-        intent.putExtra(key, (Long) value);
-      } else if (value instanceof Boolean) {
-        intent.putExtra(key, (Boolean) value);
-      } else if (value instanceof Double) {
-        intent.putExtra(key, (Double) value);
-      } else if (value instanceof Float) {
-        intent.putExtra(key, (Float) value);
-      } else if (value instanceof Bundle) {
-        intent.putExtra(key, (Bundle) value);
-      } else {
-        intent.putExtra(key, value.toString());
-      }
-    }
-  }
-
-}
diff --git a/app-mini/src/main/java/org/torproject/android/mini/MainConstants.java b/app-mini/src/main/java/org/torproject/android/mini/MainConstants.java
deleted file mode 100644
index b4d64147..00000000
--- a/app-mini/src/main/java/org/torproject/android/mini/MainConstants.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.torproject.android.mini;
-
-public interface MainConstants {
-
-    //EXIT COUNTRY CODES
-    String[] COUNTRY_CODES = {"DE","AT","SE","CH","IS","CA","US","ES","FR","BG","PL","AU","BR","CZ","DK","FI","GB","HU","NL","JP","RO","RU","SG","SK"};
-
-    //path to check Tor against
-    String URL_TOR_CHECK = "https://check.torproject.org";
-
-    String URL_TOR_BRIDGES = "https://bridges.torproject.org/bridges?transport=";
-
-    int RESULT_CLOSE_ALL = 0;
-
-}
diff --git a/app-mini/src/main/java/org/torproject/android/mini/MiniMainActivity.java b/app-mini/src/main/java/org/torproject/android/mini/MiniMainActivity.java
index 4566031d..26b17fdf 100644
--- a/app-mini/src/main/java/org/torproject/android/mini/MiniMainActivity.java
+++ b/app-mini/src/main/java/org/torproject/android/mini/MiniMainActivity.java
@@ -3,8 +3,6 @@
 
 package org.torproject.android.mini;
 
-import android.app.ActivityManager;
-import android.app.ActivityManager.RunningServiceInfo;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -39,6 +37,7 @@ import android.view.animation.AccelerateInterpolator;
 import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.TextView;
+
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.SwitchCompat;
 import androidx.appcompat.widget.Toolbar;
@@ -47,15 +46,17 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.palette.graphics.Palette;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
+
 import com.google.zxing.integration.android.IntentIntegrator;
 import com.google.zxing.integration.android.IntentResult;
+
 import org.json.JSONArray;
-import org.torproject.android.mini.settings.Languages;
-import org.torproject.android.mini.settings.LocaleHelper;
-import org.torproject.android.mini.settings.SettingsPreferences;
+import org.torproject.android.core.Languages;
+import org.torproject.android.core.LocaleHelper;
+import org.torproject.android.core.ui.Rotate3dAnimation;
+import org.torproject.android.core.ui.SettingsPreferencesActivity;
 import org.torproject.android.mini.ui.AppConfigActivity;
 import org.torproject.android.mini.ui.AppManagerActivity;
-import org.torproject.android.mini.ui.Rotate3dAnimation;
 import org.torproject.android.mini.ui.onboarding.OnboardingActivity;
 import org.torproject.android.service.OrbotConstants;
 import org.torproject.android.service.OrbotService;
@@ -73,108 +74,83 @@ import java.util.ArrayList;
 import java.util.Locale;
 import java.util.StringTokenizer;
 
-import static org.torproject.android.mini.MainConstants.RESULT_CLOSE_ALL;
 import static org.torproject.android.service.vpn.VpnPrefs.PREFS_KEY_TORIFIED;
 
-public class MiniMainActivity extends AppCompatActivity
-        implements OrbotConstants, OnLongClickListener {
+public class MiniMainActivity extends AppCompatActivity implements OrbotConstants, OnLongClickListener {
 
+    private static final int RESULT_CLOSE_ALL = 0;
+    private final static int REQUEST_VPN = 8888;
+    private final static int REQUEST_SETTINGS = 0x9874;
+    private final static int REQUEST_VPN_APPS_SELECT = 8889;
+    private final static int LOG_DRAWER_GRAVITY = Gravity.END;
+    // message types for mStatusUpdateHandler
+    private final static int STATUS_UPDATE = 1;
+    private static final int MESSAGE_TRAFFIC_COUNT = 2;
+    private static final int MESSAGE_PORTS = 3;
+    private static final float ROTATE_FROM = 0.0f;
+    private static final float ROTATE_TO = 360.0f * 4f;// 3.141592654f * 32.0f;
+    ArrayList<String> pkgIds = new ArrayList<>();
+    AlertDialog aDialog = null;
     /* Useful UI bits */
 //    private TextView lblStatus = null; //the main text display widget
     private ImageView imgStatus = null; //the main touchable image for activating Orbot
-
     private TextView downloadText = null;
     private TextView uploadText = null;
     private TextView mTxtOrbotLog = null;
-
-	private SwitchCompat mBtnVPN = null;
-
-	private DrawerLayout mDrawer;
-
+    private SwitchCompat mBtnVPN = null;
+    private DrawerLayout mDrawer;
     /* Some tracking bits */
     private String torStatus = null; //latest status reported from the tor service
     private Intent lastStatusIntent;  // the last ACTION_STATUS Intent received
-
     private SharedPreferences mPrefs = null;
-
     private boolean autoStartFromIntent = false;
-    
-    private final static int REQUEST_VPN = 8888;
-    private final static int REQUEST_SETTINGS = 0x9874;
-    private final static int REQUEST_VPN_APPS_SELECT = 8889;
-
-    private final static int LOG_DRAWER_GRAVITY = Gravity.END;
-
-    // message types for mStatusUpdateHandler
-    private final static int STATUS_UPDATE = 1;
-    private static final int MESSAGE_TRAFFIC_COUNT = 2;
-    private static final int MESSAGE_PORTS = 3;
-
-
     private RecyclerView rv;
+    // this is what takes messages or values from the callback threads or other non-mainUI threads
+//and passes them back into the main UI thread for display to the user
+    private Handler mStatusUpdateHandler = new Handler() {
 
-    ArrayList<String> pkgIds = new ArrayList<>();
-
-    /**
-     * Called when the activity is first created.
-     */
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        mPrefs = Prefs.getSharedPrefs(getApplicationContext());
-
-        /* Create the widgets before registering for broadcasts to guarantee
-         * that the widgets exist when the status updates try to update them */
-    	doLayout();
-
-    	/* receive the internal status broadcasts, which are separate from the public
-    	 * status broadcasts to prevent other apps from sending fake/wrong status
-    	 * info to this app */
-        LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
-        lbm.registerReceiver(mLocalBroadcastReceiver,
-                new IntentFilter(TorServiceConstants.ACTION_STATUS));
-        lbm.registerReceiver(mLocalBroadcastReceiver,
-                new IntentFilter(TorServiceConstants.LOCAL_ACTION_BANDWIDTH));
-        lbm.registerReceiver(mLocalBroadcastReceiver,
-                new IntentFilter(TorServiceConstants.LOCAL_ACTION_LOG));
-        lbm.registerReceiver(mLocalBroadcastReceiver,
-                new IntentFilter(TorServiceConstants.LOCAL_ACTION_PORTS));
-
+        @Override
+        public void handleMessage(final Message msg) {
 
 
-        boolean showFirstTime = mPrefs.getBoolean("connect_first_time", true);
+            Bundle data = msg.getData();
 
-        if (showFirstTime)
-        {
-            Editor pEdit = mPrefs.edit();
-            pEdit.putBoolean("connect_first_time", false);
-            pEdit.commit();
-            startActivity(new Intent(this,OnboardingActivity.class));
-        }
+            switch (msg.what) {
+                case MESSAGE_TRAFFIC_COUNT:
 
-        /**
-         * Resets previous DNS Port to the default
-         */
-        mPrefs.edit().putInt(VpnPrefs.PREFS_DNS_PORT,
-                TorServiceConstants.TOR_DNS_PORT_DEFAULT).apply();
+                    DataCount datacount = new DataCount(data.getLong("upload"), data.getLong("download"));
 
-    }
+                    long totalRead = data.getLong("readTotal");
+                    long totalWrite = data.getLong("writeTotal");
 
-    private void sendIntentToService(final String action) {
+//                    downloadText.setText(formatCount(datacount.Download) + " / " + formatTotal(totalRead));
+                    //                   uploadText.setText(formatCount(datacount.Upload) + " / " + formatTotal(totalWrite));
 
-        Intent intent = new Intent(MiniMainActivity.this, OrbotService.class);
-        intent.setAction(action);
-        startService(intent);
+                    downloadText.setText(formatTotal(totalRead) + " \u2193");
+                    uploadText.setText(formatTotal(totalWrite) + " \u2191");
 
-    }
+                    break;
+                case MESSAGE_PORTS:
 
-    private void stopTor() {
+                    int socksPort = data.getInt("socks");
+                    int httpPort = data.getInt("http");
 
-        Intent intent = new Intent(MiniMainActivity.this, OrbotService.class);
-        stopService(intent);
+                    break;
+                default:
+                    String newTorStatus = msg.getData().getString("status");
+                    String log = (String) msg.obj;
 
-    }
+                    if (torStatus == null && newTorStatus != null) //first time status
+                    {
+                        updateStatus(log, newTorStatus);
 
+                    } else
+                        updateStatus(log, newTorStatus);
+                    super.handleMessage(msg);
+                    break;
+            }
+        }
+    };
     /**
      * The state and log info from {@link OrbotService} are sent to the UI here in
      * the form of a local broadcast. Regular broadcasts can be sent by any app,
@@ -212,61 +188,171 @@ public class MiniMainActivity extends AppCompatActivity
 
             } else if (action.equals(TorServiceConstants.ACTION_STATUS)) {
                 lastStatusIntent = intent;
-                
+
                 Message msg = mStatusUpdateHandler.obtainMessage(STATUS_UPDATE);
                 msg.getData().putString("status", intent.getStringExtra(TorServiceConstants.EXTRA_STATUS));
 
                 mStatusUpdateHandler.sendMessage(msg);
-            }
-            else if (action.equals(TorServiceConstants.LOCAL_ACTION_PORTS)) {
+            } else if (action.equals(TorServiceConstants.LOCAL_ACTION_PORTS)) {
 
                 Message msg = mStatusUpdateHandler.obtainMessage(MESSAGE_PORTS);
-                msg.getData().putInt("socks",intent.getIntExtra(OrbotService.EXTRA_SOCKS_PROXY_PORT,-1));
-                msg.getData().putInt("http",intent.getIntExtra(OrbotService.EXTRA_HTTP_PROXY_PORT,-1));
+                msg.getData().putInt("socks", intent.getIntExtra(OrbotService.EXTRA_SOCKS_PROXY_PORT, -1));
+                msg.getData().putInt("http", intent.getIntExtra(OrbotService.EXTRA_HTTP_PROXY_PORT, -1));
 
                 mStatusUpdateHandler.sendMessage(msg);
 
             }
         }
     };
- 
-    private void doLayout ()
-    {
+
+    private static String readFromAssets(Context context, String filename) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(context.getAssets().open(filename)));
+
+        // do reading, usually loop until end of file reading
+        StringBuilder sb = new StringBuilder();
+        String mLine = reader.readLine();
+        while (mLine != null) {
+            sb.append(mLine + '\n'); // process line
+            mLine = reader.readLine();
+        }
+        reader.close();
+        return sb.toString();
+    }
+
+    public static TorifiedApp getApp(Context context, ApplicationInfo aInfo) {
+        TorifiedApp app = new TorifiedApp();
+
+        PackageManager pMgr = context.getPackageManager();
+
+
+        try {
+            app.setName(pMgr.getApplicationLabel(aInfo).toString());
+        } catch (Exception e) {
+            return null;
+        }
+
+
+        app.setEnabled(aInfo.enabled);
+        app.setUid(aInfo.uid);
+        app.setUsername(pMgr.getNameForUid(app.getUid()));
+        app.setProcname(aInfo.processName);
+        app.setPackageName(aInfo.packageName);
+
+        app.setTorified(true);
+
+        try {
+            app.setIcon(pMgr.getApplicationIcon(app.getPackageName()));
+
+
+        } catch (NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return app;
+    }
+
+    public static Bitmap drawableToBitmap(Drawable drawable) {
+        Bitmap bitmap = null;
+
+        if (drawable instanceof BitmapDrawable) {
+            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+            if (bitmapDrawable.getBitmap() != null) {
+                return bitmapDrawable.getBitmap();
+            }
+        }
+
+        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
+        } else {
+            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+        }
+
+        Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bitmap;
+    }
+
+    /**
+     * Called when the activity is first created.
+     */
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mPrefs = Prefs.getSharedPrefs(getApplicationContext());
+
+        /* Create the widgets before registering for broadcasts to guarantee
+         * that the widgets exist when the status updates try to update them */
+        doLayout();
+
+        /* receive the internal status broadcasts, which are separate from the public
+         * status broadcasts to prevent other apps from sending fake/wrong status
+         * info to this app */
+        LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
+        lbm.registerReceiver(mLocalBroadcastReceiver,
+                new IntentFilter(TorServiceConstants.ACTION_STATUS));
+        lbm.registerReceiver(mLocalBroadcastReceiver,
+                new IntentFilter(TorServiceConstants.LOCAL_ACTION_BANDWIDTH));
+        lbm.registerReceiver(mLocalBroadcastReceiver,
+                new IntentFilter(TorServiceConstants.LOCAL_ACTION_LOG));
+        lbm.registerReceiver(mLocalBroadcastReceiver,
+                new IntentFilter(TorServiceConstants.LOCAL_ACTION_PORTS));
+
+
+        boolean showFirstTime = mPrefs.getBoolean("connect_first_time", true);
+
+        if (showFirstTime) {
+            Editor pEdit = mPrefs.edit();
+            pEdit.putBoolean("connect_first_time", false);
+            pEdit.commit();
+            startActivity(new Intent(this, OnboardingActivity.class));
+        }
+
+        /**
+         * Resets previous DNS Port to the default
+         */
+        mPrefs.edit().putInt(VpnPrefs.PREFS_DNS_PORT, TorServiceConstants.TOR_DNS_PORT_DEFAULT).apply();
+
+    }
+
+    private void sendIntentToService(final String action) {
+
+        Intent intent = new Intent(MiniMainActivity.this, OrbotService.class);
+        intent.setAction(action);
+        startService(intent);
+
+    }
+
+    private void stopTor() {
+
+        Intent intent = new Intent(MiniMainActivity.this, OrbotService.class);
+        stopService(intent);
+
+    }
+
+    private void doLayout() {
         setContentView(R.layout.layout_main);
-        
+
         setTitle(R.string.app_name);
 
         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
         setSupportActionBar(toolbar);
-        
-        mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
 
-        mTxtOrbotLog = (TextView)findViewById(R.id.orbotLog);
+        mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
 
-        /**
-        lblStatus = (TextView)findViewById(R.id.lblStatus);
-        lblStatus.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                mDrawer.openDrawer(LOG_DRAWER_GRAVITY);
-            }
-        });**/
+        mTxtOrbotLog = (TextView) findViewById(R.id.orbotLog);
 
-        imgStatus = (ImageView)findViewById(R.id.imgStatus);
+        imgStatus = (ImageView) findViewById(R.id.imgStatus);
         imgStatus.setOnLongClickListener(this);
 
-        downloadText = (TextView)findViewById(R.id.trafficDown);
-        uploadText = (TextView)findViewById(R.id.trafficUp);
+        downloadText = (TextView) findViewById(R.id.trafficDown);
+        uploadText = (TextView) findViewById(R.id.trafficUp);
 
-     //   downloadText.setText(formatCount(0) + " / " + formatTotal(0)+ " \u2193");
-      //  uploadText.setText(formatCount(0) + " / " + formatTotal(0)+ " \u2191");
-
-        downloadText.setText(formatTotal(0) +" \u2193");
+        downloadText.setText(formatTotal(0) + " \u2193");
         uploadText.setText(formatTotal(0) + " \u2191");
 
 
-		mBtnVPN = (SwitchCompat)findViewById(R.id.btnVPN);
-		
+        mBtnVPN = (SwitchCompat) findViewById(R.id.btnVPN);
+
         boolean useVPN = Prefs.useVpn();
         mBtnVPN.setChecked(useVPN);
 
@@ -287,26 +373,17 @@ public class MiniMainActivity extends AppCompatActivity
         LinearLayoutManager llm = new LinearLayoutManager(this);
         llm.setOrientation(LinearLayoutManager.VERTICAL);
         rv.setLayoutManager(llm);
-
-        /**
-        findViewById(R.id.btnAdd).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                startActivityForResult(new Intent(MiniMainActivity.this, AppManagerActivity.class), REQUEST_VPN_APPS_SELECT);
-            }
-        });**/
     }
 
-
     @Override
     protected void attachBaseContext(Context base) {
         super.attachBaseContext(LocaleHelper.onAttach(base));
     }
 
-   /*
-    * Create the UI Options Menu (non-Javadoc)
-    * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
-    */
+    /*
+     * Create the UI Options Menu (non-Javadoc)
+     * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
+     */
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         super.onCreateOptionsMenu(menu);
@@ -314,111 +391,52 @@ public class MiniMainActivity extends AppCompatActivity
         inflater.inflate(R.menu.orbot_main, menu);
         return true;
     }
-    
-    
 
     @Override
-	public boolean onOptionsItemSelected(MenuItem item) {
+    public boolean onOptionsItemSelected(MenuItem item) {
 
-        if (item.getItemId() == R.id.menu_newnym)
-        {
+        if (item.getItemId() == R.id.menu_newnym) {
             requestNewTorIdentity();
+        } else if (item.getItemId() == R.id.menu_settings) {
+            Intent intent = SettingsPreferencesActivity.createIntent(this, R.xml.preferences);
+            startActivityForResult(intent, REQUEST_SETTINGS);
         }
-    	 else if (item.getItemId() == R.id.menu_settings)
-         {
-             Intent intent = new Intent(MiniMainActivity.this, SettingsPreferences.class);
-             startActivityForResult(intent, REQUEST_SETTINGS);
-         }
-    	 /**
-         else if (item.getItemId() == R.id.menu_exit)
-         {
-                 //exit app
-                 doExit();
-                 
-         }**/
-         else if (item.getItemId() == R.id.menu_about)
-         {
-                 showAbout();
-                 
-                 
-         }
-         /**
-         else if (item.getItemId() == R.id.menu_scan)
-         {
-         	IntentIntegrator integrator = new IntentIntegrator(MiniMainActivity.this);
-         	integrator.initiateScan();
-         }
-         else if (item.getItemId() == R.id.menu_share_bridge)
-         {
-         	
-     		String bridges = Prefs.getBridgesList();
-         	
-     		if (bridges != null && bridges.length() > 0)
-     		{
-         		try {
-						bridges = "bridge://" + URLEncoder.encode(bridges,"UTF-8");
-	            		
-	                	IntentIntegrator integrator = new IntentIntegrator(MiniMainActivity.this);
-	                	integrator.shareText(bridges);
-	                	
-					} catch (UnsupportedEncodingException e) {
-						// TODO Auto-generated catch block
-						e.printStackTrace();
-					}
-     		}
-
-         }**/
-     
-		return super.onOptionsItemSelected(item);
-	}
-
-	private void showAbout ()
-        {
-                
-            LayoutInflater li = LayoutInflater.from(this);
-            View view = li.inflate(R.layout.layout_about, null); 
-            
-            String version = "";
-            
-            try {
-                version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName + " (Tor " + OrbotService.BINARY_TOR_VERSION + ")";
-            } catch (NameNotFoundException e) {
-                version = "Version Not Found";
-            }
-            
-            TextView versionName = (TextView)view.findViewById(R.id.versionName);
-            versionName.setText(version);
+        else if (item.getItemId() == R.id.menu_about) {
+            showAbout();
+        }
+        return super.onOptionsItemSelected(item);
+    }
 
-            TextView aboutOther = (TextView)view.findViewById(R.id.aboutother);
+    private void showAbout() {
 
-            try
-            {
-                String aboutText = readFromAssets(this,"LICENSE");
-                aboutText = aboutText.replace("\n","<br/>");
-                aboutOther.setText(Html.fromHtml(aboutText));
-            }
-            catch (Exception e){}
-            
-                    new AlertDialog.Builder(this)
-            .setTitle(getString(R.string.button_about))
-            .setView(view)
-            .show();
+        LayoutInflater li = LayoutInflater.from(this);
+        View view = li.inflate(R.layout.layout_about, null);
+
+        String version = "";
+
+        try {
+            version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName + " (Tor " + OrbotService.BINARY_TOR_VERSION + ")";
+        } catch (NameNotFoundException e) {
+            version = "Version Not Found";
         }
 
-    private static String readFromAssets(Context context, String filename) throws IOException {
-        BufferedReader reader = new BufferedReader(new InputStreamReader(context.getAssets().open(filename)));
+        TextView versionName = (TextView) view.findViewById(R.id.versionName);
+        versionName.setText(version);
 
-        // do reading, usually loop until end of file reading
-        StringBuilder sb = new StringBuilder();
-        String mLine = reader.readLine();
-        while (mLine != null) {
-            sb.append(mLine + '\n'); // process line
-            mLine = reader.readLine();
+        TextView aboutOther = (TextView) view.findViewById(R.id.aboutother);
+
+        try {
+            String aboutText = readFromAssets(this, "LICENSE");
+            aboutText = aboutText.replace("\n", "<br/>");
+            aboutOther.setText(Html.fromHtml(aboutText));
+        } catch (Exception e) {
         }
-        reader.close();
-        return sb.toString();
-    }
 
+        new AlertDialog.Builder(this)
+                .setTitle(getString(R.string.button_about))
+                .setView(view)
+                .show();
+    }
 
     /**
      * This is our attempt to REALLY exit Orbot, and stop the background service
@@ -434,43 +452,36 @@ public class MiniMainActivity extends AppCompatActivity
         finish();
     }
 
-	protected void onPause() {
-		try
-		{
-			super.onPause();
-	
-			if (aDialog != null)
-				aDialog.dismiss();
-		}
-		catch (IllegalStateException ise)
-		{
-			//can happen on exit/shutdown
-		}
-	}
+    protected void onPause() {
+        try {
+            super.onPause();
 
+            if (aDialog != null)
+                aDialog.dismiss();
+        } catch (IllegalStateException ise) {
+            //can happen on exit/shutdown
+        }
+    }
 
     @Override
     public void onBackPressed() {
         // check to see if the log is open, if so close it
         if (mDrawer.isDrawerOpen(LOG_DRAWER_GRAVITY)) {
             mDrawer.closeDrawers();
-        }
-        else {
+        } else {
             super.onBackPressed();
         }
     }
 
-	private void refreshVPNApps() {
+    private void refreshVPNApps() {
         sendIntentToService(TorServiceConstants.ACTION_STOP_VPN);
         sendIntentToService(TorServiceConstants.ACTION_START_VPN);
     }
 
-    private void enableVPN (boolean enable)
-    {
+    private void enableVPN(boolean enable) {
         if (enable && pkgIds.size() == 0) {
             showAppPicker();
-        }
-        else {
+        } else {
             Prefs.putUseVpn(enable);
             Prefs.putStartOnBoot(enable);
 
@@ -491,7 +502,6 @@ public class MiniMainActivity extends AppCompatActivity
         }
     }
 
-
     private synchronized void handleIntents() {
         if (getIntent() == null)
             return;
@@ -510,9 +520,7 @@ public class MiniMainActivity extends AppCompatActivity
 
                 if (urlString != null) {
 
-                    if (urlString.toLowerCase().startsWith("bridge://"))
-
-                    {
+                    if (urlString.toLowerCase().startsWith("bridge://")) {
                         String newBridgeValue = urlString.substring(9); //remove the bridge protocol piece
                         newBridgeValue = URLDecoder.decode(newBridgeValue); //decode the value here
 
@@ -542,58 +550,53 @@ public class MiniMainActivity extends AppCompatActivity
         enableBridges(true);
     }
 
-	/*
-	 * Launch the system activity for Uri viewing with the provided url
-	 */
-	private void  openBrowser(final String browserLaunchUrl,boolean forceExternal, String pkgId) {
-		if (pkgId != null) {
-            startIntent(pkgId,Intent.ACTION_VIEW,Uri.parse(browserLaunchUrl));
+    /*
+     * Launch the system activity for Uri viewing with the provided url
+     */
+    private void openBrowser(final String browserLaunchUrl, boolean forceExternal, String pkgId) {
+        if (pkgId != null) {
+            startIntent(pkgId, Intent.ACTION_VIEW, Uri.parse(browserLaunchUrl));
+        } else if (mBtnVPN.isChecked() || forceExternal) {
+            //use the system browser since VPN is on
+            startIntent(null, Intent.ACTION_VIEW, Uri.parse(browserLaunchUrl));
         }
-		else if (mBtnVPN.isChecked()||forceExternal) {
-			//use the system browser since VPN is on
-			startIntent(null,Intent.ACTION_VIEW, Uri.parse(browserLaunchUrl));
-		}
-	}
-
-    private void startIntent (String pkg, String action, Uri data)
-    {
+    }
+
+    private void startIntent(String pkg, String action, Uri data) {
         Intent i;
-		PackageManager pm = getPackageManager();
+        PackageManager pm = getPackageManager();
 
         try {
-			if (pkg != null) {
-				i = pm.getLaunchIntentForPackage(pkg);
-				if (i == null)
-					throw new PackageManager.NameNotFoundException();
-			}
-			else
-			{
-				i = new Intent();
-			}
+            if (pkg != null) {
+                i = pm.getLaunchIntentForPackage(pkg);
+                if (i == null)
+                    throw new PackageManager.NameNotFoundException();
+            } else {
+                i = new Intent();
+            }
 
             i.setAction(action);
             i.setData(data);
 
-			if (i.resolveActivity(pm)!=null)
-				startActivity(i);
+            if (i.resolveActivity(pm) != null)
+                startActivity(i);
 
         } catch (PackageManager.NameNotFoundException e) {
 
         }
     }
-    
+
     @Override
     protected void onActivityResult(int request, int response, Intent data) {
         super.onActivityResult(request, response, data);
 
-        if (request == REQUEST_SETTINGS && response == RESULT_OK)
-        {
+        if (request == REQUEST_SETTINGS && response == RESULT_OK) {
             if (data != null && (!TextUtils.isEmpty(data.getStringExtra("locale")))) {
 
                 String newLocale = data.getStringExtra("locale");
                 Prefs.setDefaultLocale(newLocale);
                 Languages.setLanguage(this, newLocale, true);
-              //  Language.setFromPreference(this, "pref_default_locale");
+                //  Language.setFromPreference(this, "pref_default_locale");
 
                 finish();
 
@@ -609,8 +612,7 @@ public class MiniMainActivity extends AppCompatActivity
 
 
             }
-        } else if (request == REQUEST_VPN_APPS_SELECT)
-        {
+        } else if (request == REQUEST_VPN_APPS_SELECT) {
             if (response == RESULT_OK &&
                     torStatus == TorServiceConstants.STATUS_ON) {
                 refreshVPNApps();
@@ -625,104 +627,78 @@ public class MiniMainActivity extends AppCompatActivity
             Prefs.putUseVpn(false);
         }
 
-        
+
         IntentResult scanResult = IntentIntegrator.parseActivityResult(request, response, data);
         if (scanResult != null) {
-             // handle scan result
-        	
-        	String results = scanResult.getContents();
-        	
-        	if (results != null && results.length() > 0)
-        	{
-	        	try {
-					
-					int urlIdx = results.indexOf("://");
-					
-					if (urlIdx!=-1)
-					{
-						results = URLDecoder.decode(results, "UTF-8");
-						results = results.substring(urlIdx+3);
-
-						showAlert(getString(R.string.bridges_updated),getString(R.string.restart_orbot_to_use_this_bridge_) + results,false);	
-						
-						setNewBridges(results);
-					}
-					else
-					{
-						JSONArray bridgeJson = new JSONArray(results);
-						StringBuffer bridgeLines = new StringBuffer();
-						
-						for (int i = 0; i < bridgeJson.length(); i++)
-						{
-							String bridgeLine = bridgeJson.getString(i);
-							bridgeLines.append(bridgeLine).append("\n");
-						}
-						
-						setNewBridges(bridgeLines.toString());
-					}
-					
-					
-				} catch (Exception e) {
-					Log.e(TAG,"unsupported",e);
-				}
-        	}
-        	
-          }
-        
-    }
+            // handle scan result
 
-    /**
-    public void promptSetupBridges ()
-    {
+            String results = scanResult.getContents();
+
+            if (results != null && results.length() > 0) {
+                try {
+
+                    int urlIdx = results.indexOf("://");
+
+                    if (urlIdx != -1) {
+                        results = URLDecoder.decode(results, "UTF-8");
+                        results = results.substring(urlIdx + 3);
+
+                        showAlert(getString(R.string.bridges_updated), getString(R.string.restart_orbot_to_use_this_bridge_) + results, false);
+
+                        setNewBridges(results);
+                    } else {
+                        JSONArray bridgeJson = new JSONArray(results);
+                        StringBuffer bridgeLines = new StringBuffer();
+
+                        for (int i = 0; i < bridgeJson.length(); i++) {
+                            String bridgeLine = bridgeJson.getString(i);
+                            bridgeLines.append(bridgeLine).append("\n");
+                        }
+
+                        setNewBridges(bridgeLines.toString());
+                    }
+
+
+                } catch (Exception e) {
+                    Log.e(TAG, "unsupported", e);
+                }
+            }
 
-        if (mBtnBridges.isChecked())
-        {
-            Prefs.putBridgesEnabled(true);
-            startActivity(new Intent(this, BridgeWizardActivity.class));
         }
-        else
-        {
-        	enableBridges(false);
+
+    }
+
+    private void enableBridges(boolean enable) {
+        Prefs.putBridgesEnabled(enable);
+
+        if (torStatus == TorServiceConstants.STATUS_ON) {
+            String bridgeList = Prefs.getBridgesList();
+            if (bridgeList != null && bridgeList.length() > 0) {
+                requestTorRereadConfig();
+            }
         }
-        
-    }**/
-    
-
-    private void enableBridges (boolean enable)
-    {
-		Prefs.putBridgesEnabled(enable);
-
-		if (torStatus == TorServiceConstants.STATUS_ON)
-		{
-			String bridgeList = Prefs.getBridgesList();
-			if (bridgeList != null && bridgeList.length() > 0)
-			{
-				requestTorRereadConfig ();
-			}
-		}
     }
 
     private void requestTorRereadConfig() {
         sendIntentToService(TorServiceConstants.CMD_SIGNAL_HUP);
     }
 
-
     @Override
     protected void onResume() {
         super.onResume();
 
-        if (mBtnVPN.isChecked()!=Prefs.useVpn())
+        if (mBtnVPN.isChecked() != Prefs.useVpn())
             mBtnVPN.setChecked(Prefs.useVpn());
 
-		requestTorStatus();
+        requestTorStatus();
 
-		if (torStatus == null)
-		    updateStatus("", TorServiceConstants.STATUS_STOPPING);
+        if (torStatus == null)
+            updateStatus("", TorServiceConstants.STATUS_STOPPING);
         else
             updateStatus(null, torStatus);
 
-           //now you can handle the intents properly
-           handleIntents();
+        //now you can handle the intents properly
+        handleIntents();
 
         pkgIds.clear();
         String tordAppString = mPrefs.getString(PREFS_KEY_TORIFIED, "");
@@ -735,40 +711,33 @@ public class MiniMainActivity extends AppCompatActivity
 
     }
 
-    AlertDialog aDialog = null;
-    
     //general alert dialog for mostly Tor warning messages
     //sometimes this can go haywire or crazy with too many error
     //messages from Tor, and the user cannot stop or exit Orbot
     //so need to ensure repeated error messages are not spamming this method
-    private void showAlert(String title, String msg, boolean button)
-    {
-            try
-            {
-                    if (aDialog != null && aDialog.isShowing())
-                            aDialog.dismiss();
-            }
-            catch (Exception e){} //swallow any errors
-            
-             if (button)
-             {
-                            aDialog = new AlertDialog.Builder(MiniMainActivity.this)
-                     .setIcon(R.drawable.onion32)
-             .setTitle(title)
-             .setMessage(msg)
-             .setPositiveButton(R.string.btn_okay, null)
-             .show();
-             }
-             else
-             {
-                     aDialog = new AlertDialog.Builder(MiniMainActivity.this)
-                     .setIcon(R.drawable.onion32)
-             .setTitle(title)
-             .setMessage(msg)
-             .show();
-             }
-    
-             aDialog.setCanceledOnTouchOutside(true);
+    private void showAlert(String title, String msg, boolean button) {
+        try {
+            if (aDialog != null && aDialog.isShowing())
+                aDialog.dismiss();
+        } catch (Exception e) {
+        } //swallow any errors
+
+        if (button) {
+            aDialog = new AlertDialog.Builder(MiniMainActivity.this)
+                    .setIcon(R.drawable.onion32)
+                    .setTitle(title)
+                    .setMessage(msg)
+                    .setPositiveButton(R.string.btn_okay, null)
+                    .show();
+        } else {
+            aDialog = new AlertDialog.Builder(MiniMainActivity.this)
+                    .setIcon(R.drawable.onion32)
+                    .setTitle(title)
+                    .setMessage(msg)
+                    .show();
+        }
+
+        aDialog.setCanceledOnTouchOutside(true);
     }
 
     /**
@@ -777,10 +746,9 @@ public class MiniMainActivity extends AppCompatActivity
      */
     private void updateStatus(String torServiceMsg, String newTorStatus) {
 
-        if (!TextUtils.isEmpty(torServiceMsg))
-        {
+        if (!TextUtils.isEmpty(torServiceMsg)) {
             if (torServiceMsg.contains(TorServiceConstants.LOG_NOTICE_HEADER)) {
-           //     lblStatus.setText(torServiceMsg);
+                //     lblStatus.setText(torServiceMsg);
             }
 
             mTxtOrbotLog.append(torServiceMsg + '\n');
@@ -790,18 +758,16 @@ public class MiniMainActivity extends AppCompatActivity
         if (torStatus == null || (newTorStatus != null && newTorStatus.equals(torStatus))) {
             torStatus = newTorStatus;
             return;
-        }
-    	else
-    	    torStatus = newTorStatus;
+        } else
+            torStatus = newTorStatus;
 
         if (torStatus == TorServiceConstants.STATUS_ON) {
-        	
+
             imgStatus.setImageResource(R.drawable.toron);
 
             //lblStatus.setText(getString(R.string.status_activated));
 
-            if (autoStartFromIntent)
-            {
+            if (autoStartFromIntent) {
                 autoStartFromIntent = false;
                 Intent resultIntent = lastStatusIntent;
 
@@ -810,7 +776,7 @@ public class MiniMainActivity extends AppCompatActivity
 
                 resultIntent.putExtra(
                         TorServiceConstants.EXTRA_STATUS,
-                        torStatus == null?TorServiceConstants.STATUS_OFF:torStatus
+                        torStatus == null ? TorServiceConstants.STATUS_OFF : torStatus
                 );
 
                 setResult(RESULT_OK, resultIntent);
@@ -818,35 +784,33 @@ public class MiniMainActivity extends AppCompatActivity
                 finish();
                 Log.d(TAG, "autoStartFromIntent finish");
             }
-            
-            
+
 
         } else if (torStatus == TorServiceConstants.STATUS_STARTING) {
 
             imgStatus.setImageResource(R.drawable.torstarting);
 
-            if (torServiceMsg != null)
-            {
-            	if (torServiceMsg.contains(TorServiceConstants.LOG_NOTICE_BOOTSTRAPPED)) {
+            if (torServiceMsg != null) {
+                if (torServiceMsg.contains(TorServiceConstants.LOG_NOTICE_BOOTSTRAPPED)) {
                     //        		lblStatus.setText(torServiceMsg);
                 }
             }
-      //      else
-        //    	lblStatus.setText(getString(R.string.status_starting_up));
+            //      else
+            //    	lblStatus.setText(getString(R.string.status_starting_up));
 
 
         } else if (torStatus == TorServiceConstants.STATUS_STOPPING) {
 
-        //	  if (torServiceMsg != null && torServiceMsg.contains(TorServiceConstants.LOG_NOTICE_HEADER))
-          //    	lblStatus.setText(torServiceMsg);
-        	  
+            //	  if (torServiceMsg != null && torServiceMsg.contains(TorServiceConstants.LOG_NOTICE_HEADER))
+            //    	lblStatus.setText(torServiceMsg);
+
             imgStatus.setImageResource(R.drawable.torstarting);
 //            lblStatus.setText(torServiceMsg);
 
         } else if (torStatus == TorServiceConstants.STATUS_OFF) {
 
             imgStatus.setImageResource(R.drawable.toroff);
-  //          lblStatus.setText("Tor v" + OrbotService.BINARY_TOR_VERSION);
+            //          lblStatus.setText("Tor v" + OrbotService.BINARY_TOR_VERSION);
 
 
         }
@@ -863,7 +827,7 @@ public class MiniMainActivity extends AppCompatActivity
         sendIntentToService(TorServiceConstants.ACTION_START);
         mTxtOrbotLog.setText("");
     }
-    
+
     /**
      * Request tor status without starting it
      * {@link TorServiceConstants#ACTION_START} {@link Intent} to
@@ -873,16 +837,6 @@ public class MiniMainActivity extends AppCompatActivity
         sendIntentToService(TorServiceConstants.ACTION_STATUS);
     }
 
-    private boolean isTorServiceRunning() {
-        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
-        for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
-            if (OrbotService.class.getName().equals(service.service.getClassName())) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     public boolean onLongClick(View view) {
 
         if (torStatus == TorServiceConstants.STATUS_OFF) {
@@ -890,92 +844,18 @@ public class MiniMainActivity extends AppCompatActivity
         } else {
             stopTor();
         }
-        
-        return true;
-                
-    }
-
-// this is what takes messages or values from the callback threads or other non-mainUI threads
-//and passes them back into the main UI thread for display to the user
-    private Handler mStatusUpdateHandler = new Handler() {
-
-        @Override
-        public void handleMessage(final Message msg) {
-
-
-            Bundle data = msg.getData();
-
-            switch (msg.what) {
-                case MESSAGE_TRAFFIC_COUNT:
-
-                    DataCount datacount =  new DataCount(data.getLong("upload"),data.getLong("download"));
-                    
-                    long totalRead = data.getLong("readTotal");
-                    long totalWrite = data.getLong("writeTotal");
-                
-//                    downloadText.setText(formatCount(datacount.Download) + " / " + formatTotal(totalRead));
- //                   uploadText.setText(formatCount(datacount.Upload) + " / " + formatTotal(totalWrite));
-
-                    downloadText.setText(formatTotal(totalRead) +" \u2193");
-                    uploadText.setText(formatTotal(totalWrite) + " \u2191");
-
-                    break;
-                case MESSAGE_PORTS:
-
-                    int socksPort = data.getInt("socks");
-                    int httpPort = data.getInt("http");
-
-                    break;
-                default:
-                    String newTorStatus = msg.getData().getString("status");
-                    String log = (String)msg.obj;
 
-                    if (torStatus == null && newTorStatus != null) //first time status
-                    {
-                        updateStatus(log, newTorStatus);
+        return true;
 
-                    }
-                    else
-                        updateStatus(log, newTorStatus);
-                    super.handleMessage(msg);
-                    break;
-            }
-        }
-    };
+    }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
-          LocalBroadcastManager.getInstance(this).unregisterReceiver(mLocalBroadcastReceiver);
+        LocalBroadcastManager.getInstance(this).unregisterReceiver(mLocalBroadcastReceiver);
 
     }
 
-    public class DataCount {
-           // data uploaded
-           public long Upload;
-           // data downloaded
-           public long Download;
-           
-           DataCount(long Upload, long Download){
-               this.Upload = Upload;
-               this.Download = Download;
-           }
-       }
-       
-    private String formatCount(long count) {
-        NumberFormat numberFormat = NumberFormat.getInstance(Locale.getDefault());
-        // Converts the supplied argument into a string.
-        // Under 2Mb, returns "xxx.xKb"
-        // Over 2Mb, returns "xxx.xxMb"
-        if (count < 1e6)
-            return numberFormat.format(Math.round(((float) ((int) (count * 10 / 1024)) / 10)))
-                    + getString(R.string.kbps);
-        else
-            return numberFormat.format(Math
-                    .round(((float) ((int) (count * 100 / 1024 / 1024)) / 100)))
-                    + getString(R.string.mbps);
-    }
-
     private String formatTotal(long count) {
         NumberFormat numberFormat = NumberFormat.getInstance(Locale.getDefault());
         // Converts the supplied argument into a string.
@@ -983,57 +863,59 @@ public class MiniMainActivity extends AppCompatActivity
         // Over 2Mb, returns "xxx.xxMb"
         if (count < 1e6)
             return numberFormat.format(Math.round(
-                    (int)(((float)count)) * 10f / 1024f / 10f)
-                    )
+                    (int) (((float) count)) * 10f / 1024f / 10f)
+            )
                     + getString(R.string.kb);
         else
             return numberFormat.format(Math
                     .round(
-                            ((float)count)) * 100f / 1024f / 1024f / 100f
-                        )
+                            ((float) count)) * 100f / 1024f / 1024f / 100f
+            )
                     + getString(R.string.mb);
     }
 
-    private static final float ROTATE_FROM = 0.0f;
-    private static final float ROTATE_TO = 360.0f*4f;// 3.141592654f * 32.0f;
+    private void requestNewTorIdentity() {
+        sendIntentToService(TorServiceConstants.CMD_NEWNYM);
 
-    private void requestNewTorIdentity ()
-    {
-        sendIntentToService (TorServiceConstants.CMD_NEWNYM);
-
-        Rotate3dAnimation rotation = new Rotate3dAnimation(ROTATE_FROM, ROTATE_TO, imgStatus.getWidth()/2f,imgStatus.getWidth()/2f,20f,false);
+        Rotate3dAnimation rotation = new Rotate3dAnimation(ROTATE_FROM, ROTATE_TO, imgStatus.getWidth() / 2f, imgStatus.getWidth() / 2f, 20f, false);
         rotation.setFillAfter(true);
         rotation.setInterpolator(new AccelerateInterpolator());
-        rotation.setDuration((long) 2*1000);
+        rotation.setDuration((long) 2 * 1000);
         rotation.setRepeatCount(0);
         imgStatus.startAnimation(rotation);
 //        lblStatus.setText(getString(R.string.newnym));
     }
 
+    public void showAppPicker() {
+        startActivityForResult(new Intent(MiniMainActivity.this, AppManagerActivity.class), REQUEST_VPN_APPS_SELECT);
 
-    public class RVAdapter extends RecyclerView.Adapter<RVAdapter.AppViewHolder>{
-
-
-        public class AppViewHolder extends RecyclerView.ViewHolder {
+    }
 
-            ImageView iv;
-            TextView tv;
-            View parent;
+    public void showAppConfig(String pkgId) {
+        Intent data = new Intent(this, AppConfigActivity.class);
+        data.putExtra(Intent.EXTRA_PACKAGE_NAME, pkgId);
+        startActivityForResult(data, REQUEST_VPN_APPS_SELECT);
+    }
 
-            AppViewHolder(View itemView) {
-                super(itemView);
-                parent = itemView;
-                iv = itemView.findViewById(R.id.itemicon);
-                tv = itemView.findViewById(R.id.itemtext);
+    public class DataCount {
+        // data uploaded
+        public long Upload;
+        // data downloaded
+        public long Download;
+
+        DataCount(long Upload, long Download) {
+            this.Upload = Upload;
+            this.Download = Download;
+        }
+    }
 
-            }
+    public class RVAdapter extends RecyclerView.Adapter<RVAdapter.AppViewHolder> {
 
-        }
 
         @Override
         public int getItemCount() {
 
-            return pkgIds.size()+1;
+            return pkgIds.size() + 1;
         }
 
         @Override
@@ -1043,7 +925,6 @@ public class MiniMainActivity extends AppCompatActivity
             final AppViewHolder avh = new AppViewHolder(v);
 
 
-
             return avh;
         }
 
@@ -1051,10 +932,10 @@ public class MiniMainActivity extends AppCompatActivity
         public void onBindViewHolder(final AppViewHolder avh, int i) {
 
 
-            if (i < getItemCount()-1) {
+            if (i < getItemCount() - 1) {
                 final String pkgId = pkgIds.get(i);
 
-                ApplicationInfo aInfo = null;
+                ApplicationInfo aInfo;
                 try {
                     aInfo = getPackageManager().getApplicationInfo(pkgId, 0);
                     TorifiedApp app = getApp(MiniMainActivity.this, aInfo);
@@ -1085,9 +966,7 @@ public class MiniMainActivity extends AppCompatActivity
                 } catch (NameNotFoundException e) {
                     e.printStackTrace();
                 }
-            }
-            else
-            {
+            } else {
                 avh.iv.setVisibility(View.INVISIBLE);
                 avh.tv.setText("+ ADD APP");
                 avh.parent.setOnClickListener(new View.OnClickListener() {
@@ -1101,80 +980,20 @@ public class MiniMainActivity extends AppCompatActivity
             }
         }
 
+        public class AppViewHolder extends RecyclerView.ViewHolder {
 
-    }
-
-    public static TorifiedApp getApp (Context context, ApplicationInfo aInfo)
-    {
-        TorifiedApp app = new TorifiedApp();
-
-        PackageManager pMgr = context.getPackageManager();
-
-
-        try
-        {
-            app.setName(pMgr.getApplicationLabel(aInfo).toString());
-        }
-        catch (Exception e)
-        {
-            return null;
-        }
-
-
-        app.setEnabled(aInfo.enabled);
-        app.setUid(aInfo.uid);
-        app.setUsername(pMgr.getNameForUid(app.getUid()));
-        app.setProcname(aInfo.processName);
-        app.setPackageName(aInfo.packageName);
-
-        app.setTorified(true);
-
-        try {
-            app.setIcon(pMgr.getApplicationIcon(app.getPackageName()));
-
-
-        } catch (NameNotFoundException e) {
-            e.printStackTrace();
-        }
-        return app;
-    }
-
-
-    public void showAppPicker ()
-    {
-        startActivityForResult(new Intent(MiniMainActivity.this, AppManagerActivity.class), REQUEST_VPN_APPS_SELECT);
-
-    }
-
-    public void showAppConfig (String pkgId)
-    {
-        Intent data = new Intent(this, AppConfigActivity.class);
-        data.putExtra(Intent.EXTRA_PACKAGE_NAME,pkgId);
-        startActivityForResult(data,REQUEST_VPN_APPS_SELECT);
-    }
-
+            ImageView iv;
+            TextView tv;
+            View parent;
 
-    public static Bitmap drawableToBitmap (Drawable drawable) {
-        Bitmap bitmap = null;
+            AppViewHolder(View itemView) {
+                super(itemView);
+                parent = itemView;
+                iv = itemView.findViewById(R.id.itemicon);
+                tv = itemView.findViewById(R.id.itemtext);
 
-        if (drawable instanceof BitmapDrawable) {
-            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
-            if(bitmapDrawable.getBitmap() != null) {
-                return bitmapDrawable.getBitmap();
             }
-        }
 
-        if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
-            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
-        } else {
-            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
         }
-
-        Canvas canvas = new Canvas(bitmap);
-        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
-        drawable.draw(canvas);
-        return bitmap;
     }
-
-
 }
diff --git a/app-mini/src/main/java/org/torproject/android/mini/OnBootReceiver.java b/app-mini/src/main/java/org/torproject/android/mini/OnBootReceiver.java
deleted file mode 100644
index c958df8b..00000000
--- a/app-mini/src/main/java/org/torproject/android/mini/OnBootReceiver.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.torproject.android.mini;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-
-import org.torproject.android.service.OrbotService;
-import org.torproject.android.service.TorServiceConstants;
-import org.torproject.android.service.util.Prefs;
-
-public class OnBootReceiver extends BroadcastReceiver {
-
-	private static boolean sReceivedBoot = false;
-
-	@Override
-	public void onReceive(Context context, Intent intent) {
-		if (Prefs.startOnBoot() && (!sReceivedBoot)) {
-			startService(TorServiceConstants.ACTION_START_ON_BOOT, context);
-			sReceivedBoot = true;
-		}
-	}
-	
-	private void startService (String action, Context context) {
-		Intent intent = new Intent(context, OrbotService.class);
-		intent.setAction(action);
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-			context.startForegroundService(intent);
-		}
-		else
-		{
-			context.startService(intent);
-		}
-
-	}
-	
-	
-}
-
diff --git a/app-mini/src/main/java/org/torproject/android/mini/OrbotMiniApp.java b/app-mini/src/main/java/org/torproject/android/mini/OrbotMiniApp.java
index 41bd0697..f0612cc5 100644
--- a/app-mini/src/main/java/org/torproject/android/mini/OrbotMiniApp.java
+++ b/app-mini/src/main/java/org/torproject/android/mini/OrbotMiniApp.java
@@ -1,32 +1,18 @@
-
 package org.torproject.android.mini;
 
-import android.annotation.SuppressLint;
-import android.app.Activity;
 import android.app.Application;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import androidx.core.app.NotificationCompat;
-import com.github.javiersantos.appupdater.AppUpdater;
-import com.github.javiersantos.appupdater.enums.Display;
-import com.github.javiersantos.appupdater.enums.UpdateFrom;
-import org.torproject.android.mini.settings.Languages;
-import org.torproject.android.mini.settings.LocaleHelper;
+
+import org.torproject.android.core.Languages;
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.service.OrbotConstants;
 import org.torproject.android.service.util.Prefs;
 
 import java.util.Locale;
 
-
 public class OrbotMiniApp extends Application implements OrbotConstants {
 
-    private Locale locale;
-
     @Override
     public void onCreate() {
         super.onCreate();
@@ -35,18 +21,12 @@ public class OrbotMiniApp extends Application implements OrbotConstants {
         if (!Prefs.getDefaultLocale().equals(Locale.getDefault().getLanguage())) {
             Languages.setLanguage(this, Prefs.getDefaultLocale(), true);
         }
-
-        //check for updates via github, since it is unlikely to be blocked; notify the user of places where upgrades can be found
-        new AppUpdater(this)
-                .setUpdateFrom(UpdateFrom.JSON)
-                .setUpdateJSON("https://raw.githubusercontent.com/n8fr8/orbot/master/update.json")
-                .setDisplay(Display.NOTIFICATION).start();
     }
 
     @Override
     protected void attachBaseContext(Context base) {
         Prefs.setContext(base);
-        super.attachBaseContext(LocaleHelper.onAttach(base, Prefs.getDefaultLocale()));
+        super.attachBaseContext(LocaleHelper.onAttach(base));
     }
 
     @Override
@@ -56,53 +36,4 @@ public class OrbotMiniApp extends Application implements OrbotConstants {
         if (!Prefs.getDefaultLocale().equals(Locale.getDefault().getLanguage()))
             Languages.setLanguage(this, Prefs.getDefaultLocale(), true);
     }
-	/**
-    public static void forceChangeLanguage(Activity activity) {
-        Intent intent = activity.getIntent();
-        if (intent == null) // when launched as LAUNCHER
-            intent = new Intent(activity, OrbotMainActivity.class);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-        activity.finish();
-        activity.overridePendingTransition(0, 0);
-        activity.startActivity(intent);
-        activity.overridePendingTransition(0, 0);
-    }**/
-
-    public static Languages getLanguages(Activity activity) {
-        return Languages.get(activity);
-    }
-
-
-    @SuppressLint("NewApi")
-    protected void showToolbarNotification (String shortMsg, String notifyMsg, int notifyId, int icon)
-    {
-
-        NotificationCompat.Builder notifyBuilder;
-
-        //Reusable code.
-        PackageManager pm = getPackageManager();
-        Intent intent = pm.getLaunchIntentForPackage(getPackageName());
-        PendingIntent pendIntent = PendingIntent.getActivity(this, 0, intent, 0);
-
-        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-
-        notifyBuilder = new NotificationCompat.Builder(this)
-                .setContentTitle(getString(org.torproject.android.service.R.string.app_name));
-
-
-        notifyBuilder.setContentIntent(pendIntent);
-
-        notifyBuilder.setContentText(shortMsg);
-        notifyBuilder.setSmallIcon(icon);
-        notifyBuilder.setTicker(notifyMsg);
-
-        notifyBuilder.setOngoing(false);
-
-        notifyBuilder.setStyle(new NotificationCompat.BigTextStyle()
-                .bigText(notifyMsg).setBigContentTitle(getString(org.torproject.android.service.R.string.app_name)));
-
-        Notification notification = notifyBuilder.build();
-
-        notificationManager.notify(notifyId, notification);
-    }
-}
+}
\ No newline at end of file
diff --git a/app-mini/src/main/java/org/torproject/android/mini/settings/Languages.java b/app-mini/src/main/java/org/torproject/android/mini/settings/Languages.java
deleted file mode 100644
index 5fe04a62..00000000
--- a/app-mini/src/main/java/org/torproject/android/mini/settings/Languages.java
+++ /dev/null
@@ -1,228 +0,0 @@
-package org.torproject.android.mini.settings;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.ContextWrapper;
-import android.content.Intent;
-import android.content.res.AssetManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-public class Languages {
-    public static final String TAG = "Languages";
-
-    public static final Locale defaultLocale;
-    public static final Locale TIBETAN = new Locale("bo");
-    static final Locale localesToTest[] = {
-            Locale.ENGLISH, Locale.FRENCH, Locale.GERMAN,
-            Locale.ITALIAN, Locale.JAPANESE, Locale.KOREAN,
-            Locale.TRADITIONAL_CHINESE, Locale.SIMPLIFIED_CHINESE,
-            TIBETAN, new Locale("af"), new Locale("am"),
-            new Locale("ar"), new Locale("ay"), new Locale("az"), new Locale("bg"),
-            new Locale("bn"), new Locale("ca"), new Locale("cs"),
-            new Locale("da"), new Locale("el"), new Locale("es"),
-            new Locale("et"), new Locale("eu"), new Locale("fa"),
-            new Locale("fi"), new Locale("gl"), new Locale("hi"),
-            new Locale("hr"), new Locale("hu"), new Locale("hy"),
-            new Locale("in"), new Locale("hy"), new Locale("in"),
-            new Locale("is"), new Locale("it"), new Locale("iw"),
-            new Locale("ka"), new Locale("kk"), new Locale("km"),
-            new Locale("kn"), new Locale("ky"), new Locale("lo"),
-            new Locale("lt"), new Locale("lv"), new Locale("mk"),
-            new Locale("ml"), new Locale("mn"), new Locale("mr"),
-            new Locale("ms"), new Locale("my"), new Locale("nb"),
-            new Locale("ne"), new Locale("nl"), new Locale("pl"),
-            new Locale("pt"), new Locale("rm"), new Locale("ro"),
-            new Locale("ru"), new Locale("si"), new Locale("sk"),
-            new Locale("sl"), new Locale("sn"), new Locale("sr"),
-            new Locale("sv"), new Locale("sw"), new Locale("ta"),
-            new Locale("te"), new Locale("th"), new Locale("tl"),
-            new Locale("tr"), new Locale("uk"), new Locale("ur"),
-            new Locale("uz"), new Locale("vi"), new Locale("zu"),
-    };
-
-    private static final String USE_SYSTEM_DEFAULT = "";
-    private static final String defaultString = "Use System Default";
-
-    private static Locale locale;
-    private static Languages singleton;
-    private static Class<?> clazz;
-    private static int resId;
-    private static Map<String, String> tmpMap = new TreeMap<String, String>();
-    private static Map<String, String> nameMap;
-
-    static {
-        defaultLocale = Locale.getDefault();
-    }
-
-    private Languages(Activity activity) {
-        AssetManager assets = activity.getAssets();
-        Configuration config = activity.getResources().getConfiguration();
-        // Resources() requires DisplayMetrics, but they are only needed for drawables
-        DisplayMetrics ignored = new DisplayMetrics();
-        activity.getWindowManager().getDefaultDisplay().getMetrics(ignored);
-        Resources resources;
-        Set<Locale> localeSet = new LinkedHashSet<Locale>();
-        for (Locale locale : localesToTest) {
-            config.locale = locale;
-            resources = new Resources(assets, ignored, config);
-            if (!TextUtils.equals(defaultString, resources.getString(resId))
-                    || locale.equals(Locale.ENGLISH))
-                localeSet.add(locale);
-        }
-
-        for (Locale locale : localeSet) {
-            if (locale.equals(TIBETAN)) {
-                // include English name for devices without Tibetan font support
-                tmpMap.put(TIBETAN.toString(), "Tibetan བོད་སྐད།"); // Tibetan
-            } else if (locale.equals(Locale.SIMPLIFIED_CHINESE)) {
-                tmpMap.put(Locale.SIMPLIFIED_CHINESE.toString(), "中文 (中国)"); // Chinese (China)
-            } else if (locale.equals(Locale.TRADITIONAL_CHINESE)) {
-                tmpMap.put(Locale.TRADITIONAL_CHINESE.toString(), "中文 (台灣)"); // Chinese (Taiwan)
-            } else {
-                tmpMap.put(locale.toString(), locale.getDisplayLanguage(locale));
-            }
-        }
-
-        /* USE_SYSTEM_DEFAULT is a fake one for displaying in a chooser menu. */
-       // localeSet.add(null);
-       // tmpMap.put(USE_SYSTEM_DEFAULT, activity.getString(resId));
-        nameMap = Collections.unmodifiableMap(tmpMap);
-    }
-
-    /**
-     * Get the instance of {@link Languages} to work with, providing the
-     * {@link Activity} that is will be working as part of, as well as the
-     * {@code resId} that has the exact string "Use System Default",
-     * i.e. {@code R.string.use_system_default}.
-     * <p/>
-     * That string resource {@code resId} is also used to find the supported
-     * translations: if an included translation has a translated string that
-     * matches that {@code resId}, then that language will be included as a
-     * supported language.
-     *
-     * @param clazz the {@link Class} of the default {@code Activity},
-     *              usually the main {@code Activity} from where the
-     *              Settings is launched from.
-     * @param resId the string resource ID to for the string "Use System Default",
-     *              e.g. {@code R.string.use_system_default}
-     * @return
-     */
-    public static void setup(Class<?> clazz, int resId) {
-        if (Languages.clazz == null) {
-            Languages.clazz = clazz;
-            Languages.resId = resId;
-        } else {
-            throw new RuntimeException("Languages singleton was already initialized, duplicate call to Languages.setup()!");
-        }
-    }
-
-    /**
-     * Get the singleton to work with.
-     *
-     * @param activity the {@link Activity} this is working as part of
-     * @return
-     */
-    public static Languages get(Activity activity) {
-        if (singleton == null) {
-            singleton = new Languages(activity);
-        }
-        return singleton;
-    }
-
-    @SuppressLint("NewApi")
-	public static void setLanguage(final ContextWrapper contextWrapper, String language, boolean refresh) {
-        if (locale != null && TextUtils.equals(locale.getLanguage(), language) && (!refresh)) {
-            return; // already configured
-        } else if (language == null || language == USE_SYSTEM_DEFAULT) {
-            locale = defaultLocale;
-        } else {
-            /* handle locales with the country in it, i.e. zh_CN, zh_TW, etc */
-            String localeSplit[] = language.split("_");
-            if (localeSplit.length > 1) {
-                locale = new Locale(localeSplit[0], localeSplit[1]);
-            } else {
-                locale = new Locale(language);
-            }
-        }
-
-        setLocale(contextWrapper, locale);
-
-    }
-
-    @SuppressWarnings("deprecation")
-    private static void setLocale(final ContextWrapper contextWrapper, Locale locale){
-        Resources resources = contextWrapper.getResources();
-        Configuration configuration = resources.getConfiguration();
-        DisplayMetrics displayMetrics = resources.getDisplayMetrics();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
-            configuration.setLocale(locale);
-            contextWrapper.getApplicationContext().createConfigurationContext(configuration);
-        }
-        else{
-            configuration.locale=locale;
-            resources.updateConfiguration(configuration,displayMetrics);
-        }
-    }
-
-    /**
-     * Force reload the {@link Activity to make language changes take effect.}
-     *
-     * @param activity the {@code Activity} to force reload
-     */
-    public static void forceChangeLanguage(Activity activity) {
-        Intent intent = activity.getIntent();
-        if (intent == null) // when launched as LAUNCHER
-            return;
-        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-        activity.finish();
-        activity.overridePendingTransition(0, 0);
-        activity.startActivity(intent);
-        activity.overridePendingTransition(0, 0);
-    }
-
-    /**
-     * Return the name of the language based on the locale.
-     *
-     * @param locale
-     * @return
-     */
-    public String getName(String locale) {
-        String ret = nameMap.get(locale);
-        // if no match, try to return a more general name (i.e. English for
-        // en_IN)
-        if (ret == null && locale.contains("_"))
-            ret = nameMap.get(locale.split("_")[0]);
-        return ret;
-    }
-
-    /**
-     * Return an array of the names of all the supported languages, sorted to
-     * match what is returned by {@link Languages#getSupportedLocales()}.
-     *
-     * @return
-     */
-    public String[] getAllNames() {
-        return nameMap.values().toArray(new String[nameMap.size()]);
-    }
-
-    /**
-     * Get sorted list of supported locales.
-     *
-     * @return
-     */
-    public String[] getSupportedLocales() {
-        Set<String> keys = nameMap.keySet();
-        return keys.toArray(new String[keys.size()]);
-    }
-}
diff --git a/app-mini/src/main/java/org/torproject/android/mini/settings/LocaleHelper.java b/app-mini/src/main/java/org/torproject/android/mini/settings/LocaleHelper.java
deleted file mode 100644
index e4b29be2..00000000
--- a/app-mini/src/main/java/org/torproject/android/mini/settings/LocaleHelper.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package org.torproject.android.mini.settings;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Build;
-import org.torproject.android.service.util.Prefs;
-
-import java.util.Locale;
-
-/**
- * This class is used to change your application locale and persist this change for the next time
- * that your app is going to be used.
- * <p/>
- * You can also change the locale of your application on the fly by using the setLocale method.
- * <p/>
- * Created by gunhansancar on 07/10/15.
- * https://gunhansancar.com/change-language-programmatically-in-android/
- */
-public class LocaleHelper {
-
-    private static final String SELECTED_LANGUAGE = "Locale.Helper.Selected.Language";
-
-    public static Context onAttach(Context context) {
-        String lang = getPersistedData(context, Locale.getDefault().getLanguage());
-        return setLocale(context, lang);
-    }
-
-    public static Context onAttach(Context context, String defaultLanguage) {
-        String lang = getPersistedData(context, defaultLanguage);
-        return setLocale(context, lang);
-    }
-
-    public static String getLanguage(Context context) {
-        return getPersistedData(context, Locale.getDefault().getLanguage());
-    }
-
-    public static Context setLocale(Context context, String language) {
-        persist(context, language);
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            return updateResources(context, language);
-        }
-
-        return updateResourcesLegacy(context, language);
-    }
-
-    private static String getPersistedData(Context context, String defaultLanguage) {
-        return Prefs.getDefaultLocale();
-    }
-
-    private static void persist(Context context, String language) {
-        Prefs.setDefaultLocale(language);
-    }
-
-    @TargetApi(Build.VERSION_CODES.N)
-    private static Context updateResources(Context context, String language) {
-        Locale locale = new Locale(language);
-        Locale.setDefault(locale);
-
-        Configuration configuration = context.getResources().getConfiguration();
-        configuration.setLocale(locale);
-        configuration.setLayoutDirection(locale);
-
-        return context.createConfigurationContext(configuration);
-    }
-
-    @SuppressWarnings("deprecation")
-    private static Context updateResourcesLegacy(Context context, String language) {
-        Locale locale = new Locale(language);
-        Locale.setDefault(locale);
-
-        Resources resources = context.getResources();
-
-        Configuration configuration = resources.getConfiguration();
-        configuration.locale = locale;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-            configuration.setLayoutDirection(locale);
-        }
-
-        resources.updateConfiguration(configuration, resources.getDisplayMetrics());
-
-        return context;
-    }
-}
\ No newline at end of file
diff --git a/app-mini/src/main/java/org/torproject/android/mini/settings/SettingsPreferences.java b/app-mini/src/main/java/org/torproject/android/mini/settings/SettingsPreferences.java
deleted file mode 100644
index e5b5074b..00000000
--- a/app-mini/src/main/java/org/torproject/android/mini/settings/SettingsPreferences.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */
-/* See LICENSE for licensing information */
-
-package org.torproject.android.mini.settings;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.Preference.OnPreferenceChangeListener;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceScreen;
-import android.view.inputmethod.EditorInfo;
-import android.widget.EditText;
-import org.torproject.android.mini.R;
-
-public class SettingsPreferences extends PreferenceActivity {
-    private ListPreference prefLocale = null;
-
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        addPreferencesFromResource(R.xml.preferences);
-        setNoPersonalizedLearningOnEditTextPreferences();
-        getPreferenceManager().setSharedPreferencesMode(Context.MODE_MULTI_PROCESS);
-
-        prefLocale = (ListPreference) findPreference("pref_default_locale");
-
-        Languages languages = Languages.get(this);
-        prefLocale.setEntries(languages.getAllNames());
-        prefLocale.setEntryValues(languages.getSupportedLocales());
-        prefLocale.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
-            @Override
-            public boolean onPreferenceChange(Preference preference, Object newValue) {
-                String language = (String) newValue;
-                Intent intentResult = new Intent();
-                intentResult.putExtra("locale", language);
-                setResult(RESULT_OK, intentResult);
-                finish();
-                return false;
-            }
-        });
-    }
-
-    @Override
-    protected void attachBaseContext(Context base) {
-        super.attachBaseContext(LocaleHelper.onAttach(base));
-    }
-
-    private void setNoPersonalizedLearningOnEditTextPreferences() {
-        PreferenceScreen preferenceScreen = getPreferenceScreen();
-        int categoryCount = preferenceScreen.getPreferenceCount();
-        for (int i = 0; i < categoryCount; i++) {
-            Preference p = preferenceScreen.getPreference(i);
-            if (p instanceof PreferenceCategory) {
-                PreferenceCategory pc = (PreferenceCategory) p;
-                int preferenceCount = pc.getPreferenceCount();
-                for (int j = 0; j < preferenceCount; j++) {
-                    p = pc.getPreference(j);
-                    if (p instanceof EditTextPreference) {
-                        EditText editText = ((EditTextPreference) p).getEditText();
-                        editText.setImeOptions(editText.getImeOptions() | EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING);
-                    }
-                }
-            }
-        }
-    }
-
-}
diff --git a/app-mini/src/main/java/org/torproject/android/mini/ui/NoPersonalizedLearningEditText.java b/app-mini/src/main/java/org/torproject/android/mini/ui/NoPersonalizedLearningEditText.java
deleted file mode 100644
index 07d91429..00000000
--- a/app-mini/src/main/java/org/torproject/android/mini/ui/NoPersonalizedLearningEditText.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.torproject.android.mini.ui;
-
-import android.content.Context;
-import androidx.appcompat.widget.AppCompatEditText;
-import android.util.AttributeSet;
-import android.view.inputmethod.EditorInfo;
-
-public class NoPersonalizedLearningEditText extends AppCompatEditText {
-    public NoPersonalizedLearningEditText(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setImeOptions(getImeOptions() | EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING);
-    }
-}
diff --git a/app-mini/src/main/java/org/torproject/android/mini/ui/Rotate3dAnimation.java b/app-mini/src/main/java/org/torproject/android/mini/ui/Rotate3dAnimation.java
deleted file mode 100644
index ac8de549..00000000
--- a/app-mini/src/main/java/org/torproject/android/mini/ui/Rotate3dAnimation.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package org.torproject.android.mini.ui;
-
-import android.graphics.Camera;
-import android.graphics.Matrix;
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
-
-/**
- * An animation that rotates the view on the Y axis between two specified angles.
- * This animation also adds a translation on the Z axis (depth) to improve the effect.
- */
-public class Rotate3dAnimation extends Animation {
-    private final float mFromDegrees;
-    private final float mToDegrees;
-    private final float mCenterX;
-    private final float mCenterY;
-    private final float mDepthZ;
-    private final boolean mReverse;
-    private Camera mCamera;
-
-    /**
-     * Creates a new 3D rotation on the Y axis. The rotation is defined by its
-     * start angle and its end angle. Both angles are in degrees. The rotation
-     * is performed around a center point on the 2D space, definied by a pair
-     * of X and Y coordinates, called centerX and centerY. When the animation
-     * starts, a translation on the Z axis (depth) is performed. The length
-     * of the translation can be specified, as well as whether the translation
-     * should be reversed in time.
-     *
-     * @param fromDegrees the start angle of the 3D rotation
-     * @param toDegrees the end angle of the 3D rotation
-     * @param centerX the X center of the 3D rotation
-     * @param centerY the Y center of the 3D rotation
-     * @param reverse true if the translation should be reversed, false otherwise
-     */
-    public Rotate3dAnimation(float fromDegrees, float toDegrees,
-            float centerX, float centerY, float depthZ, boolean reverse) {
-        mFromDegrees = fromDegrees;
-        mToDegrees = toDegrees;
-        mCenterX = centerX;
-        mCenterY = centerY;
-        mDepthZ = depthZ;
-        mReverse = reverse;
-    }
-
-    @Override
-    public void initialize(int width, int height, int parentWidth, int parentHeight) {
-        super.initialize(width, height, parentWidth, parentHeight);
-        mCamera = new Camera();
-    }
-
-    @Override
-    protected void applyTransformation(float interpolatedTime, Transformation t) {
-        final float fromDegrees = mFromDegrees;
-        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
-
-        final float centerX = mCenterX;
-        final float centerY = mCenterY;
-        final Camera camera = mCamera;
-
-        final Matrix matrix = t.getMatrix();
-
-        camera.save();
-        if (mReverse) {
-            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
-        } else {
-            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
-        }
-        camera.rotateY(degrees);
-        camera.getMatrix(matrix);
-        camera.restore();
-
-        matrix.preTranslate(-centerX, -centerY);
-        matrix.postTranslate(centerX, centerY);
-    }
-}
diff --git a/app-mini/src/main/java/org/torproject/android/mini/ui/onboarding/OnboardingActivity.java b/app-mini/src/main/java/org/torproject/android/mini/ui/onboarding/OnboardingActivity.java
index bce4585a..56031c34 100644
--- a/app-mini/src/main/java/org/torproject/android/mini/ui/onboarding/OnboardingActivity.java
+++ b/app-mini/src/main/java/org/torproject/android/mini/ui/onboarding/OnboardingActivity.java
@@ -5,8 +5,9 @@ import android.os.Bundle;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
 import com.github.paolorotolo.appintro.AppIntro;
+
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.mini.R;
-import org.torproject.android.mini.settings.LocaleHelper;
 
 public class OnboardingActivity extends AppIntro {
 
diff --git a/app-mini/src/main/res/layout/content_app_config.xml b/app-mini/src/main/res/layout/content_app_config.xml
index 3a8c41a8..bdb56595 100644
--- a/app-mini/src/main/res/layout/content_app_config.xml
+++ b/app-mini/src/main/res/layout/content_app_config.xml
@@ -4,7 +4,6 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    app:layout_behavior="@string/appbar_scrolling_view_behavior"
     tools:context=".ui.AppConfigActivity"
     android:orientation="vertical"
     tools:showIn="@layout/activity_app_config"
diff --git a/app/build.gradle b/app/build.gradle
index 16656dc7..017315ca 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -125,6 +125,8 @@ dependencies {
     implementation 'com.google.android.material:material:1.1.0'
     implementation 'pl.bclogic:pulsator4droid:1.0.3'
     implementation 'com.github.apl-devs:appintro:v4.2.2'
+    implementation project(path: ':appcore')
+    implementation project(path: ':intentintegrator')
     androidTestImplementation "tools.fastlane:screengrab:1.2.0"
     implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
     implementation 'com.android.volley:volley:1.1.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fa7a54d4..971fd52b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -74,7 +74,7 @@
             android:theme="@android:style/Theme.Translucent" />
 
         <activity
-            android:name=".settings.SettingsPreferences"
+            android:name=".core.ui.SettingsPreferencesActivity"
             android:label="@string/app_name" />
 
         <activity
@@ -143,7 +143,7 @@
         </receiver>
 
         <receiver
-            android:name=".OnBootReceiver"
+            android:name=".core.OnBootReceiver"
             android:enabled="true"
             android:exported="true">
             <intent-filter>
diff --git a/app/src/main/java/com/google/zxing/integration/android/IntentResult.java b/app/src/main/java/com/google/zxing/integration/android/IntentResult.java
deleted file mode 100644
index 2469af92..00000000
--- a/app/src/main/java/com/google/zxing/integration/android/IntentResult.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 2009 ZXing authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.zxing.integration.android;
-
-/**
- * <p>Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.</p>
- *
- * @author Sean Owen
- */
-public final class IntentResult {
-
-  private final String contents;
-  private final String formatName;
-  private final byte[] rawBytes;
-  private final Integer orientation;
-  private final String errorCorrectionLevel;
-
-  IntentResult() {
-    this(null, null, null, null, null);
-  }
-
-  IntentResult(String contents,
-               String formatName,
-               byte[] rawBytes,
-               Integer orientation,
-               String errorCorrectionLevel) {
-    this.contents = contents;
-    this.formatName = formatName;
-    this.rawBytes = rawBytes;
-    this.orientation = orientation;
-    this.errorCorrectionLevel = errorCorrectionLevel;
-  }
-
-  /**
-   * @return raw content of barcode
-   */
-  public String getContents() {
-    return contents;
-  }
-
-  /**
-   * @return name of format, like "QR_CODE", "UPC_A". See {@code BarcodeFormat} for more format names.
-   */
-  public String getFormatName() {
-    return formatName;
-  }
-
-  /**
-   * @return raw bytes of the barcode content, if applicable, or null otherwise
-   */
-  public byte[] getRawBytes() {
-    return rawBytes;
-  }
-
-  /**
-   * @return rotation of the image, in degrees, which resulted in a successful scan. May be null.
-   */
-  public Integer getOrientation() {
-    return orientation;
-  }
-
-  /**
-   * @return name of the error correction level used in the barcode, if applicable
-   */
-  public String getErrorCorrectionLevel() {
-    return errorCorrectionLevel;
-  }
-  
-  @Override
-  public String toString() {
-    StringBuilder dialogText = new StringBuilder(100);
-    dialogText.append("Format: ").append(formatName).append('\n');
-    dialogText.append("Contents: ").append(contents).append('\n');
-    int rawBytesLength = rawBytes == null ? 0 : rawBytes.length;
-    dialogText.append("Raw bytes: (").append(rawBytesLength).append(" bytes)\n");
-    dialogText.append("Orientation: ").append(orientation).append('\n');
-    dialogText.append("EC level: ").append(errorCorrectionLevel).append('\n');
-    return dialogText.toString();
-  }
-
-}
diff --git a/app/src/main/java/org/torproject/android/OnBootReceiver.java b/app/src/main/java/org/torproject/android/OnBootReceiver.java
deleted file mode 100644
index b8d88e58..00000000
--- a/app/src/main/java/org/torproject/android/OnBootReceiver.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.torproject.android;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import org.torproject.android.service.OrbotService;
-import org.torproject.android.service.TorServiceConstants;
-import org.torproject.android.service.util.Prefs;
-
-public class OnBootReceiver extends BroadcastReceiver {
-
-	private static boolean sReceivedBoot = false;
-
-	@Override
-	public void onReceive(Context context, Intent intent) {
-
-		if (Prefs.startOnBoot() && (!sReceivedBoot))
-		{
-			startService(TorServiceConstants.ACTION_START_ON_BOOT, context);
-			sReceivedBoot = true;
-		}
-	}
-
-
-	private void startService (String action, Context context)
-	{
-		Intent intent = new Intent(context, OrbotService.class);
-		intent.setAction(action);
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-			context.startForegroundService(intent);
-		}
-		else
-		{
-			context.startService(intent);
-		}
-
-	}
-
-
-}
-
diff --git a/app/src/main/java/org/torproject/android/OrbotApp.java b/app/src/main/java/org/torproject/android/OrbotApp.java
index 7cb4317f..d037efa9 100644
--- a/app/src/main/java/org/torproject/android/OrbotApp.java
+++ b/app/src/main/java/org/torproject/android/OrbotApp.java
@@ -4,10 +4,10 @@ import android.app.Application;
 import android.content.Context;
 import android.content.res.Configuration;
 
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.service.OrbotConstants;
 import org.torproject.android.service.util.Prefs;
-import org.torproject.android.settings.Languages;
-import org.torproject.android.settings.LocaleHelper;
+import org.torproject.android.core.Languages;
 
 import java.util.Locale;
 
@@ -26,7 +26,7 @@ public class OrbotApp extends Application implements OrbotConstants {
     @Override
     protected void attachBaseContext(Context base) {
         Prefs.setContext(base);
-        super.attachBaseContext(LocaleHelper.onAttach(base, Prefs.getDefaultLocale()));
+        super.attachBaseContext(LocaleHelper.onAttach(base));
     }
 
     @Override
diff --git a/app/src/main/java/org/torproject/android/OrbotMainActivity.java b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
index 81631e9f..61b6333d 100644
--- a/app/src/main/java/org/torproject/android/OrbotMainActivity.java
+++ b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
@@ -54,17 +54,17 @@ import com.google.zxing.integration.android.IntentIntegrator;
 import com.google.zxing.integration.android.IntentResult;
 
 import org.json.JSONArray;
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.service.OrbotConstants;
 import org.torproject.android.service.OrbotService;
 import org.torproject.android.service.TorServiceConstants;
 import org.torproject.android.service.util.Prefs;
 import org.torproject.android.service.util.Utils;
 import org.torproject.android.service.vpn.VpnPrefs;
-import org.torproject.android.settings.Languages;
-import org.torproject.android.settings.LocaleHelper;
-import org.torproject.android.settings.SettingsPreferences;
+import org.torproject.android.core.Languages;
+import org.torproject.android.core.ui.SettingsPreferencesActivity;
 import org.torproject.android.ui.AppManagerActivity;
-import org.torproject.android.ui.Rotate3dAnimation;
+import org.torproject.android.core.ui.Rotate3dAnimation;
 import org.torproject.android.ui.dialog.AboutDialogFragment;
 import org.torproject.android.ui.hiddenservices.ClientCookiesActivity;
 import org.torproject.android.ui.hiddenservices.HiddenServicesActivity;
@@ -449,10 +449,10 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
         if (item.getItemId() == R.id.menu_newnym) {
             requestNewTorIdentity();
         } else if (item.getItemId() == R.id.menu_settings) {
-            Intent intent = new Intent(OrbotMainActivity.this, SettingsPreferences.class);
+            Intent intent = SettingsPreferencesActivity.createIntent(this, R.xml.preferences);
             startActivityForResult(intent, REQUEST_SETTINGS);
         } else if (item.getItemId() == R.id.menu_exit) {
-            doExit(); // exit appp
+            doExit(); // exit app
         } else if (item.getItemId() == R.id.menu_about) {
             new AboutDialogFragment().show(getSupportFragmentManager(), AboutDialogFragment.TAG);
         } else if (item.getItemId() == R.id.menu_scan) {
diff --git a/app/src/main/java/org/torproject/android/settings/Languages.java b/app/src/main/java/org/torproject/android/settings/Languages.java
deleted file mode 100644
index b9a76b91..00000000
--- a/app/src/main/java/org/torproject/android/settings/Languages.java
+++ /dev/null
@@ -1,195 +0,0 @@
-package org.torproject.android.settings;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.ContextWrapper;
-import android.content.res.AssetManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-public class Languages {
-
-    public static Locale defaultLocale;
-    public static final Locale TIBETAN = new Locale("bo");
-    static final Locale localesToTest[] = {
-            Locale.ENGLISH, Locale.FRENCH, Locale.GERMAN,
-            Locale.ITALIAN, Locale.JAPANESE, Locale.KOREAN,
-            Locale.TRADITIONAL_CHINESE, Locale.SIMPLIFIED_CHINESE,
-            TIBETAN, new Locale("af"), new Locale("am"),
-            new Locale("ar"), new Locale("ay"), new Locale("az"), new Locale("bg"),
-            new Locale("bn"), new Locale("ca"), new Locale("cs"),
-            new Locale("da"), new Locale("el"), new Locale("es"),
-            new Locale("et"), new Locale("eu"), new Locale("fa"),
-            new Locale("fi"), new Locale("gl"), new Locale("hi"),
-            new Locale("hr"), new Locale("hu"), new Locale("hy"),
-            new Locale("in"), new Locale("hy"), new Locale("in"),
-            new Locale("is"), new Locale("it"), new Locale("iw"),
-            new Locale("ka"), new Locale("kk"), new Locale("km"),
-            new Locale("kn"), new Locale("ky"), new Locale("lo"),
-            new Locale("lt"), new Locale("lv"), new Locale("mk"),
-            new Locale("ml"), new Locale("mn"), new Locale("mr"),
-            new Locale("ms"), new Locale("my"), new Locale("nb"),
-            new Locale("ne"), new Locale("nl"), new Locale("pl"),
-            new Locale("pt"), new Locale("rm"), new Locale("ro"),
-            new Locale("ru"), new Locale("si"), new Locale("sk"),
-            new Locale("sl"), new Locale("sn"), new Locale("sr"),
-            new Locale("sv"), new Locale("sw"), new Locale("ta"),
-            new Locale("te"), new Locale("th"), new Locale("tl"),
-            new Locale("tr"), new Locale("uk"), new Locale("ur"),
-            new Locale("uz"), new Locale("vi"), new Locale("zu"),
-    };
-
-    private static final String USE_SYSTEM_DEFAULT = "";
-    private static final String defaultString = "Use System Default";
-
-    private static Locale locale;
-    private static Languages singleton;
-    private static Class<?> clazz;
-    private static int resId;
-    private static Map<String, String> tmpMap = new TreeMap<String, String>();
-    private static Map<String, String> nameMap;
-
-
-    private Languages(Activity activity) {
-
-
-        AssetManager assets = activity.getAssets();
-        Configuration config = activity.getResources().getConfiguration();
-        // Resources() requires DisplayMetrics, but they are only needed for drawables
-        DisplayMetrics ignored = new DisplayMetrics();
-        activity.getWindowManager().getDefaultDisplay().getMetrics(ignored);
-        Resources resources;
-        Set<Locale> localeSet = new LinkedHashSet<>();
-        for (Locale locale : localesToTest) {
-            resources = new Resources(assets, ignored, config);
-            if (!TextUtils.equals(defaultString, resources.getString(resId))
-                    || locale.equals(Locale.ENGLISH))
-                localeSet.add(locale);
-        }
-
-        for (Locale locale : localeSet) {
-            if (locale.equals(TIBETAN)) {
-                // include English name for devices without Tibetan font support
-                tmpMap.put(TIBETAN.toString(), "Tibetan བོད་སྐད།"); // Tibetan
-            } else if (locale.equals(Locale.SIMPLIFIED_CHINESE)) {
-                tmpMap.put(Locale.SIMPLIFIED_CHINESE.toString(), "中文 (中国)"); // Chinese (China)
-            } else if (locale.equals(Locale.TRADITIONAL_CHINESE)) {
-                tmpMap.put(Locale.TRADITIONAL_CHINESE.toString(), "中文 (台灣)"); // Chinese (Taiwan)
-            } else {
-                tmpMap.put(locale.toString(), locale.getDisplayLanguage(locale));
-            }
-        }
-
-        /* USE_SYSTEM_DEFAULT is a fake one for displaying in a chooser menu. */
-       // localeSet.add(null);
-       // tmpMap.put(USE_SYSTEM_DEFAULT, activity.getString(resId));
-        nameMap = Collections.unmodifiableMap(tmpMap);
-    }
-
-    /**
-     * Get the instance of {@link Languages} to work with, providing the
-     * {@link Activity} that is will be working as part of, as well as the
-     * {@code resId} that has the exact string "Use System Default",
-     * i.e. {@code R.string.use_system_default}.
-     * <p/>
-     * That string resource {@code resId} is also used to find the supported
-     * translations: if an included translation has a translated string that
-     * matches that {@code resId}, then that language will be included as a
-     * supported language.
-     *
-     * @param clazz the {@link Class} of the default {@code Activity},
-     *              usually the main {@code Activity} from where the
-     *              Settings is launched from.
-     * @param resId the string resource ID to for the string "Use System Default",
-     *              e.g. {@code R.string.use_system_default}
-     * @return
-     */
-    public static void setup(Class<?> clazz, int resId) {
-        defaultLocale = Locale.getDefault();
-
-        if (Languages.clazz == null) {
-            Languages.clazz = clazz;
-            Languages.resId = resId;
-        } else {
-            throw new RuntimeException("Languages singleton was already initialized, duplicate call to Languages.setup()!");
-        }
-    }
-
-    /**
-     * Get the singleton to work with.
-     *
-     * @param activity the {@link Activity} this is working as part of
-     * @return
-     */
-    public static Languages get(Activity activity) {
-        if (singleton == null) {
-            singleton = new Languages(activity);
-        }
-        return singleton;
-    }
-
-    @SuppressLint("NewApi")
-	public static void setLanguage(final ContextWrapper contextWrapper, String language, boolean refresh) {
-        if (locale != null && TextUtils.equals(locale.getLanguage(), language) && (!refresh)) {
-            return; // already configured
-        } else if (language == null || language == USE_SYSTEM_DEFAULT) {
-            locale = defaultLocale;
-        } else {
-            /* handle locales with the country in it, i.e. zh_CN, zh_TW, etc */
-            String localeSplit[] = language.split("_");
-            if (localeSplit.length > 1) {
-                locale = new Locale(localeSplit[0], localeSplit[1]);
-            } else {
-                locale = new Locale(language);
-            }
-        }
-
-        setLocale(contextWrapper, locale);
-
-    }
-
-    @SuppressWarnings("deprecation")
-    private static void setLocale(final ContextWrapper contextWrapper, Locale locale){
-        Resources resources = contextWrapper.getResources();
-        Configuration configuration = resources.getConfiguration();
-        DisplayMetrics displayMetrics = resources.getDisplayMetrics();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
-            configuration.setLocale(locale);
-            contextWrapper.getApplicationContext().createConfigurationContext(configuration);
-        }
-        else{
-            configuration.locale=locale;
-            resources.updateConfiguration(configuration,displayMetrics);
-        }
-    }
-
-    /**
-     * Return an array of the names of all the supported languages, sorted to
-     * match what is returned by {@link Languages#getSupportedLocales()}.
-     *
-     * @return
-     */
-    public String[] getAllNames() {
-        return nameMap.values().toArray(new String[nameMap.size()]);
-    }
-
-    /**
-     * Get sorted list of supported locales.
-     *
-     * @return
-     */
-    public String[] getSupportedLocales() {
-        Set<String> keys = nameMap.keySet();
-        return keys.toArray(new String[keys.size()]);
-    }
-}
diff --git a/app/src/main/java/org/torproject/android/settings/LocaleHelper.java b/app/src/main/java/org/torproject/android/settings/LocaleHelper.java
deleted file mode 100644
index 4fb9e833..00000000
--- a/app/src/main/java/org/torproject/android/settings/LocaleHelper.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package org.torproject.android.settings;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Build;
-import org.torproject.android.service.util.Prefs;
-
-import java.util.Locale;
-
-/**
- * This class is used to change your application locale and persist this change for the next time
- * that your app is going to be used.
- * <p/>
- * You can also change the locale of your application on the fly by using the setLocale method.
- * <p/>
- * Created by gunhansancar on 07/10/15.
- * https://gunhansancar.com/change-language-programmatically-in-android/
- */
-public class LocaleHelper {
-
-    public static Context onAttach(Context context) {
-        String lang = getPersistedData(context, Locale.getDefault().getLanguage());
-        return setLocale(context, lang);
-    }
-
-    public static Context onAttach(Context context, String defaultLanguage) {
-        String lang = getPersistedData(context, defaultLanguage);
-        return setLocale(context, lang);
-    }
-
-    public static Context setLocale(Context context, String language) {
-        persist(context, language);
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            return updateResources(context, language);
-        }
-
-        return updateResourcesLegacy(context, language);
-    }
-
-    private static String getPersistedData(Context context, String defaultLanguage) {
-        return Prefs.getDefaultLocale();
-    }
-
-    private static void persist(Context context, String language) {
-        Prefs.setDefaultLocale(language);
-    }
-
-    @TargetApi(Build.VERSION_CODES.N)
-    private static Context updateResources(Context context, String language) {
-        Locale locale = new Locale(language);
-        Locale.setDefault(locale);
-
-        Configuration configuration = context.getResources().getConfiguration();
-        configuration.setLocale(locale);
-        configuration.setLayoutDirection(locale);
-
-        return context.createConfigurationContext(configuration);
-    }
-
-    @SuppressWarnings("deprecation")
-    private static Context updateResourcesLegacy(Context context, String language) {
-        Locale locale = new Locale(language);
-        Locale.setDefault(locale);
-
-        Resources resources = context.getResources();
-
-        Configuration configuration = resources.getConfiguration();
-        configuration.locale = locale;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-            configuration.setLayoutDirection(locale);
-        }
-
-        resources.updateConfiguration(configuration, resources.getDisplayMetrics());
-
-        return context;
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java b/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java
deleted file mode 100644
index 4279fd65..00000000
--- a/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */
-/* See LICENSE for licensing information */
-
-package org.torproject.android.settings;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceScreen;
-import android.view.inputmethod.EditorInfo;
-import android.widget.EditText;
-import org.torproject.android.R;
-
-public class SettingsPreferences extends PreferenceActivity {
-    private ListPreference prefLocale = null;
-
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        addPreferencesFromResource(R.xml.preferences);
-        setNoPersonalizedLearningOnEditTextPreferences();
-        getPreferenceManager().setSharedPreferencesMode(Context.MODE_MULTI_PROCESS);
-
-        prefLocale = (ListPreference) findPreference("pref_default_locale");
-
-        Languages languages = Languages.get(this);
-        prefLocale.setEntries(languages.getAllNames());
-        prefLocale.setEntryValues(languages.getSupportedLocales());
-        prefLocale.setOnPreferenceChangeListener((preference, newValue) -> {
-            String language = (String) newValue;
-            Intent intentResult = new Intent();
-            intentResult.putExtra("locale", language);
-            setResult(RESULT_OK, intentResult);
-            finish();
-            return false;
-        });
-    }
-
-    @Override
-    protected void attachBaseContext(Context base) {
-        super.attachBaseContext(LocaleHelper.onAttach(base));
-    }
-
-    private void setNoPersonalizedLearningOnEditTextPreferences() {
-        PreferenceScreen preferenceScreen = getPreferenceScreen();
-        int categoryCount = preferenceScreen.getPreferenceCount();
-        for (int i = 0; i < categoryCount; i++) {
-            Preference p = preferenceScreen.getPreference(i);
-            if (p instanceof PreferenceCategory) {
-                PreferenceCategory pc = (PreferenceCategory) p;
-                int preferenceCount = pc.getPreferenceCount();
-                for (int j = 0; j < preferenceCount; j++) {
-                    p = pc.getPreference(j);
-                    if (p instanceof EditTextPreference) {
-                        EditText editText = ((EditTextPreference) p).getEditText();
-                        editText.setImeOptions(editText.getImeOptions() | EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING);
-                    }
-                }
-            }
-        }
-    }
-
-}
diff --git a/app/src/main/java/org/torproject/android/ui/NoPersonalizedLearningEditText.java b/app/src/main/java/org/torproject/android/ui/NoPersonalizedLearningEditText.java
deleted file mode 100644
index 7a21c7c5..00000000
--- a/app/src/main/java/org/torproject/android/ui/NoPersonalizedLearningEditText.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.torproject.android.ui;
-
-import android.content.Context;
-import androidx.appcompat.widget.AppCompatEditText;
-import android.util.AttributeSet;
-import android.view.inputmethod.EditorInfo;
-
-public class NoPersonalizedLearningEditText extends AppCompatEditText {
-    public NoPersonalizedLearningEditText(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setImeOptions(getImeOptions() | EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING);
-    }
-}
diff --git a/app/src/main/java/org/torproject/android/ui/Rotate3dAnimation.java b/app/src/main/java/org/torproject/android/ui/Rotate3dAnimation.java
deleted file mode 100644
index 7829d2db..00000000
--- a/app/src/main/java/org/torproject/android/ui/Rotate3dAnimation.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package org.torproject.android.ui;
-
-import android.graphics.Camera;
-import android.graphics.Matrix;
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
-
-/**
- * An animation that rotates the view on the Y axis between two specified angles.
- * This animation also adds a translation on the Z axis (depth) to improve the effect.
- */
-public class Rotate3dAnimation extends Animation {
-    private final float mFromDegrees;
-    private final float mToDegrees;
-    private final float mCenterX;
-    private final float mCenterY;
-    private final float mDepthZ;
-    private final boolean mReverse;
-    private Camera mCamera;
-
-    /**
-     * Creates a new 3D rotation on the Y axis. The rotation is defined by its
-     * start angle and its end angle. Both angles are in degrees. The rotation
-     * is performed around a center point on the 2D space, definied by a pair
-     * of X and Y coordinates, called centerX and centerY. When the animation
-     * starts, a translation on the Z axis (depth) is performed. The length
-     * of the translation can be specified, as well as whether the translation
-     * should be reversed in time.
-     *
-     * @param fromDegrees the start angle of the 3D rotation
-     * @param toDegrees the end angle of the 3D rotation
-     * @param centerX the X center of the 3D rotation
-     * @param centerY the Y center of the 3D rotation
-     * @param reverse true if the translation should be reversed, false otherwise
-     */
-    public Rotate3dAnimation(float fromDegrees, float toDegrees,
-            float centerX, float centerY, float depthZ, boolean reverse) {
-        mFromDegrees = fromDegrees;
-        mToDegrees = toDegrees;
-        mCenterX = centerX;
-        mCenterY = centerY;
-        mDepthZ = depthZ;
-        mReverse = reverse;
-    }
-
-    @Override
-    public void initialize(int width, int height, int parentWidth, int parentHeight) {
-        super.initialize(width, height, parentWidth, parentHeight);
-        mCamera = new Camera();
-    }
-
-    @Override
-    protected void applyTransformation(float interpolatedTime, Transformation t) {
-        final float fromDegrees = mFromDegrees;
-        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
-
-        final float centerX = mCenterX;
-        final float centerY = mCenterY;
-        final Camera camera = mCamera;
-
-        final Matrix matrix = t.getMatrix();
-
-        camera.save();
-        if (mReverse) {
-            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
-        } else {
-            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
-        }
-        camera.rotateY(degrees);
-        camera.getMatrix(matrix);
-        camera.restore();
-
-        matrix.preTranslate(-centerX, -centerY);
-        matrix.postTranslate(centerX, centerY);
-    }
-}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
index 0ed20f57..c5f8a5e1 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
@@ -20,7 +20,7 @@ import com.google.zxing.integration.android.IntentResult;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.torproject.android.R;
-import org.torproject.android.settings.LocaleHelper;
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.ui.hiddenservices.adapters.ClientCookiesAdapter;
 import org.torproject.android.ui.hiddenservices.dialogs.AddCookieDialog;
 import org.torproject.android.ui.hiddenservices.dialogs.CookieActionsDialog;
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
index 8ea67ec7..bfd29645 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
@@ -24,7 +24,7 @@ import android.widget.ListView;
 import android.widget.Spinner;
 
 import org.torproject.android.R;
-import org.torproject.android.settings.LocaleHelper;
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.ui.hiddenservices.adapters.OnionListAdapter;
 import org.torproject.android.ui.hiddenservices.dialogs.HSActionsDialog;
 import org.torproject.android.ui.hiddenservices.dialogs.HSDataDialog;
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
index 8efa47d3..67918d13 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
@@ -13,7 +13,7 @@ import org.torproject.android.R;
 import org.torproject.android.service.TorServiceConstants;
 import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
 import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
-import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+import org.torproject.android.core.ExternalStorage;
 
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java
index 120b0693..03f7b506 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java
@@ -10,7 +10,7 @@ import android.widget.ListView;
 import org.torproject.android.R;
 import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter;
 import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
-import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+import org.torproject.android.core.ExternalStorage;
 
 import java.io.File;
 import java.util.ArrayList;
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java
index 6f02f872..642c5c17 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java
@@ -10,7 +10,7 @@ import android.widget.ListView;
 import org.torproject.android.R;
 import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter;
 import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
-import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+import org.torproject.android.core.ExternalStorage;
 
 import java.io.File;
 import java.util.ArrayList;
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java
deleted file mode 100644
index f3780f87..00000000
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.torproject.android.ui.hiddenservices.storage;
-
-import android.os.Environment;
-
-import java.io.File;
-
-public class ExternalStorage {
-    private static final String ORBOT_BACKUPS_DIR = "Orbot";
-
-    public static File getOrCreateBackupDir() {
-        if (!isExternalStorageWritable())
-            return null;
-
-        File dir = new File(Environment.getExternalStorageDirectory(), ORBOT_BACKUPS_DIR);
-
-        if (!dir.isDirectory() && !dir.mkdirs())
-            return null;
-
-        return dir;
-    }
-
-    /* Checks if external storage is available for read and write */
-    private static boolean isExternalStorageWritable() {
-        String state = Environment.getExternalStorageState();
-        return Environment.MEDIA_MOUNTED.equals(state);
-    }
-
-}
diff --git a/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java b/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java
index be8ab1bf..d8f35f86 100644
--- a/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java
@@ -8,7 +8,6 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.CompoundButton;
 import android.widget.RadioButton;
 import android.widget.TextView;
 
@@ -18,9 +17,8 @@ import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
 
 import org.torproject.android.R;
-import org.torproject.android.service.OrbotService;
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.service.util.Prefs;
-import org.torproject.android.settings.LocaleHelper;
 
 import java.io.BufferedReader;
 import java.io.IOException;
diff --git a/app/src/main/java/org/torproject/android/ui/onboarding/OnboardingActivity.java b/app/src/main/java/org/torproject/android/ui/onboarding/OnboardingActivity.java
index 8e796724..7868faf5 100644
--- a/app/src/main/java/org/torproject/android/ui/onboarding/OnboardingActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/onboarding/OnboardingActivity.java
@@ -11,8 +11,8 @@ import androidx.fragment.app.Fragment;
 import com.github.paolorotolo.appintro.AppIntro;
 
 import org.torproject.android.R;
+import org.torproject.android.core.LocaleHelper;
 import org.torproject.android.service.util.Prefs;
-import org.torproject.android.settings.LocaleHelper;
 import org.torproject.android.ui.AppManagerActivity;
 import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
 
diff --git a/app/src/main/res/layout/layout_add_client_cookie_dialog.xml b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml
index 2f4b9885..c4725ff3 100644
--- a/app/src/main/res/layout/layout_add_client_cookie_dialog.xml
+++ b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml
@@ -15,7 +15,7 @@
         android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"
         android:paddingLeft="5dp" />
 
-    <org.torproject.android.ui.NoPersonalizedLearningEditText
+    <org.torproject.android.core.ui.NoPersonalizedLearningEditText
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:inputType="text"
@@ -29,7 +29,7 @@
         android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"
         android:paddingLeft="5dp" />
 
-    <org.torproject.android.ui.NoPersonalizedLearningEditText
+    <org.torproject.android.core.ui.NoPersonalizedLearningEditText
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:ems="10"
diff --git a/app/src/main/res/layout/layout_hs_data_dialog.xml b/app/src/main/res/layout/layout_hs_data_dialog.xml
index a4ac590e..cf390d31 100644
--- a/app/src/main/res/layout/layout_hs_data_dialog.xml
+++ b/app/src/main/res/layout/layout_hs_data_dialog.xml
@@ -15,7 +15,7 @@
         android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"
         android:paddingLeft="5dp" />
 
-    <org.torproject.android.ui.NoPersonalizedLearningEditText
+    <org.torproject.android.core.ui.NoPersonalizedLearningEditText
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:inputType="text"
@@ -29,7 +29,7 @@
         android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"
         android:paddingLeft="5dp" />
 
-    <org.torproject.android.ui.NoPersonalizedLearningEditText
+    <org.torproject.android.core.ui.NoPersonalizedLearningEditText
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:ems="10"
@@ -43,7 +43,7 @@
         android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"
         android:paddingLeft="5dp" />
 
-    <org.torproject.android.ui.NoPersonalizedLearningEditText
+    <org.torproject.android.core.ui.NoPersonalizedLearningEditText
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:ems="10"
diff --git a/appcore/.gitignore b/appcore/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/appcore/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/appcore/build.gradle b/appcore/build.gradle
new file mode 100644
index 00000000..701ce2bf
--- /dev/null
+++ b/appcore/build.gradle
@@ -0,0 +1,41 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 16
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+    implementation 'androidx.core:core-ktx:1.3.1'
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+    implementation project(path: ':orbotservice')
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+
+}
\ No newline at end of file
diff --git a/appcore/consumer-rules.pro b/appcore/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/appcore/proguard-rules.pro b/appcore/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/appcore/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/appcore/src/androidTest/java/org/torproject/android/core/ExampleInstrumentedTest.kt b/appcore/src/androidTest/java/org/torproject/android/core/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..b7e9cad1
--- /dev/null
+++ b/appcore/src/androidTest/java/org/torproject/android/core/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package org.torproject.android.core
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+ at RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("org.torproject.android.core.test", appContext.packageName)
+    }
+}
\ No newline at end of file
diff --git a/appcore/src/main/AndroidManifest.xml b/appcore/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..64220dd7
--- /dev/null
+++ b/appcore/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.torproject.android.core">
+</manifest>
\ No newline at end of file
diff --git a/appcore/src/main/java/org/torproject/android/core/ExternalStorage.kt b/appcore/src/main/java/org/torproject/android/core/ExternalStorage.kt
new file mode 100644
index 00000000..77493195
--- /dev/null
+++ b/appcore/src/main/java/org/torproject/android/core/ExternalStorage.kt
@@ -0,0 +1,15 @@
+package org.torproject.android.core
+
+import android.os.Environment
+import java.io.File
+
+object ExternalStorage {
+    private const val ORBOT_BACKUPS_DIR = "Orbot"
+    @JvmStatic
+    fun getOrCreateBackupDir(): File? {
+        // Checks if external storage is available for read and write
+        if (Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()) return null
+        val dir = File(Environment.getExternalStorageDirectory(), ORBOT_BACKUPS_DIR)
+        return if (!dir.isDirectory && !dir.mkdirs()) null else dir
+    }
+}
\ No newline at end of file
diff --git a/appcore/src/main/java/org/torproject/android/core/Languages.kt b/appcore/src/main/java/org/torproject/android/core/Languages.kt
new file mode 100644
index 00000000..a76a816b
--- /dev/null
+++ b/appcore/src/main/java/org/torproject/android/core/Languages.kt
@@ -0,0 +1,174 @@
+package org.torproject.android.core
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.ContextWrapper
+import android.content.res.Resources
+import android.os.Build
+import android.text.TextUtils
+import android.util.DisplayMetrics
+import java.util.*
+
+class Languages private constructor(activity: Activity) {
+    /**
+     * Return an array of the names of all the supported languages, sorted to
+     * match what is returned by [Languages.supportedLocales].
+     *
+     * @return
+     */
+    val allNames: Array<String>
+        get() = nameMap.values.toTypedArray()
+
+    val supportedLocales: Array<String>
+        get() {
+            val keys = nameMap.keys
+            return keys.toTypedArray()
+        }
+
+    companion object {
+        private var defaultLocale: Locale? = null
+        val TIBETAN = Locale("bo")
+        val localesToTest = arrayOf(
+                Locale.ENGLISH, Locale.FRENCH, Locale.GERMAN,
+                Locale.ITALIAN, Locale.JAPANESE, Locale.KOREAN,
+                Locale.TRADITIONAL_CHINESE, Locale.SIMPLIFIED_CHINESE,
+                TIBETAN, Locale("af"), Locale("am"),
+                Locale("ar"), Locale("ay"), Locale("az"), Locale("bg"),
+                Locale("bn"), Locale("ca"), Locale("cs"),
+                Locale("da"), Locale("el"), Locale("es"),
+                Locale("et"), Locale("eu"), Locale("fa"),
+                Locale("fi"), Locale("gl"), Locale("hi"),
+                Locale("hr"), Locale("hu"), Locale("hy"),
+                Locale("in"), Locale("hy"), Locale("in"),
+                Locale("is"), Locale("it"), Locale("iw"),
+                Locale("ka"), Locale("kk"), Locale("km"),
+                Locale("kn"), Locale("ky"), Locale("lo"),
+                Locale("lt"), Locale("lv"), Locale("mk"),
+                Locale("ml"), Locale("mn"), Locale("mr"),
+                Locale("ms"), Locale("my"), Locale("nb"),
+                Locale("ne"), Locale("nl"), Locale("pl"),
+                Locale("pt"), Locale("rm"), Locale("ro"),
+                Locale("ru"), Locale("si"), Locale("sk"),
+                Locale("sl"), Locale("sn"), Locale("sr"),
+                Locale("sv"), Locale("sw"), Locale("ta"),
+                Locale("te"), Locale("th"), Locale("tl"),
+                Locale("tr"), Locale("uk"), Locale("ur"),
+                Locale("uz"), Locale("vi"), Locale("zu"))
+        private const val USE_SYSTEM_DEFAULT = ""
+        private const val defaultString = "Use System Default"
+        private var locale: Locale? = null
+        private var singleton: Languages? = null
+        private var clazz: Class<*>? = null
+        private var resId = 0
+        private val tmpMap: MutableMap<String, String> = TreeMap()
+        private lateinit var nameMap: Map<String, String>
+
+        /**
+         * Get the instance of [Languages] to work with, providing the
+         * [Activity] that is will be working as part of, as well as the
+         * `resId` that has the exact string "Use System Default",
+         * i.e. `R.string.use_system_default`.
+         *
+         *
+         * That string resource `resId` is also used to find the supported
+         * translations: if an included translation has a translated string that
+         * matches that `resId`, then that language will be included as a
+         * supported language.
+         *
+         * @param clazz the [Class] of the default `Activity`,
+         * usually the main `Activity` from where the
+         * Settings is launched from.
+         * @param resId the string resource ID to for the string "Use System Default",
+         * e.g. `R.string.use_system_default`
+         * @return
+         */
+        @JvmStatic
+        fun setup(clazz: Class<*>?, resId: Int) {
+            defaultLocale = Locale.getDefault()
+            if (Companion.clazz == null) {
+                Companion.clazz = clazz
+                Companion.resId = resId
+            } else {
+                throw RuntimeException("Languages singleton was already initialized, duplicate call to Languages.setup()!")
+            }
+        }
+
+        /**
+         * Get the singleton to work with.
+         *
+         * @param activity the [Activity] this is working as part of
+         * @return
+         */
+        @JvmStatic
+        operator fun get(activity: Activity): Languages? {
+            if (singleton == null) {
+                singleton = Languages(activity)
+            }
+            return singleton
+        }
+
+        @JvmStatic
+        @SuppressLint("NewApi")
+        fun setLanguage(contextWrapper: ContextWrapper, language: String?, refresh: Boolean) {
+            locale = if (locale != null && TextUtils.equals(locale!!.language, language) && !refresh) {
+                return  // already configured
+            } else if (language == null || language === USE_SYSTEM_DEFAULT) {
+                defaultLocale
+            } else {
+                /* handle locales with the country in it, i.e. zh_CN, zh_TW, etc */
+                val localeSplit = language.split("_".toRegex()).toTypedArray()
+                if (localeSplit.size > 1) {
+                    Locale(localeSplit[0], localeSplit[1])
+                } else {
+                    Locale(language)
+                }
+            }
+            setLocale(contextWrapper, locale)
+        }
+
+        private fun setLocale(contextWrapper: ContextWrapper, locale: Locale?) {
+            val resources = contextWrapper.resources
+            val configuration = resources.configuration
+            val displayMetrics = resources.displayMetrics
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                configuration.setLocale(locale)
+                contextWrapper.applicationContext.createConfigurationContext(configuration)
+            } else {
+                configuration.locale = locale
+                resources.updateConfiguration(configuration, displayMetrics)
+            }
+        }
+    }
+
+    init {
+        val assets = activity.assets
+        val config = activity.resources.configuration
+        // Resources() requires DisplayMetrics, but they are only needed for drawables
+        val ignored = DisplayMetrics()
+        activity.windowManager.defaultDisplay.getMetrics(ignored)
+        var resources: Resources
+        val localeSet: MutableSet<Locale> = LinkedHashSet()
+        for (locale in localesToTest) {
+            resources = Resources(assets, ignored, config)
+            if (!TextUtils.equals(defaultString, resources.getString(resId))
+                    || locale == Locale.ENGLISH) localeSet.add(locale)
+        }
+        for (locale in localeSet) {
+            if (locale == TIBETAN) {
+                // include English name for devices without Tibetan font support
+                tmpMap[TIBETAN.toString()] = "Tibetan བོད་སྐད།" // Tibetan
+            } else if (locale == Locale.SIMPLIFIED_CHINESE) {
+                tmpMap[Locale.SIMPLIFIED_CHINESE.toString()] = "中文 (中国)" // Chinese (China)
+            } else if (locale == Locale.TRADITIONAL_CHINESE) {
+                tmpMap[Locale.TRADITIONAL_CHINESE.toString()] = "中文 (台灣)" // Chinese (Taiwan)
+            } else {
+                tmpMap[locale.toString()] = locale.getDisplayLanguage(locale)
+            }
+        }
+
+        /* USE_SYSTEM_DEFAULT is a fake one for displaying in a chooser menu. */
+        // localeSet.add(null);
+        // tmpMap.put(USE_SYSTEM_DEFAULT, activity.getString(resId));
+        nameMap = Collections.unmodifiableMap(tmpMap)
+    }
+}
\ No newline at end of file
diff --git a/appcore/src/main/java/org/torproject/android/core/LocaleHelper.kt b/appcore/src/main/java/org/torproject/android/core/LocaleHelper.kt
new file mode 100644
index 00000000..271e1c8a
--- /dev/null
+++ b/appcore/src/main/java/org/torproject/android/core/LocaleHelper.kt
@@ -0,0 +1,55 @@
+package org.torproject.android.core
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.os.Build
+import org.torproject.android.service.util.Prefs
+import java.util.*
+
+/**
+ * This class is used to change your application locale and persist this change for the next time
+ * that your app is going to be used.
+ *
+ *
+ * You can also change the locale of your application on the fly by using the setLocale method.
+ *
+ *
+ * Created by gunhansancar on 07/10/15.
+ * https://gunhansancar.com/change-language-programmatically-in-android/
+ */
+object LocaleHelper {
+    @JvmStatic
+    fun onAttach(context: Context): Context = setLocale(context, Prefs.getDefaultLocale())
+
+    private fun setLocale(context: Context, language: String): Context {
+        Prefs.setDefaultLocale(language)
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+            updateResources(context, language)
+        else
+            updateResourcesLegacy(context, language)
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun updateResources(context: Context, language: String): Context {
+        val locale = Locale(language)
+        Locale.setDefault(locale)
+        val configuration = context.resources.configuration
+        configuration.setLocale(locale)
+        configuration.setLayoutDirection(locale)
+        return context.createConfigurationContext(configuration)
+    }
+
+    @SuppressWarnings("deprecation")
+    private fun updateResourcesLegacy(context: Context, language: String): Context {
+        val locale = Locale(language)
+        Locale.setDefault(locale)
+        val resources = context.resources
+        val configuration = resources.configuration
+        configuration.locale = locale
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            configuration.setLayoutDirection(locale)
+        }
+        resources.updateConfiguration(configuration, resources.displayMetrics)
+        return context
+    }
+}
\ No newline at end of file
diff --git a/appcore/src/main/java/org/torproject/android/core/OnBootReceiver.kt b/appcore/src/main/java/org/torproject/android/core/OnBootReceiver.kt
new file mode 100644
index 00000000..3602a249
--- /dev/null
+++ b/appcore/src/main/java/org/torproject/android/core/OnBootReceiver.kt
@@ -0,0 +1,31 @@
+package org.torproject.android.core
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import org.torproject.android.service.OrbotService
+import org.torproject.android.service.TorServiceConstants
+import org.torproject.android.service.util.Prefs
+
+class OnBootReceiver : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent) {
+        if (Prefs.startOnBoot() && !sReceivedBoot) {
+            startService(TorServiceConstants.ACTION_START_ON_BOOT, context)
+            sReceivedBoot = true
+        }
+    }
+
+    private fun startService(action: String, context: Context) {
+        val intent = Intent(context, OrbotService::class.java).apply { this.action = action }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+            context.startForegroundService(intent)
+        else {
+            context.startService(intent)
+        }
+    }
+
+    companion object {
+        private var sReceivedBoot = false
+    }
+}
\ No newline at end of file
diff --git a/appcore/src/main/java/org/torproject/android/core/ui/NoPersonalizedLearningEditText.kt b/appcore/src/main/java/org/torproject/android/core/ui/NoPersonalizedLearningEditText.kt
new file mode 100644
index 00000000..d9e7dfb7
--- /dev/null
+++ b/appcore/src/main/java/org/torproject/android/core/ui/NoPersonalizedLearningEditText.kt
@@ -0,0 +1,12 @@
+package org.torproject.android.core.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.inputmethod.EditorInfo
+import androidx.appcompat.widget.AppCompatEditText
+
+class NoPersonalizedLearningEditText(context: Context, attrs: AttributeSet?) : AppCompatEditText(context, attrs) {
+    init {
+        imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+    }
+}
\ No newline at end of file
diff --git a/appcore/src/main/java/org/torproject/android/core/ui/Rotate3dAnimation.kt b/appcore/src/main/java/org/torproject/android/core/ui/Rotate3dAnimation.kt
new file mode 100644
index 00000000..3e4a484b
--- /dev/null
+++ b/appcore/src/main/java/org/torproject/android/core/ui/Rotate3dAnimation.kt
@@ -0,0 +1,48 @@
+package org.torproject.android.core.ui
+
+import android.graphics.Camera
+import android.view.animation.Animation
+import android.view.animation.Transformation
+
+/**
+ * An animation that rotates the view on the Y axis between two specified angles.
+ * This animation also adds a translation on the Z axis (depth) to improve the effect.
+ */
+class Rotate3dAnimation
+/**
+ * Creates a new 3D rotation on the Y axis. The rotation is defined by its
+ * start angle and its end angle. Both angles are in degrees. The rotation
+ * is performed around a center point on the 2D space, defined by a pair
+ * of X and Y coordinates, called centerX and centerY. When the animation
+ * starts, a translation on the Z axis (depth) is performed. The length
+ * of the translation can be specified, as well as whether the translation
+ * should be reversed in time.
+ *
+ * @param fromDegrees the start angle of the 3D rotation
+ * @param toDegrees the end angle of the 3D rotation
+ * @param centerX the X center of the 3D rotation
+ * @param centerY the Y center of the 3D rotation
+ * @param reverse true if the translation should be reversed, false otherwise
+ */(private val mFromDegrees: Float, private val mToDegrees: Float,
+    private val mCenterX: Float, private val mCenterY: Float, private val mDepthZ: Float, private val mReverse: Boolean) : Animation() {
+    private lateinit var mCamera: Camera
+    override fun initialize(width: Int, height: Int, parentWidth: Int, parentHeight: Int) {
+        super.initialize(width, height, parentWidth, parentHeight)
+        mCamera = Camera()
+    }
+
+    override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
+        val degrees = mFromDegrees + (mToDegrees - mFromDegrees) * interpolatedTime
+        with(mCamera) {
+            save()
+            if (mReverse) translate(0.0f, 0.0f, mDepthZ * interpolatedTime)
+            else translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime))
+            rotateY(degrees)
+            getMatrix(t.matrix)
+            restore()
+        }
+        t.matrix.preTranslate(-mCenterX, -mCenterY)
+        t.matrix.postTranslate(mCenterX, mCenterY)
+    }
+
+}
\ No newline at end of file
diff --git a/appcore/src/main/java/org/torproject/android/core/ui/SettingsPreferencesActivity.kt b/appcore/src/main/java/org/torproject/android/core/ui/SettingsPreferencesActivity.kt
new file mode 100644
index 00000000..a8eaf644
--- /dev/null
+++ b/appcore/src/main/java/org/torproject/android/core/ui/SettingsPreferencesActivity.kt
@@ -0,0 +1,65 @@
+/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */ /* See LICENSE for licensing information */
+package org.torproject.android.core.ui
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.preference.*
+import android.preference.Preference.OnPreferenceChangeListener
+import android.view.inputmethod.EditorInfo
+import androidx.annotation.XmlRes
+import org.torproject.android.core.Languages
+import org.torproject.android.core.LocaleHelper
+
+class SettingsPreferencesActivity : PreferenceActivity() {
+    private var prefLocale: ListPreference? = null
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        addPreferencesFromResource(intent.getIntExtra(BUNDLE_KEY_PREFERENCES_XML, 0))
+        setNoPersonalizedLearningOnEditTextPreferences()
+        preferenceManager.sharedPreferencesMode = MODE_MULTI_PROCESS
+        prefLocale = findPreference("pref_default_locale") as ListPreference
+        val languages = Languages[this]
+        prefLocale?.entries = languages!!.allNames
+        prefLocale?.entryValues = languages.supportedLocales
+        prefLocale?.onPreferenceChangeListener = OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
+            val language = newValue as String?
+            val intentResult = Intent()
+            intentResult.putExtra("locale", language)
+            setResult(RESULT_OK, intentResult)
+            finish()
+            false
+        }
+    }
+
+    override fun attachBaseContext(newBase: Context) = super.attachBaseContext(LocaleHelper.onAttach(newBase))
+
+    private fun setNoPersonalizedLearningOnEditTextPreferences() {
+        val preferenceScreen = preferenceScreen
+        val categoryCount = preferenceScreen.preferenceCount
+        for (i in 0 until categoryCount) {
+            var p = preferenceScreen.getPreference(i)
+            if (p is PreferenceCategory) {
+                val pc = p
+                val preferenceCount = pc.preferenceCount
+                for (j in 0 until preferenceCount) {
+                    p = pc.getPreference(j)
+                    if (p is EditTextPreference) {
+                        val editText = p.editText
+                        editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+                    }
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val BUNDLE_KEY_PREFERENCES_XML = "prefxml"
+        @JvmStatic
+        fun createIntent(context: Context?, @XmlRes xmlPrefId: Int): Intent {
+            val intent = Intent(context, SettingsPreferencesActivity::class.java)
+            intent.putExtra(BUNDLE_KEY_PREFERENCES_XML, xmlPrefId)
+            return intent
+        }
+    }
+}
\ No newline at end of file
diff --git a/appcore/src/test/java/org/torproject/android/core/ExampleUnitTest.kt b/appcore/src/test/java/org/torproject/android/core/ExampleUnitTest.kt
new file mode 100644
index 00000000..9b2de166
--- /dev/null
+++ b/appcore/src/test/java/org/torproject/android/core/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package org.torproject.android.core
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 031c2bfc..51012a87 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,11 +1,15 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 buildscript {
+    ext {
+        kotlin_version = '1.3.72'
+    }
     repositories {
         jcenter()
         google()
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:4.0.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }
 
diff --git a/intentintegrator/.gitignore b/intentintegrator/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/intentintegrator/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/intentintegrator/build.gradle b/intentintegrator/build.gradle
new file mode 100644
index 00000000..81efeb31
--- /dev/null
+++ b/intentintegrator/build.gradle
@@ -0,0 +1,30 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 16
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+}
\ No newline at end of file
diff --git a/intentintegrator/consumer-rules.pro b/intentintegrator/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/intentintegrator/proguard-rules.pro b/intentintegrator/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/intentintegrator/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/intentintegrator/src/main/AndroidManifest.xml b/intentintegrator/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a0410a4b
--- /dev/null
+++ b/intentintegrator/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.zxing.integration.android">
+
+    /
+</manifest>
\ No newline at end of file
diff --git a/app/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java b/intentintegrator/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java
similarity index 100%
rename from app/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java
rename to intentintegrator/src/main/java/com/google/zxing/integration/android/IntentIntegrator.java
diff --git a/app-mini/src/main/java/com/google/zxing/integration/android/IntentResult.java b/intentintegrator/src/main/java/com/google/zxing/integration/android/IntentResult.java
similarity index 100%
rename from app-mini/src/main/java/com/google/zxing/integration/android/IntentResult.java
rename to intentintegrator/src/main/java/com/google/zxing/integration/android/IntentResult.java
diff --git a/settings.gradle b/settings.gradle
index 05e4830c..46cf1758 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,3 @@
+include ':intentintegrator'
+include ':appcore'
 include ':orbotservice',':app',':app-mini'





More information about the tor-commits mailing list