Merge pull request #251 from kosharskiy/feature/add_edit_server_in_server_book

implemented edit server in server book
This commit is contained in:
Stefan Schüller 2021-01-16 21:12:00 +01:00 committed by GitHub
commit 4a133ed381
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 704 additions and 790 deletions

View File

@ -1,4 +1,9 @@
apply plugin: 'com.android.application'
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
}
ext.readProperty = { paramName -> readPropertyWithDefault(paramName, null) }
ext.readPropertyWithDefault = { paramName, defaultValue ->
@ -80,20 +85,29 @@ android {
applicationVariants.all { variant ->
variant.resValue "string", "versionName", variant.versionName
}
buildFeatures{
viewBinding = true
}
}
def room_version = "2.2.6"
def lifecycleVersion = '2.2.0'
def exoplayer = '2.12.3'
def fragment_version = "1.2.5"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Layouts and design
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'de.hdodenhof:circleimageview:3.0.0'
@ -134,18 +148,28 @@ dependencies {
// database lib
implementation "androidx.room:room-runtime:$room_version"
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
annotationProcessor "androidx.room:room-compiler:$room_version"
androidTestImplementation "androidx.room:room-testing:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
annotationProcessor "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
kapt "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.preference:preference-ktx:1.1.1'
// testing
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation "androidx.room:room-testing:$room_version"
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}

View File

@ -1,190 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity;
import android.app.AlertDialog;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.widget.EditText;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.schueller.peertube.R;
import net.schueller.peertube.adapter.ServerListAdapter;
import net.schueller.peertube.database.Server;
import net.schueller.peertube.database.ServerViewModel;
import net.schueller.peertube.fragment.AddServerFragment;
import java.util.Objects;
public class ServerAddressBookActivity extends CommonActivity implements AddServerFragment.OnFragmentInteractionListener {
private String TAG = "ServerAddressBookActivity";
public static final String EXTRA_REPLY = "net.schueller.peertube.room.REPLY";
private ServerViewModel mServerViewModel;
private AddServerFragment addServerFragment;
private FloatingActionButton floatingActionButton;
private FragmentManager fragmentManager;
@Override
public boolean onSupportNavigateUp() {
finish(); // close this activity as oppose to navigating up
return false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_server_address_book);
// Attaching the layout to the toolbar object
Toolbar toolbar = findViewById(R.id.tool_bar_server_address_book);
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(toolbar);
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_baseline_close_24);
mServerViewModel = new ViewModelProvider(this).get(ServerViewModel.class);
showServers();
floatingActionButton = findViewById(R.id.add_server);
floatingActionButton.setOnClickListener(view -> {
Log.d(TAG, "Click");
fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
addServerFragment = new AddServerFragment();
fragmentTransaction.replace(R.id.server_book, addServerFragment);
fragmentTransaction.commit();
floatingActionButton.hide();
});
}
@Override
public void onFragmentInteraction(Uri uri) {
}
@Override
public void onPointerCaptureChanged(boolean hasCapture) {
}
public void showServers()
{
RecyclerView recyclerView = findViewById(R.id.server_list_recyclerview);
final ServerListAdapter adapter = new ServerListAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// Delete items on swipe
ItemTouchHelper helper = new ItemTouchHelper(
new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder,
int direction) {
new AlertDialog.Builder(ServerAddressBookActivity.this)
.setTitle(getString(R.string.server_book_del_alert_title))
.setMessage(getString(R.string.server_book_del_alert_msg))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
int position = viewHolder.getAdapterPosition();
Server server = adapter.getServerAtPosition(position);
// Toast.makeText(ServerAddressBookActivity.this, "Deleting " +
// server.getServerName(), Toast.LENGTH_LONG).show();
// Delete the server
mServerViewModel.delete(server);
})
.setNegativeButton(android.R.string.no, (dialog, which) -> {
adapter.notifyItemChanged(viewHolder.getAdapterPosition());
})
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
}
});
helper.attachToRecyclerView(recyclerView);
// Update the cached copy of the words in the adapter.
mServerViewModel.getAllServers().observe(this, adapter::setServers);
}
public void addServer(View view)
{
Log.d(TAG, "addServer");
EditText serverLabel = view.findViewById(R.id.serverLabel);
EditText serverUrl = view.findViewById(R.id.serverUrl);
EditText serverUsername = view.findViewById(R.id.serverUsername);
EditText serverPassword = view.findViewById(R.id.serverPassword);
Server server = new Server(serverLabel.getText().toString());
server.setServerHost(serverUrl.getText().toString());
server.setUsername(serverUsername.getText().toString());
server.setPassword(serverPassword.getText().toString());
mServerViewModel.insert(server);
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.remove(addServerFragment);
fragmentTransaction.commit();
floatingActionButton.show();
}
public void testServer()
{
}
}

