diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2021-01-02 12:10:20 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-02 12:10:20 +0100 |
commit | b1dc96adb3107d2ecb689c213629ee1e11f26ba8 (patch) | |
tree | 59319c2844beccecf362ed0151d50cb21a60f171 | |
parent | d1426f97740103d548db007e3ad2c918d90f8bf0 (diff) | |
parent | 34e9c318996147e266b524d5ec1d1fae4f9b17c9 (diff) | |
download | AntennaPod-b1dc96adb3107d2ecb689c213629ee1e11f26ba8.zip |
Merge pull request #4493 from ByteHamster/gpodnet-setup
Simplified gpodder login process
15 files changed, 643 insertions, 727 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fb205b1c3..10225f9a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -292,19 +292,6 @@ </activity> - <activity - android:name=".activity.gpoddernet.GpodnetAuthenticationActivity" - android:configChanges="orientation" - android:label="@string/gpodnet_auth_label"> - <intent-filter> - <action android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - <meta-data - android:name="android.support.PARENT_ACTIVITY" - android:value="de.danoeh.antennapod.activity.PreferenceActivity"/> - </activity> - <receiver android:name=".receiver.ConnectivityActionReceiver"> <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java deleted file mode 100644 index cfd6ec702..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ /dev/null @@ -1,395 +0,0 @@ -package de.danoeh.antennapod.activity.gpoddernet; - -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.ViewFlipper; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; - -/** - * Guides the user through the authentication process - * Step 1: Request username and password from user - * Step 2: Choose device from a list of available devices or create a new one - * Step 3: Choose from a list of actions - */ -public class GpodnetAuthenticationActivity extends AppCompatActivity { - private static final String TAG = "GpodnetAuthActivity"; - - private ViewFlipper viewFlipper; - - private static final int STEP_DEFAULT = -1; - private static final int STEP_LOGIN = 0; - private static final int STEP_DEVICE = 1; - private static final int STEP_FINISH = 2; - - private int currentStep = -1; - - private GpodnetService service; - private volatile String username; - private volatile String password; - private volatile GpodnetDevice selectedDevice; - - private View[] views; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.gpodnetauth_activity); - service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); - - viewFlipper = findViewById(R.id.viewflipper); - LayoutInflater inflater = (LayoutInflater) - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - views = new View[]{ - inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false) - }; - for (View view : views) { - viewFlipper.addView(view); - } - advance(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void setupLoginView(View view) { - final EditText username = view.findViewById(R.id.etxtUsername); - final EditText password = view.findViewById(R.id.etxtPassword); - final Button login = view.findViewById(R.id.butLogin); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); - - password.setOnEditorActionListener((v, actionID, event) -> - actionID == EditorInfo.IME_ACTION_GO && login.performClick()); - - login.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - final String usernameStr = username.getText().toString(); - final String passwordStr = password.getText().toString(); - - if (usernameHasUnwantedChars(usernameStr)) { - txtvError.setText(R.string.gpodnetsync_username_characters_error); - txtvError.setVisibility(View.VISIBLE); - return; - } - if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials"); - AsyncTask<GpodnetService, Void, Void> authTask = new AsyncTask<GpodnetService, Void, Void>() { - - volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - login.setEnabled(false); - progressBar.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - // hide the keyboard - InputMethodManager inputManager = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); - inputManager.hideSoftInputFromWindow(login.getWindowToken(), - InputMethodManager.HIDE_NOT_ALWAYS); - - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - login.setEnabled(true); - progressBar.setVisibility(View.GONE); - - if (exception == null) { - advance(); - } else { - txtvError.setText(exception.getCause().getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected Void doInBackground(GpodnetService... params) { - try { - params[0].authenticate(usernameStr, passwordStr); - GpodnetAuthenticationActivity.this.username = usernameStr; - GpodnetAuthenticationActivity.this.password = passwordStr; - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }; - authTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, service); - } - }); - } - - private void setupDeviceView(View view) { - final EditText deviceID = view.findViewById(R.id.etxtDeviceID); - final EditText caption = view.findViewById(R.id.etxtCaption); - final Button createNewDevice = view.findViewById(R.id.butCreateNewDevice); - final Button chooseDevice = view.findViewById(R.id.butChooseExistingDevice); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); - final Spinner spinnerDevices = view.findViewById(R.id.spinnerChooseDevice); - - - // load device list - final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<>(); - new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() { - - @Override - protected void onPreExecute() { - super.onPreExecute(); - chooseDevice.setEnabled(false); - spinnerDevices.setEnabled(false); - createNewDevice.setEnabled(false); - } - - @Override - protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) { - super.onPostExecute(gpodnetDevices); - if (gpodnetDevices != null) { - List<String> deviceNames = new ArrayList<>(); - for (GpodnetDevice device : gpodnetDevices) { - deviceNames.add(device.getCaption()); - } - spinnerDevices.setAdapter(new ArrayAdapter<>(GpodnetAuthenticationActivity.this, - android.R.layout.simple_spinner_dropdown_item, deviceNames)); - spinnerDevices.setEnabled(true); - if (!deviceNames.isEmpty()) { - chooseDevice.setEnabled(true); - } - devices.set(gpodnetDevices); - deviceID.setText(generateDeviceID(gpodnetDevices)); - createNewDevice.setEnabled(true); - } - } - - @Override - protected List<GpodnetDevice> doInBackground(GpodnetService... params) { - try { - return params[0].getDevices(); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - return null; - } - } - }.execute(service); - - - createNewDevice.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkDeviceIDText(deviceID, caption, txtvError, devices.get())) { - final String deviceStr = deviceID.getText().toString(); - final String captionStr = caption.getText().toString(); - - new AsyncTask<GpodnetService, Void, GpodnetDevice>() { - - private volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - createNewDevice.setEnabled(false); - chooseDevice.setEnabled(false); - progBarCreateDevice.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - } - - @Override - protected void onPostExecute(GpodnetDevice result) { - super.onPostExecute(result); - createNewDevice.setEnabled(true); - chooseDevice.setEnabled(true); - progBarCreateDevice.setVisibility(View.GONE); - if (exception == null) { - selectedDevice = result; - advance(); - } else { - txtvError.setText(exception.getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected GpodnetDevice doInBackground(GpodnetService... params) { - try { - params[0].configureDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE); - return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }.execute(service); - } - } - }); - - chooseDevice.setOnClickListener(v -> { - final int position = spinnerDevices.getSelectedItemPosition(); - if (position != AdapterView.INVALID_POSITION) { - selectedDevice = devices.get().get(position); - advance(); - } - }); - } - - - private String generateDeviceID(List<GpodnetDevice> gpodnetDevices) { - // devices names must be of a certain form: - // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices - // This is more restrictive than needed, but I think it makes for more readable names. - String baseId = Build.MODEL.replaceAll("\\W", ""); - String id = baseId; - int num = 0; - - while (isDeviceWithIdInList(id, gpodnetDevices)) { - id = baseId + "_" + num; - num++; - } - - return id; - } - - private boolean isDeviceWithIdInList(String id, List<GpodnetDevice> gpodnetDevices) { - if (gpodnetDevices == null) { - return false; - } - for (GpodnetDevice device : gpodnetDevices) { - if (device.getId().equals(id)) { - return true; - } - } - return false; - } - - private boolean checkDeviceIDText(EditText deviceID, EditText caption, TextView txtvError, List<GpodnetDevice> devices) { - String text = deviceID.getText().toString(); - if (text.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else if (caption.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_caption_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else { - if (devices != null) { - if (isDeviceWithIdInList(text, devices)) { - txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed); - txtvError.setVisibility(View.VISIBLE); - return false; - } - txtvError.setVisibility(View.GONE); - return true; - } - return true; - } - - } - - private void setupFinishView(View view) { - final Button sync = view.findViewById(R.id.butSyncNow); - final Button back = view.findViewById(R.id.butGoMainscreen); - - sync.setOnClickListener(v -> { - finish(); - SyncService.sync(getApplicationContext()); - }); - back.setOnClickListener(v -> { - Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - }); - } - - private void writeLoginCredentials() { - if (BuildConfig.DEBUG) Log.d(TAG, "Writing login credentials"); - GpodnetPreferences.setUsername(username); - GpodnetPreferences.setPassword(password); - GpodnetPreferences.setDeviceID(selectedDevice.getId()); - } - - private void advance() { - if (currentStep < STEP_FINISH) { - - View view = views[currentStep + 1]; - if (currentStep == STEP_DEFAULT) { - setupLoginView(view); - } else if (currentStep == STEP_LOGIN) { - if (username == null || password == null) { - throw new IllegalStateException("Username and password must not be null here"); - } else { - setupDeviceView(view); - } - } else if (currentStep == STEP_DEVICE) { - if (selectedDevice == null) { - throw new IllegalStateException("Device must not be null here"); - } else { - writeLoginCredentials(); - setupFinishView(view); - } - } - if (currentStep != STEP_DEFAULT) { - viewFlipper.showNext(); - } - currentStep++; - } else { - finish(); - } - } - - private boolean usernameHasUnwantedChars(String username) { - Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); - Matcher containsUnwantedChars = special.matcher(username); - return containsUnwantedChars.find(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java deleted file mode 100644 index 8119dffcb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import androidx.appcompat.app.AlertDialog; -import android.text.Editable; -import android.text.InputType; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.LinearLayout; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; - -/** - * Creates a dialog that lets the user change the hostname for the gpodder.net service. - */ -public class GpodnetSetHostnameDialog { - - private GpodnetSetHostnameDialog(){} - - private static final String TAG = "GpodnetSetHostnameDialog"; - - public static AlertDialog createDialog(final Context context) { - AlertDialog.Builder dialog = new AlertDialog.Builder(context); - final EditText et = new EditText(context); - et.setText(GpodnetPreferences.getHostname()); - et.setInputType(InputType.TYPE_TEXT_VARIATION_URI); - dialog.setTitle(R.string.pref_gpodnet_sethostname_title) - .setView(setupContentView(context, et)) - .setPositiveButton(R.string.confirm_label, (dialog1, which) -> { - final Editable e = et.getText(); - if (e != null) { - GpodnetPreferences.setHostname(e.toString()); - } - dialog1.dismiss(); - }) - .setNegativeButton(R.string.cancel_label, (dialog1, which) -> dialog1.cancel()) - .setNeutralButton(R.string.pref_gpodnet_sethostname_use_default_host, (dialog1, which) -> { - GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST); - dialog1.dismiss(); - }) - .setCancelable(true); - return dialog.show(); - } - - private static View setupContentView(Context context, EditText et) { - LinearLayout ll = new LinearLayout(context); - ll.addView(et); - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) et.getLayoutParams(); - if (params != null) { - params.setMargins(8, 8, 8, 8); - params.width = ViewGroup.LayoutParams.MATCH_PARENT; - params.height = ViewGroup.LayoutParams.MATCH_PARENT; - } - return ll; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java new file mode 100644 index 000000000..187e8480b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java @@ -0,0 +1,302 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Paint; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.ViewFlipper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.textfield.TextInputLayout; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.IntentUtils; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Guides the user through the authentication process. + */ +public class GpodderAuthenticationFragment extends DialogFragment { + public static final String TAG = "GpodnetAuthActivity"; + + private ViewFlipper viewFlipper; + + private static final int STEP_DEFAULT = -1; + private static final int STEP_HOSTNAME = 0; + private static final int STEP_LOGIN = 1; + private static final int STEP_DEVICE = 2; + private static final int STEP_FINISH = 3; + + private int currentStep = -1; + + private GpodnetService service; + private volatile String username; + private volatile String password; + private volatile GpodnetDevice selectedDevice; + private List<GpodnetDevice> devices; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder dialog = new AlertDialog.Builder(getContext()); + dialog.setTitle(GpodnetService.DEFAULT_BASE_HOST); + dialog.setNegativeButton(R.string.cancel_label, null); + dialog.setCancelable(false); + this.setCancelable(false); + + View root = View.inflate(getContext(), R.layout.gpodnetauth_dialog, null); + viewFlipper = root.findViewById(R.id.viewflipper); + advance(); + dialog.setView(root); + + return dialog.create(); + } + + private void setupHostView(View view) { + final Button selectHost = view.findViewById(R.id.chooseHostButton); + final RadioGroup serverRadioGroup = view.findViewById(R.id.serverRadioGroup); + final EditText serverUrlText = view.findViewById(R.id.serverUrlText); + if (!GpodnetService.DEFAULT_BASE_HOST.equals(GpodnetPreferences.getHostname())) { + serverUrlText.setText(GpodnetPreferences.getHostname()); + } + final TextInputLayout serverUrlTextInput = view.findViewById(R.id.serverUrlTextInput); + serverRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { + serverUrlTextInput.setVisibility(checkedId == R.id.customServerRadio ? View.VISIBLE : View.GONE); + }); + selectHost.setOnClickListener(v -> { + if (serverRadioGroup.getCheckedRadioButtonId() == R.id.customServerRadio) { + GpodnetPreferences.setHostname(serverUrlText.getText().toString()); + } else { + GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST); + } + service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); + getDialog().setTitle(GpodnetPreferences.getHostname()); + advance(); + }); + } + + private void setupLoginView(View view) { + final EditText username = view.findViewById(R.id.etxtUsername); + final EditText password = view.findViewById(R.id.etxtPassword); + final Button login = view.findViewById(R.id.butLogin); + final TextView txtvError = view.findViewById(R.id.credentialsError); + final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); + final TextView createAccount = view.findViewById(R.id.createAccountButton); + + createAccount.setPaintFlags(createAccount.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + createAccount.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), "https://gpodder.net/register/")); + + password.setOnEditorActionListener((v, actionID, event) -> + actionID == EditorInfo.IME_ACTION_GO && login.performClick()); + + login.setOnClickListener(v -> { + final String usernameStr = username.getText().toString(); + final String passwordStr = password.getText().toString(); + + if (usernameHasUnwantedChars(usernameStr)) { + txtvError.setText(R.string.gpodnetsync_username_characters_error); + txtvError.setVisibility(View.VISIBLE); + return; + } + + login.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + InputMethodManager inputManager = (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + + Completable.fromAction(() -> { + service.authenticate(usernameStr, passwordStr); + devices = service.getDevices(); + GpodderAuthenticationFragment.this.username = usernameStr; + GpodderAuthenticationFragment.this.password = passwordStr; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + advance(); + }, error -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.getCause().getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + + }); + } + + private void setupDeviceView(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final LinearLayout devicesContainer = view.findViewById(R.id.devicesContainer); + deviceName.setText(generateDeviceName()); + + MaterialButton createDeviceButton = view.findViewById(R.id.createDeviceButton); + createDeviceButton.setOnClickListener(v -> createDevice(view)); + + for (GpodnetDevice device : devices) { + View row = View.inflate(getContext(), R.layout.gpodnetauth_device_row, null); + Button selectDeviceButton = row.findViewById(R.id.selectDeviceButton); + selectDeviceButton.setOnClickListener(v -> { + selectedDevice = device; + advance(); + }); + selectDeviceButton.setText(device.getCaption()); + devicesContainer.addView(row); + } + } + + private void createDevice(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final TextView txtvError = view.findViewById(R.id.deviceSelectError); + final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); + + String deviceNameStr = deviceName.getText().toString(); + if (isDeviceInList(deviceNameStr)) { + return; + } + progBarCreateDevice.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + deviceName.setEnabled(false); + + Observable.fromCallable(() -> { + String deviceId = generateDeviceId(deviceNameStr); + service.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE); + return new GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(device -> { + progBarCreateDevice.setVisibility(View.GONE); + selectedDevice = device; + advance(); + }, error -> { + deviceName.setEnabled(true); + progBarCreateDevice.setVisibility(View.GONE); + txtvError.setText(error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + } + + private String generateDeviceName() { + String baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL); + String name = baseName; + int num = 1; + while (isDeviceInList(name)) { + name = baseName + " (" + num + ")"; + num++; + } + return name; + } + + private String generateDeviceId(String name) { + // devices names must be of a certain form: + // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices + return FileNameGenerator.generateFileName(name).replaceAll("\\W", "_").toLowerCase(Locale.US); + } + + private boolean isDeviceInList(String name) { + if (devices == null) { + return false; + } + String id = generateDeviceId(name); + for (GpodnetDevice device : devices) { + if (device.getId().equals(id) || device.getCaption().equals(name)) { + return true; + } + } + return false; + } + + private GpodnetDevice findDevice(String id) { + if (devices == null) { + return null; + } + for (GpodnetDevice device : devices) { + if (device.getId().equals(id)) { + return device; + } + } + return null; + } + + private void setupFinishView(View view) { + final Button sync = view.findViewById(R.id.butSyncNow); + + sync.setOnClickListener(v -> { + dismiss(); + SyncService.sync(getContext()); + }); + } + + private void writeLoginCredentials() { + GpodnetPreferences.setUsername(username); + GpodnetPreferences.setPassword(password); + GpodnetPreferences.setDeviceID(selectedDevice.getId()); + } + + private void advance() { + if (currentStep < STEP_FINISH) { + + View view = viewFlipper.getChildAt(currentStep + 1); + if (currentStep == STEP_DEFAULT) { + setupHostView(view); + } else if (currentStep == STEP_HOSTNAME) { + setupLoginView(view); + } else if (currentStep == STEP_LOGIN) { + if (username == null || password == null) { + throw new IllegalStateException("Username and password must not be null here"); + } else { + setupDeviceView(view); + } + } else if (currentStep == STEP_DEVICE) { + if (selectedDevice == null) { + throw new IllegalStateException("Device must not be null here"); + } else { + writeLoginCredentials(); + setupFinishView(view); + } + } + if (currentStep != STEP_DEFAULT) { + viewFlipper.showNext(); + } + currentStep++; + } else { + dismiss(); + } + } + + private boolean usernameHasUnwantedChars(String username) { + Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); + Matcher containsUnwantedChars = special.matcher(username); + return containsUnwantedChars.find(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java index eb23a5eb1..4fb734e17 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java @@ -14,19 +14,16 @@ import de.danoeh.antennapod.core.event.SyncServiceEvent; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.sync.SyncService; import de.danoeh.antennapod.dialog.AuthenticationDialog; -import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; - public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync"; private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync"; private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; - private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -51,6 +48,7 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void syncStatusChanged(SyncServiceEvent event) { + updateGpodnetPreferenceScreen(); if (!GpodnetPreferences.loggedIn()) { return; } @@ -66,6 +64,10 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private void setupGpodderScreen() { final Activity activity = getActivity(); + findPreference(PREF_GPODNET_LOGIN).setOnPreferenceClickListener(preference -> { + new GpodderAuthenticationFragment().show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG); + return true; + }); findPreference(PREF_GPODNET_SETLOGIN_INFORMATION) .setOnPreferenceClickListener(preference -> { AuthenticationDialog dialog = new AuthenticationDialog(activity, @@ -94,11 +96,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { updateGpodnetPreferenceScreen(); return true; }); - findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(preference -> { - GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener( - dialog -> updateGpodnetPreferenceScreen()); - return true; - }); } private void updateGpodnetPreferenceScreen() { @@ -119,7 +116,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { } else { findPreference(PREF_GPODNET_LOGOUT).setSummary(null); } - findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname()); } private void updateLastGpodnetSyncReport(boolean successful, long lastTime) { diff --git a/app/src/main/res/layout/gpodnetauth_activity.xml b/app/src/main/res/layout/gpodnetauth_activity.xml deleted file mode 100644 index c096c20cf..000000000 --- a/app/src/main/res/layout/gpodnetauth_activity.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> -<ViewFlipper - android:id="@+id/viewflipper" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> -</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_credentials.xml b/app/src/main/res/layout/gpodnetauth_credentials.xml index 895b0999c..291b98da3 100644 --- a/app/src/main/res/layout/gpodnetauth_credentials.xml +++ b/app/src/main/res/layout/gpodnetauth_credentials.xml @@ -1,96 +1,97 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> - - <ImageView - android:id="@id/icon" - android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/gpodder_icon" /> + android:orientation="vertical"> - <TextView - android:id="@id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_login_descr" - android:layout_below="@id/icon" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary"/> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginBottom="8dp"> - <EditText - android:id="@+id/etxtUsername" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/username_label" - android:layout_below="@id/txtvDescription" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true" - android:maxLines="1" - android:inputType="text" - android:imeOptions="actionNext|flagNoFullscreen" - android:nextFocusForward="@id/etxtPassword"/> + <ImageView + android:layout_width="64dp" + android:layout_height="64dp" + android:src="@drawable/gpodder_icon"/> - <EditText - android:id="@+id/etxtPassword" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/password_label" - android:layout_below="@id/etxtUsername" - android:inputType="textPassword" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true" - android:imeOptions="actionGo|flagNoFullscreen" - android:imeActionLabel="@string/gpodnetauth_login_butLabel"/> + <TextView + android:id="@+id/createAccountButton" + android:layout_width="0dp" + android:textAlignment="textEnd" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:textColor="?colorAccent" + android:layout_weight="1" + android:layout_gravity="center_vertical|end" + android:text="@string/create_account"/> + </LinearLayout> - <Button - android:id="@+id/butLogin" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/etxtPassword" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:text="@string/gpodnetauth_login_butLabel"/> + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <TextView - android:id="@+id/txtvError" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_below="@id/etxtPassword" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:layout_toLeftOf="@id/butLogin" - android:layout_toStartOf="@id/butLogin" - android:textColor="@color/download_failed_red" - android:textSize="@dimen/text_size_small" - android:maxLines="2" - android:ellipsize="end" - android:gravity="center" - android:layout_margin="16dp" - tools:text="Error message" - tools:background="@android:color/holo_green_dark" /> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/etxtUsername" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/username_label" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen"/> - <ProgressBar - android:id="@+id/progBarLogin" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:visibility="gone" - android:layout_alignTop="@+id/butLogin" - android:layout_toLeftOf="@+id/butLogin" - android:layout_toStartOf="@+id/butLogin"/> + </com.google.android.material.textfield.TextInputLayout> - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary" - android:layout_marginTop="16dp" - android:text="@string/gpodnetauth_login_register" - android:autoLink="web" - android:layout_below="@id/butLogin"/> -</RelativeLayout>
\ No newline at end of file + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/etxtPassword" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password_label" + android:inputType="textPassword" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen" + android:imeActionLabel="@string/gpodnetauth_login_butLabel"/> + + </com.google.android.material.textfield.TextInputLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="end|center_vertical"> + + <TextView + android:id="@+id/credentialsError" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:textColor="@color/download_failed_red" + android:textSize="@dimen/text_size_small" + android:maxLines="2" + android:ellipsize="end" + android:gravity="center" + tools:text="Error message" + tools:background="@android:color/holo_green_dark"/> + + <ProgressBar + android:id="@+id/progBarLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_gravity="right"/> + + <Button + android:id="@+id/butLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_login_butLabel"/> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_device.xml b/app/src/main/res/layout/gpodnetauth_device.xml index 7837121e1..656ba0889 100644 --- a/app/src/main/res/layout/gpodnetauth_device.xml +++ b/app/src/main/res/layout/gpodnetauth_device.xml @@ -1,114 +1,61 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> - - <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_title" - android:layout_alignParentTop="true" - android:layout_marginBottom="16dp" - style="@style/AntennaPod.TextView.Heading"/> + android:orientation="vertical"> - <TextView - android:id="@+id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_descr" - android:layout_below="@id/txtvTitle" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary"/> - - <EditText - android:id="@+id/etxtCaption" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/gpodnetauth_device_caption" - android:layout_below="@id/txtvDescription" - android:imeOptions="flagNoFullscreen"/> + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <TextView - android:id="@+id/txtvDeviceID" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_deviceID" - android:textSize="@dimen/text_size_medium" - android:layout_below="@id/etxtCaption"/> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/deviceName" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_device_name" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen"/> - <EditText - android:id="@+id/etxtDeviceID" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvDeviceID" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:imeOptions="flagNoFullscreen"/> + </com.google.android.material.textfield.TextInputLayout> <Button - android:id="@+id/butCreateNewDevice" - android:layout_width="wrap_content" + android:id="@+id/createDeviceButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|end" + android:text="@string/gpodnetauth_create_device"/> + + <TextView + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:layout_below="@id/etxtDeviceID" - android:text="@string/gpodnetauth_device_butCreateNewDevice"/> + style="@style/AntennaPod.TextView.Heading" + android:layout_marginTop="16dp" + android:text="@string/gpodnetauth_existing_devices"/> <TextView - android:id="@+id/txtvError" - android:layout_width="0dp" + android:id="@+id/deviceSelectError" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:layout_below="@id/etxtDeviceID" - android:layout_toLeftOf="@id/butCreateNewDevice" - android:layout_toStartOf="@id/butCreateNewDevice" android:textColor="@color/download_failed_red" android:textSize="@dimen/text_size_small" + android:visibility="gone" tools:text="Error message" tools:background="@android:color/holo_green_dark" /> - <ProgressBar - android:id="@+id/progbarCreateDevice" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignTop="@id/butCreateNewDevice" - android:layout_toLeftOf="@id/butCreateNewDevice" - android:layout_toStartOf="@id/butCreateNewDevice" - android:textColor="@color/download_failed_red" - android:textSize="@dimen/text_size_medium" - android:visibility="gone" - /> - - <TextView - android:id="@+id/txtvChooseExistingDevice" + <LinearLayout + android:id="@+id/devicesContainer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_chooseExistingDevice" - android:layout_below="@id/butCreateNewDevice" - android:textColor="?android:attr/textColorPrimary" - android:textSize="@dimen/text_size_medium" - android:layout_marginTop="32dp"/> + android:orientation="vertical" /> - <Button - android:id="@+id/butChooseExistingDevice" + <ProgressBar + android:id="@+id/progbarCreateDevice" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_butChoose" - android:layout_below="@+id/spinnerChooseDevice" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true"/> - - <Spinner - android:id="@+id/spinnerChooseDevice" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvChooseExistingDevice" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true"/> + android:textColor="@color/download_failed_red" + android:visibility="gone" /> -</RelativeLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_device_row.xml b/app/src/main/res/layout/gpodnetauth_device_row.xml new file mode 100644 index 000000000..d39c00571 --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_device_row.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp"> + + <Button + android:id="@+id/selectDeviceButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?attr/materialButtonOutlinedStyle" /> +</FrameLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_dialog.xml b/app/src/main/res/layout/gpodnetauth_dialog.xml new file mode 100644 index 000000000..a70b76a49 --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_dialog.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:clipToPadding="false"> + <ViewFlipper + android:id="@+id/viewflipper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inAnimation="@anim/slide_right_in" + android:outAnimation="@anim/slide_left_out"> + + <include layout="@layout/gpodnetauth_host" /> + <include layout="@layout/gpodnetauth_credentials" /> + <include layout="@layout/gpodnetauth_device" /> + <include layout="@layout/gpodnetauth_finish" /> + + </ViewFlipper> +</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_finish.xml b/app/src/main/res/layout/gpodnetauth_finish.xml index fdaa0d5d0..f0bcfd4dc 100644 --- a/app/src/main/res/layout/gpodnetauth_finish.xml +++ b/app/src/main/res/layout/gpodnetauth_finish.xml @@ -1,46 +1,28 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> + android:layout_height="wrap_content" + android:orientation="vertical"> <ImageView android:id="@id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="64dp" + android:layout_height="64dp" android:src="@drawable/gpodder_icon" /> <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/icon" - android:text="@string/gpodnetauth_finish_title" - style="@style/AntennaPod.TextView.Heading"/> - - <TextView android:id="@+id/txtvDescription" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/gpodnetauth_finish_descr" - android:layout_below="@id/txtvTitle" - android:textSize="@dimen/text_size_medium" android:textColor="?android:attr/textColorPrimary" /> <Button android:id="@+id/butSyncNow" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/txtvDescription" - android:layout_marginTop="16dp" + android:layout_marginTop="8dp" android:text="@string/gpodnetauth_finish_butsyncnow"/> - <Button - android:id="@+id/butGoMainscreen" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/butSyncNow" - android:text="@string/gpodnetauth_finish_butgomainscreen"/> - -</RelativeLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_host.xml b/app/src/main/res/layout/gpodnetauth_host.xml new file mode 100644 index 000000000..52c5fdb5d --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_host.xml @@ -0,0 +1,50 @@ +<?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="wrap_content" + android:orientation="vertical"> + + <RadioGroup + android:id="@+id/serverRadioGroup" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <RadioButton + android:id="@+id/officialServerRadio" + android:text="@string/gpodnetauth_server_official" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:checked="true"/> + <RadioButton + android:id="@+id/customServerRadio" + android:text="@string/gpodnetauth_server_custom" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + </RadioGroup> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/serverUrlTextInput" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/serverUrlText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_host" + android:inputType="textNoSuggestions" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen" /> + + </com.google.android.material.textfield.TextInputLayout> + + <Button + android:id="@+id/chooseHostButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|end" + android:text="@string/gpodnetauth_select_server"/> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/xml/preferences_gpodder.xml b/app/src/main/res/xml/preferences_gpodder.xml index 7bddbf245..a210b8e11 100644 --- a/app/src/main/res/xml/preferences_gpodder.xml +++ b/app/src/main/res/xml/preferences_gpodder.xml @@ -1,13 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> - - <PreferenceScreen + <Preference + android:key="pref_gpodnet_description" + android:icon="@drawable/gpodder_icon" + android:summary="@string/gpodnet_description"/> + <Preference android:key="pref_gpodnet_authenticate" android:title="@string/pref_gpodnet_authenticate_title" - android:summary="@string/pref_gpodnet_authenticate_sum"> - <intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/> - </PreferenceScreen> + android:summary="@string/pref_gpodnet_authenticate_sum"/> <Preference android:key="pref_gpodnet_setlogin_information" android:title="@string/pref_gpodnet_setlogin_information_title" @@ -23,8 +24,5 @@ <Preference android:key="pref_gpodnet_logout" android:title="@string/pref_gpodnet_logout_title"/> - <Preference - android:key="pref_gpodnet_hostname" - android:title="@string/pref_gpodnet_sethostname_title"/> </PreferenceScreen> diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java index 62c8ce5f3..75ac42b31 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.sync.gpoddernet; import android.util.Log; import androidx.annotation.NonNull; +import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; import de.danoeh.antennapod.core.sync.model.EpisodeAction; @@ -36,6 +37,7 @@ import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -47,6 +49,7 @@ public class GpodnetService implements ISyncService { public static final String TAG = "GpodnetService"; public static final String DEFAULT_BASE_HOST = "gpodder.net"; private static final String BASE_SCHEME = "https"; + private static final int PORT = 443; private static final int UPLOAD_BULK_SIZE = 30; private static final MediaType TEXT = MediaType.parse("plain/text; charset=utf-8"); private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); @@ -71,7 +74,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetTag> getTopTags(int count) throws GpodnetServiceException { URL url; try { - url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/api/2/tags/%d.json", count), null).toURL(); + url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/api/2/tags/%d.json", count), null, null).toURL(); } catch (MalformedURLException | URISyntaxException e) { e.printStackTrace(); throw new GpodnetServiceException(e); @@ -104,8 +108,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag, int count) throws GpodnetServiceException { try { - URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, - "/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/api/2/tag/%s/%d.json", tag.getTag(), count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -130,7 +134,8 @@ public class GpodnetService implements ISyncService { } try { - URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/toplist/%d.json", count), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/toplist/%d.json", count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -161,8 +166,8 @@ public class GpodnetService implements ISyncService { } try { - URL url = new URI(BASE_SCHEME, baseHost, - String.format(Locale.US, "/suggestions/%d.json", count), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/suggestions/%d.json", count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -187,7 +192,7 @@ public class GpodnetService implements ISyncService { .format(Locale.US, "q=%s&scale_logo=%d", query, scaledLogoSize) : String .format("q=%s", query); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, "/search.json", + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, "/search.json", parameters, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -214,7 +219,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetDevice> getDevices() throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/devices/%s.json", username), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/devices/%s.json", username), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); JSONArray devicesArray = new JSONArray(response); @@ -226,6 +232,45 @@ public class GpodnetService implements ISyncService { } /** + * Returns synchronization status of devices. + * <p/> + * This method requires authentication. + * + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public List<List<String>> getSynchronizedDevices() throws GpodnetServiceException { + requireLoggedIn(); + try { + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/sync-devices/%s.json", username), null, null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + JSONObject syncStatus = new JSONObject(response); + List<List<String>> result = new ArrayList<>(); + + JSONArray synchronizedDevices = syncStatus.getJSONArray("synchronized"); + for (int i = 0; i < synchronizedDevices.length(); i++) { + JSONArray groupDevices = synchronizedDevices.getJSONArray(i); + List<String> group = new ArrayList<>(); + for (int j = 0; j < groupDevices.length(); j++) { + group.add(groupDevices.getString(j)); + } + result.add(group); + } + + JSONArray notSynchronizedDevices = syncStatus.getJSONArray("not-synchronized"); + for (int i = 0; i < notSynchronizedDevices.length(); i++) { + result.add(Collections.singletonList(notSynchronizedDevices.getString(i))); + } + + return result; + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** * Configures the device of a given user. * <p/> * This method requires authentication. @@ -237,8 +282,8 @@ public class GpodnetService implements ISyncService { throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/devices/%s/%s.json", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/devices/%s/%s.json", username, deviceId), null, null).toURL(); String content; if (caption != null || type != null) { JSONObject jsonContent = new JSONObject(); @@ -262,6 +307,39 @@ public class GpodnetService implements ISyncService { } /** + * Links devices for synchronization. + * <p/> + * This method requires authentication. + * + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public void linkDevices(@NonNull List<String> deviceIds) throws GpodnetServiceException { + requireLoggedIn(); + try { + final URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/sync-devices/%s.json", username), null, null).toURL(); + JSONObject jsonContent = new JSONObject(); + JSONArray group = new JSONArray(); + for (String deviceId : deviceIds) { + group.put(deviceId); + } + + JSONArray synchronizedGroups = new JSONArray(); + synchronizedGroups.put(group); + jsonContent.put("synchronize", synchronizedGroups); + jsonContent.put("stop-synchronize", new JSONArray()); + + Log.d("aaaa", jsonContent.toString()); + RequestBody body = RequestBody.create(JSON, jsonContent.toString()); + Request.Builder request = new Request.Builder().post(body).url(url); + executeRequest(request); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** * Returns the subscriptions of a specific device. * <p/> * This method requires authentication. @@ -273,8 +351,8 @@ public class GpodnetService implements ISyncService { public String getSubscriptionsOfDevice(@NonNull String deviceId) throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/subscriptions/%s/%s.opml", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/subscriptions/%s/%s.opml", username, deviceId), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); return executeRequest(request); } catch (MalformedURLException | URISyntaxException e) { @@ -295,7 +373,8 @@ public class GpodnetService implements ISyncService { public String getSubscriptionsOfUser() throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format("/subscriptions/%s.opml", username), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/subscriptions/%s.opml", username), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); return executeRequest(request); } catch (MalformedURLException | URISyntaxException e) { @@ -319,8 +398,8 @@ public class GpodnetService implements ISyncService { throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/subscriptions/%s/%s.txt", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/subscriptions/%s/%s.txt", username, deviceId), null, null).toURL(); StringBuilder builder = new StringBuilder(); for (String s : subscriptions) { builder.append(s); @@ -353,8 +432,8 @@ public class GpodnetService implements ISyncService { @NonNull Collection<String> removed) throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/subscriptions/%s/%s.json", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/subscriptions/%s/%s.json", username, deviceId), null, null).toURL(); final JSONObject requestObject = new JSONObject(); requestObject.put("add", new JSONArray(added)); @@ -389,8 +468,7 @@ public class GpodnetService implements ISyncService { String params = String.format(Locale.US, "since=%d", timestamp); String path = String.format("/api/2/subscriptions/%s/%s.json", username, deviceId); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, - null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, path, params, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -432,8 +510,8 @@ public class GpodnetService implements ISyncService { throws SyncServiceException { try { Log.d(TAG, "Uploading partial actions " + from + " to " + to + " of " + episodeActions.size()); - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/episodes/%s.json", username), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/episodes/%s.json", username), null, null).toURL(); final JSONArray list = new JSONArray(); for (int i = from; i < to; i++) { @@ -471,7 +549,7 @@ public class GpodnetService implements ISyncService { String params = String.format(Locale.US, "since=%d", timestamp); String path = String.format("/api/2/episodes/%s.json", username); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, path, params, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -497,7 +575,8 @@ public class GpodnetService implements ISyncService { public void authenticate(@NonNull String username, @NonNull String password) throws GpodnetServiceException { URL url; try { - url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/auth/%s/login.json", username), null).toURL(); + url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/auth/%s/login.json", username), null, null).toURL(); } catch (MalformedURLException | URISyntaxException e) { e.printStackTrace(); throw new GpodnetServiceException(e); @@ -567,6 +646,13 @@ public class GpodnetService implements ISyncService { if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { throw new GpodnetServiceAuthenticationException("Wrong username or password"); } else { + if (BuildConfig.DEBUG) { + try { + Log.d(TAG, response.body().string()); + } catch (IOException e) { + e.printStackTrace(); + } + } throw new GpodnetServiceBadStatusCodeException("Bad response code: " + responseCode, responseCode); } } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index d32f751f4..dca326619 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -468,8 +468,6 @@ <string name="pref_fast_forward_sum">Customize the number of seconds to jump forward when the fast forward button is clicked</string> <string name="pref_rewind">Rewind Skip Time</string> <string name="pref_rewind_sum">Customize the number of seconds to jump backwards when the rewind button is clicked</string> - <string name="pref_gpodnet_sethostname_title">Set hostname</string> - <string name="pref_gpodnet_sethostname_use_default_host">Use default host</string> <string name="pref_expandNotify_title">High Notification priority</string> <string name="pref_expandNotify_sum">This usually expands the notification to show playback buttons.</string> <string name="pref_persistNotify_title">Persistent Playback Controls</string> @@ -631,23 +629,23 @@ <string name="gpodnet_suggestions_header">SUGGESTIONS</string> <string name="gpodnet_search_hint">Search gpodder.net</string> <string name="gpodnetauth_login_title">Login</string> - <string name="gpodnetauth_login_descr">Welcome to the gpodder.net login process. First, type in your login information:</string> <string name="gpodnetauth_login_butLabel">Login</string> - <string name="gpodnetauth_login_register">If you do not have an account yet, you can create one here:\nhttps://gpodder.net/register/</string> + <string name="create_account">Create account</string> <string name="username_label">Username</string> <string name="password_label">Password</string> - <string name="gpodnetauth_device_title">Device Selection</string> + <string name="gpodnet_description">Gpodder.net is an open-source podcast synchronization service that is independent of the AntennaPod project.</string> + <string name="gpodnetauth_server_official">Official gpodder.net server</string> + <string name="gpodnetauth_server_custom">Custom server</string> + <string name="gpodnetauth_host">Hostname</string> + <string name="gpodnetauth_select_server">Select server</string> <string name="gpodnetauth_device_descr">Create a new device to use for your gpodder.net account or choose an existing one:</string> - <string name="gpodnetauth_device_deviceID">Device ID:\u0020</string> - <string name="gpodnetauth_device_caption">Caption</string> - <string name="gpodnetauth_device_butCreateNewDevice">Create new device</string> - <string name="gpodnetauth_device_chooseExistingDevice">Choose existing device:</string> - <string name="gpodnetauth_device_errorEmpty">Device ID must not be empty</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Device ID already in use</string> + <string name="gpodnetauth_device_name">Device name</string> + <string name="gpodnetauth_device_name_default">AntennaPod on %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Caption must not be empty</string> + <string name="gpodnetauth_existing_devices">Existing devices</string> + <string name="gpodnetauth_create_device">Create device</string> <string name="gpodnetauth_device_butChoose">Choose</string> - <string name="gpodnetauth_finish_title">Login successful!</string> <string name="gpodnetauth_finish_descr">Congratulations! Your gpodder.net account is now linked with your device. AntennaPod will from now on automatically sync subscriptions on your device with your gpodder.net account.</string> <string name="gpodnetauth_finish_butsyncnow">Start sync now</string> <string name="gpodnetauth_finish_butgomainscreen">Go to main screen</string> |