Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>master
@@ -1,18 +0,0 @@ | |||
package com.wireguard.android; | |||
import android.app.Fragment; | |||
import android.os.Bundle; | |||
import android.view.LayoutInflater; | |||
import android.view.View; | |||
import android.view.ViewGroup; | |||
/** | |||
* Fragment containing a simple placeholder message. | |||
*/ | |||
public class PlaceholderFragment extends Fragment { | |||
@Override | |||
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { | |||
return inflater.inflate(R.layout.placeholder_fragment, parent, false); | |||
} | |||
} |
@@ -1,54 +1,53 @@ | |||
package com.wireguard.android; | |||
import android.app.Activity; | |||
import android.app.Fragment; | |||
import android.app.FragmentManager; | |||
import android.app.FragmentTransaction; | |||
import android.content.ComponentName; | |||
import android.content.Context; | |||
import android.content.Intent; | |||
import android.content.ServiceConnection; | |||
import android.content.res.Configuration; | |||
import android.os.Bundle; | |||
import android.os.IBinder; | |||
import android.view.Menu; | |||
import android.view.MenuItem; | |||
import com.wireguard.config.Profile; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
/** | |||
* Activity that allows creating/viewing/editing/deleting WireGuard profiles. | |||
*/ | |||
public class ProfileActivity extends Activity { | |||
private final ServiceConnection connection = new ProfileServiceConnection(); | |||
public class ProfileActivity extends ServiceClientActivity<ProfileServiceInterface> { | |||
public static final String KEY_PROFILE_NAME = "profile_name"; | |||
// FIXME: These must match the constants in profile_list_activity.xml | |||
private static final String TAG_DETAIL = "detail"; | |||
private static final String TAG_LIST = "list"; | |||
private String currentProfile; | |||
private boolean isSplitLayout; | |||
private final List<ServiceConnectionListener> listeners = new ArrayList<>(); | |||
private ProfileServiceInterface service; | |||
public void addServiceConnectionListener(ServiceConnectionListener listener) { | |||
listeners.add(listener); | |||
public ProfileActivity() { | |||
super(ProfileService.class); | |||
} | |||
public ProfileServiceInterface getService() { | |||
return service; | |||
@Override | |||
public void onBackPressed() { | |||
if (!isSplitLayout && currentProfile != null) { | |||
onProfileSelected(null); | |||
} else { | |||
super.onBackPressed(); | |||
} | |||
} | |||
@Override | |||
protected void onCreate(Bundle savedInstanceState) { | |||
super.onCreate(savedInstanceState); | |||
// This layout consists only of containers for fragments. | |||
// Restore the saved profile if there is one; otherwise grab it from the intent. | |||
if (savedInstanceState != null) | |||
currentProfile = savedInstanceState.getString(KEY_PROFILE_NAME); | |||
else | |||
currentProfile = getIntent().getStringExtra(KEY_PROFILE_NAME); | |||
// Set up the base layout and fill it with fragments. | |||
setContentView(R.layout.profile_activity); | |||
isSplitLayout = findViewById(R.id.detail_fragment_container) != null; | |||
// Fill the layout with the initial set of fragments. | |||
final FragmentTransaction transaction = getFragmentManager().beginTransaction(); | |||
transaction.add(R.id.list_fragment_container, new ProfileListFragment()); | |||
if (isSplitLayout) | |||
transaction.add(R.id.detail_fragment_container, new PlaceholderFragment()); | |||
transaction.commit(); | |||
// Ensure the long-running service is started. This only needs to happen once. | |||
final Intent intent = new Intent(this, ProfileService.class); | |||
startService(intent); | |||
final int orientation = getResources().getConfiguration().orientation; | |||
isSplitLayout = orientation == Configuration.ORIENTATION_LANDSCAPE; | |||
updateLayout(currentProfile); | |||
} | |||
@Override | |||
@@ -57,50 +56,54 @@ public class ProfileActivity extends Activity { | |||
return true; | |||
} | |||
public void onMenuSettings(MenuItem item) { | |||
public void onMenuEdit(MenuItem item) { | |||
} | |||
public void onProfileSelected(Profile profile) { | |||
public void onMenuSave(MenuItem item) { | |||
} | |||
@Override | |||
public void onStart() { | |||
super.onStart(); | |||
Intent intent = new Intent(this, ProfileService.class); | |||
bindService(intent, connection, Context.BIND_AUTO_CREATE); | |||
} | |||
public void onMenuSettings(MenuItem item) { | |||
@Override | |||
public void onStop() { | |||
super.onStop(); | |||
if (service != null) { | |||
unbindService(connection); | |||
for (ServiceConnectionListener listener : listeners) | |||
listener.onServiceDisconnected(); | |||
service = null; | |||
} | |||
} | |||
public void removeServiceConnectionListener(ServiceConnectionListener listener) { | |||
listeners.remove(listener); | |||
public void onProfileSelected(String profile) { | |||
updateLayout(profile); | |||
currentProfile = profile; | |||
} | |||
private class ProfileServiceConnection implements ServiceConnection { | |||
@Override | |||
public void onServiceConnected(ComponentName component, IBinder binder) { | |||
service = (ProfileServiceInterface) binder; | |||
for (ServiceConnectionListener listener : listeners) | |||
listener.onServiceConnected(service); | |||
} | |||
@Override | |||
public void onSaveInstanceState(Bundle outState) { | |||
super.onSaveInstanceState(outState); | |||
outState.putString(KEY_PROFILE_NAME, currentProfile); | |||
} | |||
@Override | |||
public void onServiceDisconnected(ComponentName component) { | |||
// This function is only called when the service crashes or goes away unexpectedly. | |||
for (ServiceConnectionListener listener : listeners) | |||
listener.onServiceDisconnected(); | |||
service = null; | |||
private void updateLayout(String profile) { | |||
final FragmentManager fm = getFragmentManager(); | |||
final Fragment detailFragment = fm.findFragmentByTag(TAG_DETAIL); | |||
final Fragment listFragment = fm.findFragmentByTag(TAG_LIST); | |||
final FragmentTransaction transaction = fm.beginTransaction(); | |||
if (profile != null) { | |||
if (isSplitLayout) { | |||
if (listFragment.isHidden()) | |||
transaction.show(listFragment); | |||
} else { | |||
transaction.hide(listFragment); | |||
} | |||
if (detailFragment.isHidden()) | |||
transaction.show(detailFragment); | |||
} else { | |||
if (isSplitLayout) { | |||
if (detailFragment.isHidden()) | |||
transaction.show(detailFragment); | |||
} else { | |||
transaction.hide(detailFragment); | |||
} | |||
if (listFragment.isHidden()) | |||
transaction.show(listFragment); | |||
} | |||
transaction.commit(); | |||
((ProfileDetailFragment) detailFragment).setProfile(profile); | |||
} | |||
} |
@@ -1,51 +0,0 @@ | |||
package com.wireguard.android; | |||
import android.app.Fragment; | |||
import android.content.Context; | |||
/** | |||
* Base class for fragments that are part of a ProfileActivity. | |||
*/ | |||
public class ProfileActivityFragment extends Fragment implements ServiceConnectionListener { | |||
private ProfileActivity activity; | |||
protected ProfileServiceInterface service; | |||
@Override | |||
public void onAttach(Context context) { | |||
super.onAttach(context); | |||
activity = (ProfileActivity) context; | |||
} | |||
@Override | |||
public void onDetach() { | |||
super.onDetach(); | |||
activity = null; | |||
} | |||
@Override | |||
public void onStart() { | |||
super.onStart(); | |||
activity.addServiceConnectionListener(this); | |||
// If the service is already connected, there will be no callback, so run the handler now. | |||
final ProfileServiceInterface service = activity.getService(); | |||
if (service != null) | |||
onServiceConnected(service); | |||
} | |||
@Override | |||
public void onStop() { | |||
super.onStop(); | |||
activity.removeServiceConnectionListener(this); | |||
} | |||
@Override | |||
public void onServiceConnected(ProfileServiceInterface service) { | |||
this.service = service; | |||
} | |||
@Override | |||
public void onServiceDisconnected() { | |||
service = null; | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
package com.wireguard.android; | |||
import android.os.Bundle; | |||
import android.view.LayoutInflater; | |||
import android.view.Menu; | |||
import android.view.MenuInflater; | |||
import android.view.View; | |||
import android.view.ViewGroup; | |||
import com.wireguard.android.databinding.ProfileDetailFragmentBinding; | |||
/** | |||
* Fragment for viewing and editing a WireGuard profile. | |||
*/ | |||
public class ProfileDetailFragment extends ServiceClientFragment<ProfileServiceInterface> { | |||
private ProfileDetailFragmentBinding binding; | |||
private String name; | |||
public ProfileDetailFragment() { | |||
super(); | |||
setArguments(new Bundle()); | |||
} | |||
@Override | |||
public void onCreate(Bundle savedInstanceState) { | |||
super.onCreate(savedInstanceState); | |||
name = getArguments().getString(ProfileActivity.KEY_PROFILE_NAME); | |||
setHasOptionsMenu(true); | |||
} | |||
@Override | |||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | |||
inflater.inflate(R.menu.profile_detail, menu); | |||
} | |||
@Override | |||
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { | |||
binding = ProfileDetailFragmentBinding.inflate(inflater, parent, false); | |||
return binding.getRoot(); | |||
} | |||
@Override | |||
public void onServiceConnected(ProfileServiceInterface service) { | |||
super.onServiceConnected(service); | |||
binding.setProfile(service.getProfiles().get(name)); | |||
} | |||
public void setProfile(String name) { | |||
this.name = name; | |||
getArguments().putString(ProfileActivity.KEY_PROFILE_NAME, name); | |||
final ProfileServiceInterface service = getService(); | |||
if (binding != null && service != null) | |||
binding.setProfile(service.getProfiles().get(name)); | |||
} | |||
} |
@@ -11,10 +11,10 @@ import com.wireguard.android.databinding.ProfileListFragmentBinding; | |||
import com.wireguard.config.Profile; | |||
/** | |||
* Fragment containing the list of available WireGuard profiles. Must be part of a ProfileActivity. | |||
* Fragment containing the list of available WireGuard profiles. | |||
*/ | |||
public class ProfileListFragment extends ProfileActivityFragment { | |||
public class ProfileListFragment extends ServiceClientFragment<ProfileServiceInterface> { | |||
private ProfileListFragmentBinding binding; | |||
@Override | |||
@@ -25,7 +25,7 @@ public class ProfileListFragment extends ProfileActivityFragment { | |||
@Override | |||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | |||
final Profile profile = (Profile) parent.getItemAtPosition(position); | |||
((ProfileActivity) getActivity()).onProfileSelected(profile); | |||
((ProfileActivity) getActivity()).onProfileSelected(profile.getName()); | |||
} | |||
}); | |||
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { | |||
@@ -33,6 +33,7 @@ public class ProfileListFragment extends ProfileActivityFragment { | |||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, | |||
long id) { | |||
final Profile profile = (Profile) parent.getItemAtPosition(position); | |||
final ProfileServiceInterface service = getService(); | |||
if (profile == null || service == null) | |||
return false; | |||
if (profile.getIsConnected()) | |||
@@ -38,6 +38,9 @@ public class ProfileService extends Service { | |||
@Override | |||
public void onCreate() { | |||
rootShell = new RootShell(this); | |||
// Ensure the service sticks around after being unbound. This only needs to happen once. | |||
final Intent intent = new Intent(this, ProfileService.class); | |||
startService(intent); | |||
new ProfileLoader().execute(getFilesDir().listFiles(new FilenameFilter() { | |||
@Override | |||
public boolean accept(File dir, String name) { | |||
@@ -8,7 +8,7 @@ import com.wireguard.config.Profile; | |||
* Interface for the background connection service. | |||
*/ | |||
public interface ProfileServiceInterface { | |||
interface ProfileServiceInterface { | |||
/** | |||
* Attempt to set up and enable an interface for this profile. The profile's connection state | |||
* will be updated if connection is successful. If this profile is already connected, or it is | |||
@@ -0,0 +1,75 @@ | |||
package com.wireguard.android; | |||
import android.app.Activity; | |||
import android.content.ComponentName; | |||
import android.content.Context; | |||
import android.content.Intent; | |||
import android.content.ServiceConnection; | |||
import android.os.IBinder; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
/** | |||
* Base class for activities that maintain a connection to a background service. | |||
*/ | |||
abstract class ServiceClientActivity<T> extends Activity implements ServiceConnectionProvider<T> { | |||
private final ServiceConnectionCallbacks callbacks = new ServiceConnectionCallbacks(); | |||
private final List<ServiceConnectionListener<T>> listeners = new ArrayList<>(); | |||
private T service; | |||
private final Class<?> serviceClass; | |||
protected ServiceClientActivity(Class<?> serviceClass) { | |||
this.serviceClass = serviceClass; | |||
} | |||
@Override | |||
public void addServiceConnectionListener(ServiceConnectionListener<T> listener) { | |||
listeners.add(listener); | |||
} | |||
public T getService() { | |||
return service; | |||
} | |||
@Override | |||
public void onStart() { | |||
super.onStart(); | |||
bindService(new Intent(this, serviceClass), callbacks, Context.BIND_AUTO_CREATE); | |||
} | |||
@Override | |||
public void onStop() { | |||
super.onStop(); | |||
if (service != null) { | |||
service = null; | |||
unbindService(callbacks); | |||
for (ServiceConnectionListener listener : listeners) | |||
listener.onServiceDisconnected(); | |||
} | |||
} | |||
@Override | |||
public void removeServiceConnectionListener(ServiceConnectionListener<T> listener) { | |||
listeners.remove(listener); | |||
} | |||
private class ServiceConnectionCallbacks implements ServiceConnection { | |||
@Override | |||
public void onServiceConnected(ComponentName component, IBinder binder) { | |||
@SuppressWarnings("unchecked") | |||
final T localBinder = (T) binder; | |||
service = localBinder; | |||
for (ServiceConnectionListener<T> listener : listeners) | |||
listener.onServiceConnected(service); | |||
} | |||
@Override | |||
public void onServiceDisconnected(ComponentName component) { | |||
service = null; | |||
for (ServiceConnectionListener<T> listener : listeners) | |||
listener.onServiceDisconnected(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,61 @@ | |||
package com.wireguard.android; | |||
import android.app.Fragment; | |||
import android.content.Context; | |||
/** | |||
* Base class for fragments in activities that maintain a connection to a background service. | |||
*/ | |||
abstract class ServiceClientFragment<T> extends Fragment implements ServiceConnectionListener<T> { | |||
private ServiceConnectionProvider<T> provider; | |||
private T service; | |||
protected T getService() { | |||
return service; | |||
} | |||
@Override | |||
public void onAttach(Context context) { | |||
super.onAttach(context); | |||
@SuppressWarnings("unchecked") | |||
final ServiceConnectionProvider<T> localContext = (ServiceConnectionProvider<T>) context; | |||
provider = localContext; | |||
} | |||
@Override | |||
public void onDetach() { | |||
super.onDetach(); | |||
provider = null; | |||
} | |||
@Override | |||
public void onStart() { | |||
super.onStart(); | |||
provider.addServiceConnectionListener(this); | |||
// Run the handler if the connection state changed while we were not paying attention. | |||
final T localService = provider.getService(); | |||
if (localService != service) { | |||
if (localService != null) | |||
onServiceConnected(localService); | |||
else | |||
onServiceDisconnected(); | |||
} | |||
} | |||
@Override | |||
public void onStop() { | |||
super.onStop(); | |||
provider.removeServiceConnectionListener(this); | |||
} | |||
@Override | |||
public void onServiceConnected(T service) { | |||
this.service = service; | |||
} | |||
@Override | |||
public void onServiceDisconnected() { | |||
service = null; | |||
} | |||
} |
@@ -1,11 +1,11 @@ | |||
package com.wireguard.android; | |||
/** | |||
* Interface for fragments that need notification about connection changes to the ProfileService. | |||
* Interface for fragments that need notification about service connection changes. | |||
*/ | |||
interface ServiceConnectionListener { | |||
void onServiceConnected(ProfileServiceInterface service); | |||
interface ServiceConnectionListener<T> { | |||
void onServiceConnected(T service); | |||
void onServiceDisconnected(); | |||
} |
@@ -0,0 +1,13 @@ | |||
package com.wireguard.android; | |||
/** | |||
* Interface for activities that provide a connection to a service. | |||
*/ | |||
interface ServiceConnectionProvider<T> { | |||
void addServiceConnectionListener(ServiceConnectionListener<T> listener); | |||
T getService(); | |||
void removeServiceConnectionListener(ServiceConnectionListener<T> listener); | |||
} |
@@ -0,0 +1,9 @@ | |||
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:width="48dp" | |||
android:height="48dp" | |||
android:viewportWidth="24.0" | |||
android:viewportHeight="24.0"> | |||
<path | |||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" | |||
android:fillColor="#FFFFFF"/> | |||
</vector> |
@@ -0,0 +1,10 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:width="48dp" | |||
android:height="48dp" | |||
android:viewportHeight="24.0" | |||
android:viewportWidth="24.0"> | |||
<path | |||
android:fillColor="#FFFFFF" | |||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" /> | |||
</vector> |
@@ -1,19 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:baselineAligned="false" | |||
android:orientation="horizontal"> | |||
<FrameLayout | |||
android:id="@+id/list_fragment_container" | |||
android:layout_width="0dp" | |||
android:layout_height="match_parent" | |||
android:layout_weight="1" /> | |||
<FrameLayout | |||
android:id="@+id/detail_fragment_container" | |||
android:layout_width="0dp" | |||
android:layout_height="match_parent" | |||
android:layout_weight="2" /> | |||
</LinearLayout> |
@@ -1,7 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<TextView xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:id="@+id/text" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:gravity="center" | |||
android:text="@string/placeholder_text" /> |
@@ -1,5 +1,21 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:id="@+id/list_fragment_container" | |||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" /> | |||
android:layout_height="match_parent" | |||
android:baselineAligned="false" | |||
android:orientation="horizontal"> | |||
<fragment | |||
android:name="com.wireguard.android.ProfileListFragment" | |||
android:layout_width="0dp" | |||
android:layout_height="match_parent" | |||
android:layout_weight="1" | |||
android:tag="list" /> | |||
<fragment | |||
android:name="com.wireguard.android.ProfileDetailFragment" | |||
android:layout_width="0dp" | |||
android:layout_height="match_parent" | |||
android:layout_weight="2" | |||
android:tag="detail" /> | |||
</LinearLayout> |
@@ -0,0 +1,71 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<layout xmlns:android="http://schemas.android.com/apk/res/android" | |||
xmlns:app="http://schemas.android.com/apk/res-auto"> | |||
<data> | |||
<import type="android.view.View" /> | |||
<variable | |||
name="profile" | |||
type="com.wireguard.config.Profile" /> | |||
</data> | |||
<FrameLayout | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:padding="16dp"> | |||
<TextView | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:gravity="center" | |||
android:text="@string/placeholder_text" | |||
android:visibility="@{profile == null ? View.VISIBLE : View.GONE}" /> | |||
<ScrollView | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:visibility="@{profile == null ? View.GONE : View.VISIBLE}"> | |||
<RelativeLayout | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content"> | |||
<TextView | |||
android:id="@+id/profile_name_label" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:layout_alignParentTop="true" | |||
android:labelFor="@+id/profile_name_text" | |||
android:text="@string/profile_name" /> | |||
<TextView | |||
android:id="@+id/profile_name_text" | |||
style="?android:attr/textAppearanceMedium" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:layout_below="@+id/profile_name_label" | |||
android:text="@{profile.name}" /> | |||
<TextView | |||
android:id="@+id/public_key_label" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:layout_below="@+id/profile_name_text" | |||
android:labelFor="@+id/public_key_text" | |||
android:text="@string/public_key" /> | |||
<TextView | |||
android:id="@+id/public_key_text" | |||
style="?android:attr/textAppearanceMedium" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:layout_below="@+id/public_key_label" | |||
android:ellipsize="end" | |||
android:maxLines="1" | |||
android:text="@{profile.interface.publicKey}" /> | |||
</RelativeLayout> | |||
</ScrollView> | |||
</FrameLayout> | |||
</layout> |
@@ -4,6 +4,7 @@ | |||
<data> | |||
<!--suppress AndroidDomInspection --> | |||
<variable | |||
name="profiles" | |||
type="android.databinding.ObservableArrayMap<String, com.wireguard.config.Profile>" /> | |||
@@ -0,0 +1,9 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<menu xmlns:android="http://schemas.android.com/apk/res/android"> | |||
<item | |||
android:alphabeticShortcut="e" | |||
android:icon="@drawable/ic_action_edit" | |||
android:onClick="onMenuEdit" | |||
android:showAsAction="always" | |||
android:title="@string/edit" /> | |||
</menu> |
@@ -0,0 +1,9 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<menu xmlns:android="http://schemas.android.com/apk/res/android"> | |||
<item | |||
android:alphabeticShortcut="s" | |||
android:icon="@drawable/ic_action_save" | |||
android:onClick="onMenuSave" | |||
android:showAsAction="always" | |||
android:title="@string/save" /> | |||
</menu> |
@@ -3,6 +3,9 @@ | |||
<string name="app_name">WireGuard</string> | |||
<string name="connected">Connected</string> | |||
<string name="disconnected">Disconnected</string> | |||
<string name="placeholder_text">No profile selected.</string> | |||
<string name="placeholder_text">No profile selected</string> | |||
<string name="profile_name">Profile name</string> | |||
<string name="public_key">Public key</string> | |||
<string name="save">Save</string> | |||
<string name="settings">Settings</string> | |||
</resources> |