View File

@ -0,0 +1,168 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity
import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.viewModels
import androidx.fragment.app.FragmentManager
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
import net.schueller.peertube.adapter.ServerListAdapter
import net.schueller.peertube.database.Server
import net.schueller.peertube.database.ServerViewModel
import net.schueller.peertube.databinding.ActivityServerAddressBookBinding
import net.schueller.peertube.fragment.AddServerFragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.Session
import net.schueller.peertube.service.LoginService
import java.util.*
class ServerAddressBookActivity : CommonActivity() {
private val TAG = "ServerAddressBookActivity"
private val mServerViewModel: ServerViewModel by viewModels()
private var addServerFragment: AddServerFragment? = null
private val fragmentManager: FragmentManager by lazy { supportFragmentManager }
private lateinit var mBinding: ActivityServerAddressBookBinding
override fun onSupportNavigateUp(): Boolean {
finish() // close this activity as oppose to navigating up
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityServerAddressBookBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(mBinding.toolBarServerAddressBook)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
}
showServers()
mBinding.addServer.setOnClickListener {
Log.d(TAG, "Click")
val fragmentTransaction = fragmentManager.beginTransaction()
addServerFragment = AddServerFragment().also {
fragmentTransaction.replace(R.id.server_book, it)
fragmentTransaction.commit()
mBinding.addServer.hide()
}
}
}
private fun onServerClick(server: Server) {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
val editor = sharedPref.edit()
val serverUrl = APIUrlHelper.cleanServerUrl(server.serverHost)
editor.putString(getString(R.string.pref_api_base_key), serverUrl)
editor.apply()
// Logout if logged in
val session = Session.getInstance()
if (session.isLoggedIn) {
session.invalidate()
}
// attempt authentication if we have a username
if (server.username.isNullOrBlank().not()) {
LoginService.Authenticate(server.username, server.password)
}
// close this activity
finish()
Toast.makeText(this, getString(R.string.server_selection_set_server, serverUrl), Toast.LENGTH_LONG).show()
}
private fun onEditClick(server: Server) {
val fragmentTransaction = fragmentManager.beginTransaction()
addServerFragment = AddServerFragment.newInstance(server).also {
fragmentTransaction.replace(R.id.server_book, it)
fragmentTransaction.commit()
mBinding.addServer.hide()
}
}
private fun showServers() {
val adapter = ServerListAdapter(mutableListOf(), { onServerClick(it) }, { onEditClick(it) }).also {
mBinding.serverListRecyclerview.adapter = it
}
// Delete items on swipe
val helper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
AlertDialog.Builder(this@ServerAddressBookActivity)
.setTitle(getString(R.string.server_book_del_alert_title))
.setMessage(getString(R.string.server_book_del_alert_msg))
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
val position = viewHolder.adapterPosition
val server = adapter.getServerAtPosition(position)
// Toast.makeText(ServerAddressBookActivity.this, "Deleting " +
// server.getServerName(), Toast.LENGTH_LONG).show();
// Delete the server
mServerViewModel.delete(server)
}
.setNegativeButton(android.R.string.no) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.adapterPosition) }
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
})
helper.attachToRecyclerView(mBinding.serverListRecyclerview)
// Update the cached copy of the words in the adapter.
mServerViewModel.allServers.observe(this, { servers: List<Server> ->
adapter.setServers(servers)
addServerFragment?.let {
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.remove(it)
fragmentTransaction.commit()
mBinding.addServer.show()
}
})
}
companion object {
const val EXTRA_REPLY = "net.schueller.peertube.room.REPLY"
}
}

