diff --git a/app/src/main/java/com/wireguard/android/PlaceholderFragment.java b/app/src/main/java/com/wireguard/android/PlaceholderFragment.java deleted file mode 100644 index 8c15e757..00000000 --- a/app/src/main/java/com/wireguard/android/PlaceholderFragment.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/com/wireguard/android/ProfileActivity.java b/app/src/main/java/com/wireguard/android/ProfileActivity.java index 040feb23..722f1dca 100644 --- a/app/src/main/java/com/wireguard/android/ProfileActivity.java +++ b/app/src/main/java/com/wireguard/android/ProfileActivity.java @@ -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(); - private boolean isSplitLayout; - private final List listeners = new ArrayList<>(); - private ProfileServiceInterface service; +public class ProfileActivity extends ServiceClientActivity { + public static final String KEY_PROFILE_NAME = "profile_name"; - public void addServiceConnectionListener(ServiceConnectionListener listener) { - listeners.add(listener); + // 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; + + 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 onMenuEdit(MenuItem item) { + + } + + public void onMenuSave(MenuItem item) { + + } + public void onMenuSettings(MenuItem item) { } - public void onProfileSelected(Profile profile) { - + public void onProfileSelected(String profile) { + updateLayout(profile); + currentProfile = profile; } @Override - public void onStart() { - super.onStart(); - Intent intent = new Intent(this, ProfileService.class); - bindService(intent, connection, Context.BIND_AUTO_CREATE); + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_PROFILE_NAME, currentProfile); } - @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); - } - - 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 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); } } diff --git a/app/src/main/java/com/wireguard/android/ProfileActivityFragment.java b/app/src/main/java/com/wireguard/android/ProfileActivityFragment.java deleted file mode 100644 index 9ec9f51d..00000000 --- a/app/src/main/java/com/wireguard/android/ProfileActivityFragment.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/wireguard/android/ProfileDetailFragment.java b/app/src/main/java/com/wireguard/android/ProfileDetailFragment.java new file mode 100644 index 00000000..4af7fb25 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/ProfileDetailFragment.java @@ -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 { + 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)); + } +} diff --git a/app/src/main/java/com/wireguard/android/ProfileListFragment.java b/app/src/main/java/com/wireguard/android/ProfileListFragment.java index be9b8d42..ed72b63b 100644 --- a/app/src/main/java/com/wireguard/android/ProfileListFragment.java +++ b/app/src/main/java/com/wireguard/android/ProfileListFragment.java @@ -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 { 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()) diff --git a/app/src/main/java/com/wireguard/android/ProfileService.java b/app/src/main/java/com/wireguard/android/ProfileService.java index 96371d6c..1f40a0a1 100644 --- a/app/src/main/java/com/wireguard/android/ProfileService.java +++ b/app/src/main/java/com/wireguard/android/ProfileService.java @@ -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) { diff --git a/app/src/main/java/com/wireguard/android/ProfileServiceInterface.java b/app/src/main/java/com/wireguard/android/ProfileServiceInterface.java index 54b32595..65dc27a0 100644 --- a/app/src/main/java/com/wireguard/android/ProfileServiceInterface.java +++ b/app/src/main/java/com/wireguard/android/ProfileServiceInterface.java @@ -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 diff --git a/app/src/main/java/com/wireguard/android/ServiceClientActivity.java b/app/src/main/java/com/wireguard/android/ServiceClientActivity.java new file mode 100644 index 00000000..263e012c --- /dev/null +++ b/app/src/main/java/com/wireguard/android/ServiceClientActivity.java @@ -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 extends Activity implements ServiceConnectionProvider { + private final ServiceConnectionCallbacks callbacks = new ServiceConnectionCallbacks(); + private final List> listeners = new ArrayList<>(); + private T service; + private final Class serviceClass; + + protected ServiceClientActivity(Class serviceClass) { + this.serviceClass = serviceClass; + } + + @Override + public void addServiceConnectionListener(ServiceConnectionListener 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 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 listener : listeners) + listener.onServiceConnected(service); + } + + @Override + public void onServiceDisconnected(ComponentName component) { + service = null; + for (ServiceConnectionListener listener : listeners) + listener.onServiceDisconnected(); + } + } +} diff --git a/app/src/main/java/com/wireguard/android/ServiceClientFragment.java b/app/src/main/java/com/wireguard/android/ServiceClientFragment.java new file mode 100644 index 00000000..3b209438 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/ServiceClientFragment.java @@ -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 extends Fragment implements ServiceConnectionListener { + private ServiceConnectionProvider provider; + private T service; + + protected T getService() { + return service; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + @SuppressWarnings("unchecked") + final ServiceConnectionProvider localContext = (ServiceConnectionProvider) 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; + } +} diff --git a/app/src/main/java/com/wireguard/android/ServiceConnectionListener.java b/app/src/main/java/com/wireguard/android/ServiceConnectionListener.java index 5f3c8d0a..c77fe931 100644 --- a/app/src/main/java/com/wireguard/android/ServiceConnectionListener.java +++ b/app/src/main/java/com/wireguard/android/ServiceConnectionListener.java @@ -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 { + void onServiceConnected(T service); void onServiceDisconnected(); } diff --git a/app/src/main/java/com/wireguard/android/ServiceConnectionProvider.java b/app/src/main/java/com/wireguard/android/ServiceConnectionProvider.java new file mode 100644 index 00000000..79a0efde --- /dev/null +++ b/app/src/main/java/com/wireguard/android/ServiceConnectionProvider.java @@ -0,0 +1,13 @@ +package com.wireguard.android; + +/** + * Interface for activities that provide a connection to a service. + */ + +interface ServiceConnectionProvider { + void addServiceConnectionListener(ServiceConnectionListener listener); + + T getService(); + + void removeServiceConnectionListener(ServiceConnectionListener listener); +} diff --git a/app/src/main/res/drawable/ic_action_edit.xml b/app/src/main/res/drawable/ic_action_edit.xml new file mode 100644 index 00000000..c910949f --- /dev/null +++ b/app/src/main/res/drawable/ic_action_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_save.xml b/app/src/main/res/drawable/ic_action_save.xml new file mode 100644 index 00000000..08280124 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_save.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout-land/profile_activity.xml b/app/src/main/res/layout-land/profile_activity.xml deleted file mode 100644 index acc56925..00000000 --- a/app/src/main/res/layout-land/profile_activity.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/placeholder_fragment.xml b/app/src/main/res/layout/placeholder_fragment.xml deleted file mode 100644 index 8a321c6b..00000000 --- a/app/src/main/res/layout/placeholder_fragment.xml +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/app/src/main/res/layout/profile_activity.xml b/app/src/main/res/layout/profile_activity.xml index d9b6a1f6..2645552b 100644 --- a/app/src/main/res/layout/profile_activity.xml +++ b/app/src/main/res/layout/profile_activity.xml @@ -1,5 +1,21 @@ - + android:layout_height="match_parent" + android:baselineAligned="false" + android:orientation="horizontal"> + + + + + diff --git a/app/src/main/res/layout/profile_detail_fragment.xml b/app/src/main/res/layout/profile_detail_fragment.xml new file mode 100644 index 00000000..248f11a1 --- /dev/null +++ b/app/src/main/res/layout/profile_detail_fragment.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/profile_list_fragment.xml b/app/src/main/res/layout/profile_list_fragment.xml index c0edd444..f5954092 100644 --- a/app/src/main/res/layout/profile_list_fragment.xml +++ b/app/src/main/res/layout/profile_list_fragment.xml @@ -4,6 +4,7 @@ + diff --git a/app/src/main/res/menu/profile_detail.xml b/app/src/main/res/menu/profile_detail.xml new file mode 100644 index 00000000..499adcdb --- /dev/null +++ b/app/src/main/res/menu/profile_detail.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/menu/profile_detail_edit.xml b/app/src/main/res/menu/profile_detail_edit.xml new file mode 100644 index 00000000..ad97fcbf --- /dev/null +++ b/app/src/main/res/menu/profile_detail_edit.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cbedef43..4010e776 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,9 @@ WireGuard Connected Disconnected - No profile selected. + No profile selected + Profile name + Public key + Save Settings