View File

@ -1,156 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.adapter;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.schueller.peertube.R;
import net.schueller.peertube.database.Server;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.network.Session;
import net.schueller.peertube.service.LoginService;
import java.util.List;
import static android.app.Activity.RESULT_OK;
public class ServerListAdapter extends RecyclerView.Adapter<ServerListAdapter.ServerViewHolder> {
private final LayoutInflater mInflater;
private List<Server> mServers; // Cached copy of Servers
public ServerListAdapter(Context context) {
this.mInflater = LayoutInflater.from(context);
}
@NonNull
@Override
public ServerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = mInflater.inflate(R.layout.row_server_address_book, parent, false);
return new ServerViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull ServerViewHolder holder, int position) {
if (mServers != null) {
Server current = mServers.get(position);
holder.serverLabel.setText(current.getServerName());
holder.serverUrl.setText(current.getServerHost());
if (TextUtils.isEmpty(current.getUsername())) {
holder.hasLogin.setVisibility(View.GONE);
} else {
holder.hasLogin.setVisibility(View.VISIBLE);
}
} else {
// Covers the case of data not being ready yet.
holder.serverLabel.setText(R.string.server_book_no_servers_found);
}
holder.itemView.setOnClickListener(v -> {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mInflater.getContext());
SharedPreferences.Editor editor = sharedPref.edit();
String serverUrl = APIUrlHelper.cleanServerUrl(getServerAtPosition(position).getServerHost());
editor.putString(mInflater.getContext().getString(R.string.pref_api_base_key), serverUrl);
editor.apply();
// Logout if logged in
Session session = Session.getInstance();
if (session.isLoggedIn()) {
session.invalidate();
}
// attempt authentication if we have a username
if (!TextUtils.isEmpty(getServerAtPosition(position).getUsername())) {
LoginService.Authenticate(
getServerAtPosition(position).getUsername(),
getServerAtPosition(position).getPassword()
);
}
// tell server list activity to reload list
Intent intent = new Intent();
((Activity) mInflater.getContext()).setResult(RESULT_OK, intent);
// close this activity
((Activity) mInflater.getContext()).finish();
Toast.makeText(mInflater.getContext(), mInflater.getContext().getString(R.string.server_selection_set_server, serverUrl), Toast.LENGTH_LONG).show();
});
//
// holder.itemView.setOnLongClickListener(v -> {
// Log.v("ServerListAdapter", "setOnLongClickListener " + position);
// return true;
// });
}
public void setServers(List<Server> Servers) {
mServers = Servers;
this.notifyDataSetChanged();
}
// getItemCount() is called many times, and when it is first called,
// mServers has not been updated (means initially, it's null, and we can't return null).
@Override
public int getItemCount() {
if (mServers != null)
return mServers.size();
else return 0;
}
static class ServerViewHolder extends RecyclerView.ViewHolder {
TextView serverLabel, serverUrl, serverUsername;
ImageView hasLogin;
private ServerViewHolder(View itemView) {
super(itemView);
serverLabel = itemView.findViewById(R.id.serverLabelRow);
serverUrl = itemView.findViewById(R.id.serverUrlRow);
hasLogin = itemView.findViewById(R.id.sb_row_has_login_icon);
}
}
public Server getServerAtPosition (int position) {
return mServers.get(position);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.adapter.ServerListAdapter.ServerViewHolder
import net.schueller.peertube.database.Server
import net.schueller.peertube.databinding.RowServerAddressBookBinding
import net.schueller.peertube.utils.visibleIf
class ServerListAdapter(private val mServers: MutableList<Server>, private val onClick: (Server) -> Unit, private val onEditClick: (Server) -> Unit) : RecyclerView.Adapter<ServerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServerViewHolder {
val binding = RowServerAddressBookBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ServerViewHolder(binding)
}
override fun onBindViewHolder(holder: ServerViewHolder, position: Int) {
holder.bind(mServers[position])
}
fun setServers(servers: List<Server>) {
mServers.clear()
mServers.addAll(servers)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return mServers.size
}
inner class ServerViewHolder (private val binding: RowServerAddressBookBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(server: Server) {
binding.serverLabelRow.text = server.serverName
binding.serverUrlRow.text = server.serverHost
binding.sbRowHasLoginIcon.visibleIf { server.username.isNullOrBlank().not() }
binding.root.setOnClickListener { onClick(server) }
binding.editIcon.setOnClickListener { onEditClick(server) }
}
}
fun getServerAtPosition(position: Int): Server {
return mServers[position]
}
}

View File

@ -1,86 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "server_table")
public class Server {
@PrimaryKey(autoGenerate = true)
private int id;
@NonNull
@ColumnInfo(name = "server_name")
private String serverName;
@ColumnInfo(name = "server_host")
private String serverHost;
@ColumnInfo(name = "username")
private String username;
@ColumnInfo(name = "password")
private String password;
public Server(@NonNull String serverName) {
this.serverName = serverName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getServerName() {
return serverName;
}
public void setServerName(String serverName) {
this.serverName = serverName;
}
public String getServerHost() {
return serverHost;
}
public void setServerHost(String serverHost) {
this.serverHost = serverHost;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@ -14,31 +14,31 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database;
package net.schueller.peertube.database
import android.app.Application;
import android.os.Parcelable
import androidx.room.PrimaryKey
import androidx.room.ColumnInfo
import androidx.room.Entity
import kotlinx.android.parcel.Parcelize
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
@Parcelize
@Entity(tableName = "server_table")
data class Server(
import java.util.List;
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
public class ServerViewModel extends AndroidViewModel {
@ColumnInfo(name = "server_name")
var serverName: String,
private ServerRepository mRepository;
@ColumnInfo(name = "server_host")
var serverHost: String? = null,
private LiveData<List<Server>> mAllServers;
@ColumnInfo(name = "username")
var username: String? = null,
public ServerViewModel (Application application) {
super(application);
mRepository = new ServerRepository(application);
mAllServers = mRepository.getAllServers();
}
@ColumnInfo(name = "password")
var password: String? = null
public LiveData<List<Server>> getAllServers() { return mAllServers; }
public void insert(Server server) { mRepository.insert(server); }
public void delete(Server server) {mRepository.delete(server);}
}
) : Parcelable

View File

@ -14,29 +14,26 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database;
package net.schueller.peertube.database
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import java.util.List;
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
public interface ServerDao {
interface ServerDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(Server server);
@Insert
suspend fun insert(server: Server)
@Update
suspend fun update(server: Server)
@Query("DELETE FROM server_table")
void deleteAll();
suspend fun deleteAll()
@Delete
void delete(Server server);
suspend fun delete(server: Server)
@Query("SELECT * from server_table ORDER BY server_name DESC")
LiveData<List<Server>> getAllServers();
@get:Query("SELECT * from server_table ORDER BY server_name DESC")
val allServers: LiveData<List<Server>>
}

View File

@ -1,79 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database;
import android.app.Application;
import android.os.AsyncTask;
import androidx.lifecycle.LiveData;
import java.util.List;
class ServerRepository {
private ServerDao mServerDao;
private LiveData<List<Server>> mAllServers;
ServerRepository(Application application) {
ServerRoomDatabase db = ServerRoomDatabase.getDatabase(application);
mServerDao = db.serverDao();
mAllServers = mServerDao.getAllServers();
}
LiveData<List<Server>> getAllServers() {
return mAllServers;
}
void insert (Server server) {
new insertAsyncTask(mServerDao).execute(server);
}
public void delete(Server server) {
new deleteServerAsyncTask(mServerDao).execute(server);
}
private static class insertAsyncTask extends AsyncTask<Server, Void, Void> {
private ServerDao mAsyncTaskDao;
insertAsyncTask(ServerDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Server... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
private static class deleteServerAsyncTask extends AsyncTask<Server, Void, Void> {
private ServerDao mAsyncTaskDao;
deleteServerAsyncTask(ServerDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Server... params) {
mAsyncTaskDao.delete(params[0]);
return null;
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import android.app.Application
import android.os.AsyncTask
import androidx.lifecycle.LiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class ServerRepository(application: Application) {
private val mServerDao: ServerDao
val allServers: LiveData<List<Server>>
get() = mServerDao.allServers
init {
val db = ServerRoomDatabase.getDatabase(application)
mServerDao = db.serverDao()
}
suspend fun update(server: Server) = withContext(Dispatchers.IO) {
mServerDao.update(server)
}
suspend fun insert(server: Server) = withContext(Dispatchers.IO) {
mServerDao.insert(server)
}
suspend fun delete(server: Server) = withContext(Dispatchers.IO){
mServerDao.delete(server)
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class ServerViewModel(application: Application) : AndroidViewModel(application) {
private val mRepository: ServerRepository = ServerRepository(application)
val allServers: LiveData<List<Server>> = mRepository.allServers
fun insert(server: Server) {
viewModelScope.launch {
mRepository.insert(server)
}
}
fun update(server: Server) {
viewModelScope.launch {
mRepository.update(server)
}
}
fun delete(server: Server) {
viewModelScope.launch {
mRepository.delete(server)
}
}
}

View File

@ -1,180 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.text.TextUtils;
import android.util.Log;
import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import net.schueller.peertube.R;
import net.schueller.peertube.activity.SearchServerActivity;
import net.schueller.peertube.activity.ServerAddressBookActivity;
import net.schueller.peertube.helper.APIUrlHelper;
import java.util.Objects;
import static android.app.Activity.RESULT_OK;
public class AddServerFragment extends Fragment {
public static final String TAG = "AddServerFragment";
public static final Integer PICK_SERVER = 1;
private OnFragmentInteractionListener mListener;
private View mView;
public AddServerFragment() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
// Inflate the layout for this fragment
mView = inflater.inflate(R.layout.fragment_add_server, container, false);
// bind button click
Button addServerButton = mView.findViewById(R.id.addServerButton);
addServerButton.setOnClickListener(view -> {
Activity act = getActivity();
boolean formValid = true;
// close keyboard
try {
assert act != null;
InputMethodManager inputManager = (InputMethodManager)
act.getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(Objects.requireNonNull(act.getCurrentFocus()).getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
} catch (Exception e) {
}
EditText selectedLabel = mView.findViewById(R.id.serverLabel);
if ( TextUtils.isEmpty(selectedLabel.getText())){
selectedLabel.setError( act.getString(R.string.server_book_label_is_required ));
Toast.makeText(act, R.string.invalid_url, Toast.LENGTH_LONG).show();
formValid = false;
}
// validate url
EditText selectedUrl = mView.findViewById(R.id.serverUrl);
String serverUrl = APIUrlHelper.cleanServerUrl(selectedUrl.getText().toString());
selectedUrl.setText(serverUrl);
if (!Patterns.WEB_URL.matcher(serverUrl).matches()) {
selectedUrl.setError( act.getString(R.string.server_book_valid_url_is_required ) );
Toast.makeText(act, R.string.invalid_url, Toast.LENGTH_LONG).show();
formValid = false;
}
if (formValid) {
if (act instanceof ServerAddressBookActivity) {
((ServerAddressBookActivity) act).addServer(mView);
}
}
});
// Button testServerButton = mView.findViewById(R.id.testServerButton);
// testServerButton.setOnClickListener(view -> {
// Activity act = getActivity();
// if (act instanceof ServerAddressBookActivity) {
// ((ServerAddressBookActivity) act).testServer();
// }
// });
Button pickServerUrl = mView.findViewById(R.id.pickServerUrl);
pickServerUrl.setOnClickListener(view -> {
Intent intentServer = new Intent(getActivity(), SearchServerActivity.class);
this.startActivityForResult(intentServer, PICK_SERVER);
});
return mView;
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_SERVER) {
if(resultCode == RESULT_OK) {
String serverUrlTest = data.getStringExtra("serverUrl");
//Log.d(TAG, "serverUrl " + serverUrlTest);
EditText serverUrl = mView.findViewById(R.id.serverUrl);
serverUrl.setText(serverUrlTest);
EditText serverLabel = mView.findViewById(R.id.serverLabel);
if ("".equals(serverLabel.getText().toString())) {
serverLabel.setText(data.getStringExtra("serverName"));
}
}
}
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnFragmentInteractionListener) {
mListener = (OnFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnFragmentInteractionListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import net.schueller.peertube.R
import net.schueller.peertube.activity.SearchServerActivity
import net.schueller.peertube.database.Server
import net.schueller.peertube.database.ServerViewModel
import net.schueller.peertube.databinding.FragmentAddServerBinding
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.utils.hideKeyboard
class AddServerFragment : Fragment() {
private lateinit var mBinding: FragmentAddServerBinding
private val mServerViewModel: ServerViewModel by activityViewModels()
private var mServer: Server? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
mServer = it.getParcelable(SERVER_ARG)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mBinding = FragmentAddServerBinding.inflate(inflater, container, false)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initServerEdit()
mBinding.addServerButton.setOnClickListener {
var formValid = true
hideKeyboard()
if (mBinding.serverLabel.text.toString().isBlank()) {
mBinding.serverLabel.error = getString(R.string.server_book_label_is_required)
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_LONG).show()
formValid = false
}
// validate url
mBinding.serverUrl.apply {
APIUrlHelper.cleanServerUrl(text.toString())?.let {
setText(it)
if (!Patterns.WEB_URL.matcher(it).matches()) {
error = getString(R.string.server_book_valid_url_is_required)
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_LONG).show()
formValid = false
}
}
}
if (formValid) {
mServer?.apply {
mBinding.let {
serverName = it.serverLabel.text.toString()
serverHost = it.serverUrl.text.toString()
username = it.serverUsername.text.toString()
password = it.serverPassword.text.toString()
mServerViewModel.update(this)
}
return@setOnClickListener
}
mBinding.apply {
val server = Server(serverName = serverLabel.text.toString())
server.serverHost = serverUrl.text.toString()
server.username = serverUsername.text.toString()
server.password = serverPassword.text.toString()
mServerViewModel.insert(server)
}
}
}
mBinding.pickServerUrl.setOnClickListener {
val intentServer = Intent(activity, SearchServerActivity::class.java)
this.startActivityForResult(intentServer, PICK_SERVER)
}
}
private fun initServerEdit() {
mServer?.let {
mBinding.apply {
serverLabel.setText(it.serverName)
serverUrl.setText(it.serverHost)
serverUsername.setText(it.username)
serverPassword.setText(it.password)
addServerButton.text = getString(R.string.server_book_add_save_button)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != PICK_SERVER) {
return
}
if (resultCode != Activity.RESULT_OK) {
return
}
val serverUrlTest = data?.getStringExtra("serverUrl")
//Log.d(TAG, "serverUrl " + serverUrlTest);
mBinding.serverUrl.setText(serverUrlTest)
mBinding.serverLabel.apply {
if (text.toString().isBlank()) {
setText(data?.getStringExtra("serverName"))
}
}
}
companion object {
private const val TAG = "AddServerFragment"
private const val PICK_SERVER = 1
private const val SERVER_ARG = "server"
fun newInstance(server: Server) = AddServerFragment().apply {
arguments = Bundle().also {
it.putParcelable(SERVER_ARG, server)
}
}
}
}

View File

@ -0,0 +1,58 @@
package net.schueller.peertube.utils
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
fun View.gone() {
this.visibility = View.GONE
}
fun View.visible() {
this.visibility = View.VISIBLE
}
fun View.invisible() {
this.visibility = View.INVISIBLE
}
fun View.visibleIf(predicate: () -> Boolean) {
when (predicate.invoke()) {
true -> visible()
else -> gone()
}
}
fun View.goneIf(predicate: () -> Boolean) {
when (predicate.invoke()) {
true -> gone()
else -> visible()
}
}
fun View.invisibleIf(predicate: () -> Boolean) {
when (predicate.invoke()) {
true -> invisible()
else -> visible()
}
}
fun Fragment.hideKeyboard(): Boolean {
activity?.currentFocus?.let {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(it.windowToken, 0)
return true
}
return false
}
fun AppCompatActivity.hideKeyboard(): Boolean {
currentFocus?.let {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(it.windowToken, 0)
return true
}
return false
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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"/>
</vector>

View File

@ -31,7 +31,25 @@
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@drawable/ic_baseline_add_24" />
<include layout="@layout/content_server_address_book" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/server_list_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
tools:listitem="@layout/row_server_address_book"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".activity.ServerAddressBookActivity"
tools:showIn="@layout/activity_server_address_book">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/server_list_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
tools:listitem="@layout/row_server_address_book" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -19,28 +19,29 @@
android:hint="@string/server_book_add_label"
android:inputType="textPersonName" />
<RelativeLayout
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="wrap_content">
android:layout_width="match_parent"
android:orientation="horizontal"
>
<EditText
android:layout_alignParentStart="true"
android:id="@+id/serverUrl"
android:layout_width="fill_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:hint="@string/server_book_add_server_url"
android:inputType="textUri"
android:layout_toStartOf="@+id/pickServerUrl"/>
/>
<Button
android:layout_alignParentEnd="true"
android:id="@+id/pickServerUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_book_add_pick_server_button" />
</RelativeLayout>
</LinearLayout>
<EditText
android:id="@+id/serverUsername"

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
@ -9,43 +11,52 @@
card_view:cardElevation="0dp"
card_view:cardUseCompatPadding="true">
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:padding="12dp">
<LinearLayout
android:layout_alignParentStart="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/serverLabelRow"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Headline" />
card_view:layout_constraintTop_toTopOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Headline"
tools:text="@tools:sample/lorem"
/>
<TextView
android:id="@+id/serverUrlRow"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="0dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
</LinearLayout>
card_view:layout_constraintTop_toBottomOf="@id/serverLabelRow"
card_view:layout_constraintStart_toStartOf="parent"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead"
tools:text="@tools:sample/lorem"
/>
<ImageView
android:id="@+id/sb_row_has_login_icon"
android:src="@drawable/ic_baseline_account_circle_24"
android:visibility="visible"
android:contentDescription="@string/server_book_list_has_login"
android:layout_width="24dp"
android:layout_alignParentEnd="true"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content"
card_view:layout_constraintTop_toTopOf="parent"
card_view:layout_constraintEnd_toEndOf="parent"
android:contentDescription="@string/server_book_list_has_login"
android:src="@drawable/ic_baseline_account_circle_24"
/>
</RelativeLayout>
<ImageView
android:id="@+id/edit_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
card_view:layout_constraintTop_toBottomOf="@id/sb_row_has_login_icon"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/server_book_list_has_login"
android:src="@drawable/ic_edit_24"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -335,6 +335,7 @@
<string name="server_book_add_username">Username</string>
<string name="server_book_add_password">Password</string>
<string name="server_book_add_add_button">Add</string>
<string name="server_book_add_save_button">Save</string>
<string name="server_book_list_has_login">Has Login</string>
<string name="login_current_server_hint">Current Server</string>
<string name="title_activity_server_address_book">Address Book</string>

View File

@ -2,12 +2,15 @@
buildscript {
ext.kotlin_version = '1.4.21'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong