diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index d7d8aace..00000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index d9ee0bfe..00000000 Binary files a/.idea/caches/build_file_checksums.ser and /dev/null differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 30aa626c..00000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 99202cc2..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 3d8ecea3..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 8970495f..a0ee430a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Features - Customizable primary and accent colors - Supports ARM and x86 devices - Supports SDK 21+ (Lollipop 5.0) +- Intentservice to start tasks via third party apps! TODO ------------ @@ -46,6 +47,22 @@ Grab the [latest version](https://github.com/kaczmarkiewiczp/rcloneExplorer/rele - For x86 devices download RcloneExplorer-x86.apk - Ultimately, RcloneExplorer.apk will work with both ARM and x86 devices. + + +Intentservice +------------- +This app includes the ability to launch an intent! Create a task to sync to a remote, and copy it's id (via the treedot-menu) +The intent needs the following: + +| Intent | Content | | +| :------------- | :-------------: | -------------: | +| packageName | ca.pkay.rcloneexplorer | | +| className | ca.pkay.rcloneexplorer.Services.TaskStartService | | +| Action | START_TASK | | +| Integer Extra | task | idOfTask | +| Boolean Extra | notification | true or false | + + Credits/Libraries ----------------- - [Android Support Libraries](https://developer.android.com/topic/libraries/support-library) diff --git a/app/build.gradle b/app/build.gradle index 55862551..cb7e4522 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'com.android.application' -apply plugin: 'io.fabric' android { compileSdkVersion 28 @@ -40,13 +39,8 @@ dependencies { implementation 'com.github.GrenderG:Toasty:1.3.0' implementation 'com.android.support:support-v4:28.0.0' implementation 'com.github.bumptech.glide:glide:4.7.1' - implementation 'com.google.firebase:firebase-core:16.0.5' - implementation 'com.crashlytics.sdk.android:crashlytics:2.9.5' - implementation 'com.google.firebase:firebase-messaging:17.3.4' annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } - -apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bdba3096..7ae5875d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,12 +2,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:theme="@style/AppTheme.NoActionBar"> + + + + + + + + + + + + @@ -128,7 +93,58 @@ android:name=".RemoteConfig.RemoteConfig" android:label="@string/title_activity_remote_config" android:theme="@style/AppTheme.NoActionBar" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Database/DatabaseHandler.java b/app/src/main/java/ca/pkay/rcloneexplorer/Database/DatabaseHandler.java new file mode 100644 index 00000000..49c577b4 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Database/DatabaseHandler.java @@ -0,0 +1,128 @@ +package ca.pkay.rcloneexplorer.Database; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.util.ArrayList; +import java.util.List; + +public class DatabaseHandler extends SQLiteOpenHelper { + + + // If you change the database schema, you must increment the database version. + public static final int DATABASE_VERSION = 1; + public static final String DATABASE_NAME = "rcloneExplorer.db"; + + public DatabaseHandler(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.execSQL(DatabaseInfo.SQL_CREATE_TABLES); + } + + @Override + public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { + + } + + public List getAllTasks(){ + SQLiteDatabase db = getReadableDatabase(); + + String[] projection = { + Task.COLUMN_NAME_ID, + Task.COLUMN_NAME_TITLE, + Task.COLUMN_NAME_REMOTE_ID, + Task.COLUMN_NAME_REMOTE_TYPE, + Task.COLUMN_NAME_REMOTE_PATH, + Task.COLUMN_NAME_LOCAL_PATH, + Task.COLUMN_NAME_SYNC_DIRECTION + }; + + String selection = ""; + String[] selectionArgs = {}; + + String sortOrder = Task.COLUMN_NAME_ID + " ASC"; + + Cursor cursor = db.query( + Task.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + sortOrder + ); + + List results = new ArrayList<>(); + while(cursor.moveToNext()) { + + Task task = new Task(cursor.getLong(0)); + task.setTitle(cursor.getString(1)); + task.setRemote_id(cursor.getString(2)); + task.setRemote_type(cursor.getInt(3)); + task.setRemote_path(cursor.getString(4)); + task.setLocal_path(cursor.getString(5)); + task.setDirection(cursor.getInt(6)); + + results.add(task); + } + cursor.close(); + + return results; + + } + + public Task createEntry(Task taskToStore){ + SQLiteDatabase db = getWritableDatabase(); + + ContentValues values = new ContentValues(); + values.put(Task.COLUMN_NAME_TITLE, taskToStore.getTitle()); + values.put(Task.COLUMN_NAME_LOCAL_PATH, taskToStore.getLocal_path()); + values.put(Task.COLUMN_NAME_REMOTE_ID, taskToStore.getRemote_id()); + values.put(Task.COLUMN_NAME_REMOTE_PATH, taskToStore.getRemote_path()); + values.put(Task.COLUMN_NAME_REMOTE_TYPE, taskToStore.getRemote_type()); + values.put(Task.COLUMN_NAME_SYNC_DIRECTION, taskToStore.getDirection()); + + long newRowId = db.insert(Task.TABLE_NAME, null, values); + + Task newObject = new Task(newRowId); + newObject.setTitle(taskToStore.getTitle()); + newObject.setLocal_path(taskToStore.getLocal_path()); + newObject.setRemote_id(taskToStore.getRemote_id()); + newObject.setRemote_path(taskToStore.getRemote_path()); + newObject.setRemote_type(taskToStore.getRemote_type()); + newObject.setDirection(taskToStore.getDirection()); + + return newObject; + + } + + public void updateEntry(Task taskToUpdate) { + SQLiteDatabase db = getWritableDatabase(); + + ContentValues values = new ContentValues(); + values.put(Task.COLUMN_NAME_TITLE, taskToUpdate.getTitle()); + values.put(Task.COLUMN_NAME_LOCAL_PATH, taskToUpdate.getLocal_path()); + values.put(Task.COLUMN_NAME_REMOTE_ID, taskToUpdate.getRemote_id()); + values.put(Task.COLUMN_NAME_REMOTE_PATH, taskToUpdate.getRemote_path()); + values.put(Task.COLUMN_NAME_REMOTE_TYPE, taskToUpdate.getRemote_type()); + values.put(Task.COLUMN_NAME_SYNC_DIRECTION, taskToUpdate.getDirection()); + + db.update(Task.TABLE_NAME, values, Task.COLUMN_NAME_ID+" = ?", new String[]{String.valueOf(taskToUpdate.getId())}); + + } + + public int deleteEntry(long id){ + SQLiteDatabase db = getWritableDatabase(); + String selection = Task.COLUMN_NAME_ID + " LIKE ?"; + String[] selectionArgs = {String.valueOf(id)}; + return db.delete(Task.TABLE_NAME, selection, selectionArgs); + + } + +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Database/DatabaseInfo.java b/app/src/main/java/ca/pkay/rcloneexplorer/Database/DatabaseInfo.java new file mode 100644 index 00000000..e6154de5 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Database/DatabaseInfo.java @@ -0,0 +1,14 @@ +package ca.pkay.rcloneexplorer.Database; + +class DatabaseInfo { + + + public static final String SQL_CREATE_TABLES = "CREATE TABLE " + Task.TABLE_NAME + " (" + + Task.COLUMN_NAME_ID + " INTEGER PRIMARY KEY," + + Task.COLUMN_NAME_TITLE + " TEXT," + + Task.COLUMN_NAME_REMOTE_ID + " TEXT," + + Task.COLUMN_NAME_REMOTE_TYPE + " INTEGER," + + Task.COLUMN_NAME_REMOTE_PATH+ " TEXT," + + Task.COLUMN_NAME_LOCAL_PATH + " TEXT," + + Task.COLUMN_NAME_SYNC_DIRECTION + " INTEGER)"; +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Database/Task.java b/app/src/main/java/ca/pkay/rcloneexplorer/Database/Task.java new file mode 100644 index 00000000..6a5d4533 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Database/Task.java @@ -0,0 +1,88 @@ +package ca.pkay.rcloneexplorer.Database; + +public class Task { + + public static String TABLE_NAME = "task_table"; + + public static String COLUMN_NAME_ID= "task_id"; + public static String COLUMN_NAME_TITLE = "task_title"; + public static String COLUMN_NAME_REMOTE_ID = "task_remote_id"; + public static String COLUMN_NAME_REMOTE_TYPE = "task_remote_type"; + public static String COLUMN_NAME_REMOTE_PATH = "task_remote_path"; + public static String COLUMN_NAME_LOCAL_PATH = "task_local_path"; + public static String COLUMN_NAME_SYNC_DIRECTION = "task_direction"; + + private Long id; + + private String title=""; + private String remote_id=""; + private int remote_type=0; + private String remote_path=""; + private String local_path = ""; + private int direction=0; + + + public Task(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getRemote_id() { + return remote_id; + } + + public void setRemote_id(String remote_id) { + this.remote_id = remote_id; + } + + public int getRemote_type() { + return remote_type; + } + + public void setRemote_type(int remote_type) { + this.remote_type = remote_type; + } + + public String getRemote_path() { + return remote_path; + } + + public void setRemote_path(String remote_path) { + this.remote_path = remote_path; + } + + public String getLocal_path() { + return local_path; + } + + public void setLocal_path(String local_path) { + this.local_path = local_path; + } + + public int getDirection() { + return direction; + } + + public void setDirection(int direction) { + this.direction = direction; + } + + public String toString(){ + return title + ": " + remote_id + ": " + remote_type + ": " + remote_path + ": " + local_path + ": " + direction; + } +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Database/xml/Exporter.java b/app/src/main/java/ca/pkay/rcloneexplorer/Database/xml/Exporter.java new file mode 100644 index 00000000..863a4745 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Database/xml/Exporter.java @@ -0,0 +1,129 @@ +package ca.pkay.rcloneexplorer.Database.xml; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Environment; +import android.support.v4.app.ActivityCompat; +import android.widget.Toast; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import ca.pkay.rcloneexplorer.Database.DatabaseHandler; +import ca.pkay.rcloneexplorer.Database.Task; +import ca.pkay.rcloneexplorer.R; +import es.dmoral.toasty.Toasty; + +public class Exporter { + + private static final int PERMISSION_WRITE_EXTERNAL = 739; + + public static void export(Activity activity){ + try { + String xml = Exporter.create(activity); + Exporter.storeFile(activity, xml); + } catch (ParserConfigurationException | TransformerException | IOException e) { + e.printStackTrace(); + } + } + + public static String create(Activity a) throws ParserConfigurationException, TransformerException { + + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = documentBuilder.newDocument(); + + Element rootElement = document.createElement("tasks"); + document.appendChild(rootElement); + + DatabaseHandler dbHandler = new DatabaseHandler(a); + + for(Task task : dbHandler.getAllTasks()){ + Element em = document.createElement("task"); + em.appendChild(createChild(document, "id", String.valueOf(task.getId()))); + em.appendChild(createChild(document, "name", task.getTitle())); + em.appendChild(createChild(document, "remote_name", task.getRemote_id())); + em.appendChild(createChild(document, "remote_path", task.getRemote_path())); + em.appendChild(createChild(document, "local_path", task.getLocal_path())); + em.appendChild(createChild(document, "sync_direction", String.valueOf(task.getDirection()))); + rootElement.appendChild(em); + } + + + StringWriter sw = new StringWriter(); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(new DOMSource(document), new StreamResult(sw)); + + + return sw.toString(); + } + + public static void storeFile(Activity activity, String content) throws IOException { + + if(!Exporter.isWriteStoragePermissionGranted(activity)){ + return; + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss"); + String currentDateAndTime = sdf.format(new Date()); + + String filename = "rcloneExplorer_"+currentDateAndTime+".xml"; + + + File path = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/rcloneExplorer"); + path.mkdirs(); + File file = new File(path, filename); + FileOutputStream stream = new FileOutputStream(file); + OutputStreamWriter myOutWriter = new OutputStreamWriter(stream); + + try { + myOutWriter.append(content); + Toasty.info(activity, activity.getResources().getString(R.string.exporter_success)+" "+Environment.getExternalStorageDirectory().getAbsolutePath()+"/rcloneExplorer/"+filename, Toast.LENGTH_SHORT, true).show(); + } finally { + myOutWriter.close(); + stream.close(); + } + + } + + private static Element createChild(Document document, String name, String content){ + Element child = document.createElement(name); + child.setTextContent(content); + return child; + } + + + public static boolean isWriteStoragePermissionGranted(Activity a) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (a.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + return true; + } else { + ActivityCompat.requestPermissions(a, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_WRITE_EXTERNAL); + return false; + } + }else{ + //permission is always granted on lower versions + return true; + } + } + +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Database/xml/Importer.java b/app/src/main/java/ca/pkay/rcloneexplorer/Database/xml/Importer.java new file mode 100644 index 00000000..0c5f687a --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Database/xml/Importer.java @@ -0,0 +1,79 @@ +package ca.pkay.rcloneexplorer.Database.xml; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import ca.pkay.rcloneexplorer.Database.Task; + +public class Importer { + + public static final int READ_REQUEST_CODE = 896; + public static final int PERM_REQUEST_CODE = 897; + + public static ArrayList createTasklist(String content) throws ParserConfigurationException, IOException, SAXException { + ArrayList result = new ArrayList<>(); + + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = documentBuilder.parse(new InputSource(new StringReader(content))); + + NodeList nodeList = document.getElementsByTagName("task"); + + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + NodeList children = node.getChildNodes(); + Task task = new Task(-1L); + for (int j = 0; j < children.getLength(); j++) { + Node type = children.item(j); + + String nodeContent = type.getTextContent(); + switch (type.getNodeName()) { + case "id": + task.setId(Long.valueOf(nodeContent)); + break; + case "name": + task.setTitle(nodeContent); + break; + case "remote_name": + task.setRemote_id(nodeContent); + break; + case "remote_path": + task.setRemote_path(nodeContent); + break; + case "local_path": + task.setLocal_path(nodeContent); + break; + case "sync_direction": + task.setDirection(Integer.valueOf(nodeContent)); + break; + } + } + result.add(task); + } + return result; + } + + public static boolean getFilePermission(Activity a) { + boolean hasPermission = (ContextCompat.checkSelfPermission(a, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); + if (!hasPermission) { + ActivityCompat.requestPermissions(a, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERM_REQUEST_CODE); + } + return hasPermission; + } + +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/TaskDialog.java b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/TaskDialog.java new file mode 100644 index 00000000..6172994b --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/TaskDialog.java @@ -0,0 +1,243 @@ +package ca.pkay.rcloneexplorer.Dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.ArrayList; + +import ca.pkay.rcloneexplorer.Database.DatabaseHandler; +import ca.pkay.rcloneexplorer.Database.Task; +import ca.pkay.rcloneexplorer.Items.RemoteItem; +import ca.pkay.rcloneexplorer.Items.SyncDirectionObject; +import ca.pkay.rcloneexplorer.R; +import ca.pkay.rcloneexplorer.Rclone; +import ca.pkay.rcloneexplorer.RecyclerViewAdapters.TasksRecyclerViewAdapter; + +public class TaskDialog extends Dialog { + + private Button task_back; + private Button task_next; + private Button task_save; + private Button task_cancel; + private TasksRecyclerViewAdapter recyclerViewAdapter; + private Rclone rcloneInstance = new Rclone(getContext()); + + private Task existingTask; + + private int uiButtonState = 0; + + public TaskDialog(@NonNull Context context, TasksRecyclerViewAdapter tasksRecyclerViewAdapter) { + super(context); + this.recyclerViewAdapter=tasksRecyclerViewAdapter; + } + + public TaskDialog(@NonNull Context context, TasksRecyclerViewAdapter tasksRecyclerViewAdapter, Task task) { + super(context); + this.recyclerViewAdapter=tasksRecyclerViewAdapter; + this.existingTask=task; + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.task_dialog); + + + getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); + setCanceledOnTouchOutside(true); + + task_back = findViewById(R.id.task_back); + task_next = findViewById(R.id.task_next); + task_save = findViewById(R.id.task_save); + task_cancel = findViewById(R.id.task_cancel); + + task_back.setVisibility(View.INVISIBLE); + + + + Spinner remoteDropdown = findViewById(R.id.task_remote_spinner); + + String[] items = new String[rcloneInstance.getRemotes().size()]; + + for (int i = 0; i< rcloneInstance.getRemotes().size(); i++) { + items[i]= rcloneInstance.getRemotes().get(i).getName(); + } + + ArrayAdapter adapter = new ArrayAdapter(getContext(), android.R.layout.simple_spinner_dropdown_item, items); + remoteDropdown.setAdapter(adapter); + + + Spinner directionDropdown = findViewById(R.id.task_direction_spinner); + String[] options = SyncDirectionObject.getOptionsArray(getContext()); + ArrayAdapter directionAdapter = new ArrayAdapter(getContext(), android.R.layout.simple_spinner_dropdown_item, options); + directionDropdown.setAdapter(directionAdapter); + + + populateFields(items); + hideAllSettingsInUI(); + decideUIButtonState(); + + + task_next.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Log.e("APP!", "TaskDialog: next!"); + hideAllSettingsInUI(); + uiButtonState++; + decideUIButtonState(); + } + }); + task_back.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Log.e("APP!", "TaskDialog: back!"); + hideAllSettingsInUI(); + uiButtonState--; + decideUIButtonState(); + } + }); + task_cancel.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Log.e("APP!", "TaskDialog: cancel!"); + cancel(); + } + }); + task_save.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Log.e("APP!", "TaskDialog: Save!"); + if(existingTask==null){ + saveTask(); + }else{ + persistTaskChanges(); + } + } + }); + } + + private void populateFields(String[] remotes) { + Log.e("app!", "Populate Task"); + if(existingTask!=null){ + Log.e("app!", "Populate Task"+existingTask.getTitle()); + ((TextView)findViewById(R.id.task_title_textfield)).setText(existingTask.getTitle()); + Spinner s = findViewById(R.id.task_remote_spinner); + + int i=0; + for(String remote: remotes) { + if(remote.equals(existingTask.getRemote_id())){ + s.setSelection(i); + } + i++; + } + + ((TextView)findViewById(R.id.task_remote_path_textfield)).setText(existingTask.getRemote_path()); + ((TextView)findViewById(R.id.task_local_path_textfield)).setText(existingTask.getLocal_path()); + ((Spinner)findViewById(R.id.task_direction_spinner)).setSelection(existingTask.getDirection()-1); + } + } + + private void persistTaskChanges(){ + + DatabaseHandler dbHandler = new DatabaseHandler(getContext()); + dbHandler.updateEntry(getTaskValues(existingTask.getId())); + + recyclerViewAdapter.setList((ArrayList) dbHandler.getAllTasks()); + + Log.e("app!", "Update Task: "); + cancel(); + } + + private void saveTask(){ + DatabaseHandler dbHandler = new DatabaseHandler(getContext()); + Task newTask = dbHandler.createEntry(getTaskValues(0L)); + recyclerViewAdapter.addTask(newTask); + + Log.e("app!", "Task Dialog: "+newTask.toString()); + cancel(); + } + + private Task getTaskValues(Long id ){ + Task taskToPopulate = new Task(id); + taskToPopulate.setTitle(((EditText)findViewById(R.id.task_title_textfield)).getText().toString()); + + String remotename=((Spinner)findViewById(R.id.task_remote_spinner)).getSelectedItem().toString(); + taskToPopulate.setRemote_id(remotename); + + int direction = ((Spinner)findViewById(R.id.task_direction_spinner)).getSelectedItemPosition()+1; + + + + for (RemoteItem ri: rcloneInstance.getRemotes()) { + if(ri.getName().equals(taskToPopulate.getRemote_id())){ + taskToPopulate.setRemote_type(ri.getType()); + } + } + + taskToPopulate.setRemote_path(((EditText)findViewById(R.id.task_remote_path_textfield)).getText().toString()); + taskToPopulate.setLocal_path(((EditText)findViewById(R.id.task_local_path_textfield)).getText().toString()); + taskToPopulate.setDirection(direction); + return taskToPopulate; + } + + + private void hideAllSettingsInUI(){ + + findViewById(R.id.task_name_layout).setVisibility(View.GONE); + findViewById(R.id.task_remote_layout).setVisibility(View.GONE); + findViewById(R.id.task_remote_path_layout).setVisibility(View.GONE); + findViewById(R.id.task_local_path_layout).setVisibility(View.GONE); + findViewById(R.id.task_direction_layout).setVisibility(View.GONE); + + } + + private void decideUIButtonState(){ + + if(uiButtonState ==0){ + task_back.setVisibility(View.INVISIBLE); + task_save.setVisibility(View.GONE); + task_next.setVisibility(View.VISIBLE); + }else if (uiButtonState == 4){ + task_save.setVisibility(View.VISIBLE); + task_back.setVisibility(View.VISIBLE); + task_next.setVisibility(View.GONE); + }else { + task_back.setVisibility(View.VISIBLE); + task_next.setVisibility(View.VISIBLE); + task_save.setVisibility(View.GONE); + } + + switch(uiButtonState) { + case 0: + ((TextView)findViewById(R.id.task_dialog_title)).setText(getContext().getString(R.string.task_dialog_title_name)); + findViewById(R.id.task_name_layout).setVisibility(View.VISIBLE); + break; + case 1: + ((TextView)findViewById(R.id.task_dialog_title)).setText(getContext().getString(R.string.task_dialog_title_remote)); + findViewById(R.id.task_remote_layout).setVisibility(View.VISIBLE); + break; + case 2: + ((TextView)findViewById(R.id.task_dialog_title)).setText(getContext().getString(R.string.task_dialog_title_remote_path)); + findViewById(R.id.task_remote_path_layout).setVisibility(View.VISIBLE); + break; + case 3: + ((TextView)findViewById(R.id.task_dialog_title)).setText(getContext().getString(R.string.task_dialog_title_local_path)); + findViewById(R.id.task_local_path_layout).setVisibility(View.VISIBLE); + break; + case 4: + ((TextView)findViewById(R.id.task_dialog_title)).setText(getContext().getString(R.string.task_dialog_title_sync_direction)); + findViewById(R.id.task_direction_layout).setVisibility(View.VISIBLE); + break; + } + } + +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java index 7ba1688f..9dca94ea 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java @@ -71,6 +71,7 @@ import ca.pkay.rcloneexplorer.Items.FileItem; import ca.pkay.rcloneexplorer.Items.RemoteItem; import ca.pkay.rcloneexplorer.Dialogs.OpenAsDialog; +import ca.pkay.rcloneexplorer.Items.SyncDirectionObject; import ca.pkay.rcloneexplorer.MainActivity; import ca.pkay.rcloneexplorer.R; import ca.pkay.rcloneexplorer.Rclone; @@ -1404,16 +1405,19 @@ private void showSyncDialog(String path) { } else { builder = new AlertDialog.Builder(context); } - String[] options = new String[] {"Sync local to remote", "Sync remote to local"}; + String[] options = SyncDirectionObject.getOptionsArray(getContext()); builder.setTitle(R.string.select_sync_direction); builder.setItems(options, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - syncDirection = Rclone.SYNC_DIRECTION_LOCAL_TO_REMOTE; - } else { - syncDirection = Rclone.SYNC_DIRECTION_REMOTE_TO_LOCAL; + int value = which+1; + switch (value){ + case SyncDirectionObject.SYNC_REMOTE_TO_LOCAL: syncDirection = SyncDirectionObject.SYNC_REMOTE_TO_LOCAL; break; + case SyncDirectionObject.COPY_LOCAL_TO_REMOTE: syncDirection = SyncDirectionObject.COPY_LOCAL_TO_REMOTE; break; + case SyncDirectionObject.COPY_REMOTE_TO_LOCAL: syncDirection = SyncDirectionObject.COPY_REMOTE_TO_LOCAL; break; + default: syncDirection = SyncDirectionObject.SYNC_LOCAL_TO_REMOTE; break; } + Intent intent = new Intent(context, FilePicker.class); intent.putExtra(FilePicker.FILE_PICKER_PICK_DESTINATION_TYPE, true); startActivityForResult(intent, FILE_PICKER_SYNC_RESULT); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/TasksFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/TasksFragment.java new file mode 100644 index 00000000..518fe133 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/TasksFragment.java @@ -0,0 +1,191 @@ +package ca.pkay.rcloneexplorer.Fragments; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.xml.sax.SAXException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import javax.xml.parsers.ParserConfigurationException; + +import ca.pkay.rcloneexplorer.Database.DatabaseHandler; +import ca.pkay.rcloneexplorer.Database.Task; +import ca.pkay.rcloneexplorer.Database.xml.Exporter; +import ca.pkay.rcloneexplorer.Database.xml.Importer; +import ca.pkay.rcloneexplorer.Dialogs.TaskDialog; +import ca.pkay.rcloneexplorer.R; +import ca.pkay.rcloneexplorer.RecyclerViewAdapters.TasksRecyclerViewAdapter; +import es.dmoral.toasty.Toasty; +import jp.wasabeef.recyclerview.animators.LandingAnimator; + +public class TasksFragment extends Fragment { + + private TasksRecyclerViewAdapter recyclerViewAdapter; + private Activity filePickerActivity; + + + public TasksFragment() { + // Required empty public constructor + } + + public static TasksFragment newInstance() { + return new TasksFragment(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ((FragmentActivity) getContext()).setTitle(R.string.tasks); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + + View view = inflater.inflate(R.layout.fragment_tasks, container, false); + + final Context context = view.getContext(); + + RecyclerView recyclerView = view.findViewById(R.id.task_list); + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + recyclerView.setItemAnimator(new LandingAnimator()); + + final DatabaseHandler dbHandler = new DatabaseHandler(context); + + recyclerViewAdapter = new TasksRecyclerViewAdapter(dbHandler.getAllTasks(), context); + recyclerView.setAdapter(recyclerViewAdapter); + + + view.findViewById(R.id.newTask).setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + new TaskDialog(context, recyclerViewAdapter).show(); + } + }); + + return view; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + } + + @Override + public void onDetach() { + super.onDetach(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.tasks_fragment_menu, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + case R.id.action_import: + if(Importer.getFilePermission(getActivity())){ + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/xml"); + startActivityForResult(intent, Importer.READ_REQUEST_CODE); + } + break; + case R.id.action_export: + Exporter.export(getActivity()); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + Activity activity = getActivity(); + Context context = getContext(); + + if(activity==null){ + Toasty.error(context, context.getResources().getString(R.string.importer_unknown_error), Toast.LENGTH_SHORT, true).show(); + return; + } + + switch (requestCode){ + case Importer.PERM_REQUEST_CODE: + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/xml"); + startActivityForResult(intent, Importer.READ_REQUEST_CODE); + break; + case Importer.READ_REQUEST_CODE: + try { + String importedData=""; + if (resultCode == Activity.RESULT_OK) { + + Uri uri = null; + if (resultData != null) { + uri = resultData.getData(); + + if(uri==null){ + Toasty.error(context, context.getResources().getString(R.string.importer_no_file_selected), Toast.LENGTH_SHORT, true).show(); + return; + } + + InputStream in = activity.getContentResolver().openInputStream(uri); + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + StringBuilder out = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + out.append(line); + } + + reader.close(); + if (in != null) { + in.close(); + } + importedData = out.toString(); + } + } + + ArrayList importedList = Importer.createTasklist(importedData); + DatabaseHandler dbHandler = new DatabaseHandler(context); + for (Task t : dbHandler.getAllTasks()){ + dbHandler.deleteEntry(t.getId()); + } + + for(Task t: importedList){ + dbHandler.createEntry(t); + } + + recyclerViewAdapter.setList((ArrayList) dbHandler.getAllTasks()); + + } catch (IOException | SAXException | ParserConfigurationException e) { + e.printStackTrace(); + } + + break; + } + } +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java b/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java index f2ab72cc..76f5cbaa 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java @@ -44,6 +44,12 @@ public RemoteItem(String name, String type) { this.type = getTypeFromString(type); } + public RemoteItem(String name, int type, String typeReadable) { + this.name = name; + this.typeReadable = typeReadable; + this.type = type; + } + private RemoteItem(Parcel in) { name = in.readString(); type = in.readInt(); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Items/SyncDirectionObject.java b/app/src/main/java/ca/pkay/rcloneexplorer/Items/SyncDirectionObject.java new file mode 100644 index 00000000..432ba97f --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Items/SyncDirectionObject.java @@ -0,0 +1,39 @@ +package ca.pkay.rcloneexplorer.Items; + +import android.content.Context; + +import ca.pkay.rcloneexplorer.R; + +/** + * Copyright (C) 2019 Felix Nüsse + * Created on 22.12.19 - 14:46 + *

+ * Edited by: Felix Nüsse felix.nuesse(at)t-online.de + *

+ * rcloneExplorer + *

+ * This program is released under the MIT license + *

+ *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +public class SyncDirectionObject { + + public static final int SYNC_LOCAL_TO_REMOTE = 1; + public static final int SYNC_REMOTE_TO_LOCAL = 2; + public static final int COPY_LOCAL_TO_REMOTE = 3; + public static final int COPY_REMOTE_TO_LOCAL = 4; + + + public static String[] getOptionsArray(Context context) { + return context.getResources().getStringArray(R.array.sync_direction_array); + } +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java index 2ea2b27b..8ab2ac36 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java @@ -35,10 +35,8 @@ import android.view.SubMenu; import android.view.View; import android.widget.Toast; - import com.crashlytics.android.Crashlytics; import com.google.firebase.messaging.FirebaseMessaging; - import java.io.File; import java.io.IOException; import java.util.Collections; @@ -49,6 +47,7 @@ import ca.pkay.rcloneexplorer.Dialogs.LoadingDialog; import ca.pkay.rcloneexplorer.Fragments.FileExplorerFragment; import ca.pkay.rcloneexplorer.Fragments.RemotesFragment; +import ca.pkay.rcloneexplorer.Fragments.TasksFragment; import ca.pkay.rcloneexplorer.Items.RemoteItem; import ca.pkay.rcloneexplorer.Settings.SettingsActivity; import es.dmoral.toasty.Toasty; @@ -296,6 +295,9 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { case R.id.nav_remotes: startRemotesFragment(); break; + case R.id.nav_tasks: + startTasksFragment(); + break; case R.id.nav_import: if (rclone.isConfigFileCreated()) { warnUserAboutOverwritingConfiguration(); @@ -359,6 +361,19 @@ private void startRemotesFragment() { } } + private void startTasksFragment() { + fragment = TasksFragment.newInstance(); + FragmentManager fragmentManager = getSupportFragmentManager(); + + for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { + fragmentManager.popBackStack(); + } + + if (!isFinishing()) { + fragmentManager.beginTransaction().replace(R.id.flFragment, fragment).commitAllowingStateLoss(); + } + } + private RemoteItem getRemoteItemFromName(String remoteName) { List remoteItemList = rclone.getRemotes(); for (RemoteItem remoteItem : remoteItemList) { diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java index 6e4a5139..5594099b 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java @@ -29,12 +29,10 @@ import ca.pkay.rcloneexplorer.Items.FileItem; import ca.pkay.rcloneexplorer.Items.RemoteItem; +import ca.pkay.rcloneexplorer.Items.SyncDirectionObject; import es.dmoral.toasty.Toasty; public class Rclone { - - public static final int SYNC_DIRECTION_LOCAL_TO_REMOTE = 1; - public static final int SYNC_DIRECTION_REMOTE_TO_LOCAL = 2; public static final int SERVE_PROTOCOL_HTTP = 1; public static final int SERVE_PROTOCOL_WEBDAV = 2; public static final int SERVE_PROTOCOL_FTP = 3; @@ -416,11 +414,15 @@ public Process sync(RemoteItem remoteItem, String remote, String localPath, int String localRemotePath = (remoteItem.isRemoteType(RemoteItem.LOCAL)) ? Environment.getExternalStorageDirectory().getAbsolutePath() + "/" : ""; String remotePath = (remote.compareTo("//" + remoteName) == 0) ? remoteName + ":" + localRemotePath : remoteName + ":" + localRemotePath + remote; - if (syncDirection == 1) { + if (syncDirection == SyncDirectionObject.SYNC_LOCAL_TO_REMOTE) { command = createCommandWithOptions("sync", localPath, remotePath, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); - } else if (syncDirection == 2) { + } else if (syncDirection == SyncDirectionObject.SYNC_REMOTE_TO_LOCAL) { command = createCommandWithOptions("sync", remotePath, localPath, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); - } else { + } else if (syncDirection == SyncDirectionObject.COPY_LOCAL_TO_REMOTE) { + command = createCommandWithOptions("copy", localPath, remotePath, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); + }else if (syncDirection == SyncDirectionObject.COPY_REMOTE_TO_LOCAL) { + command = createCommandWithOptions("copy", remotePath, localPath, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); + }else { return null; } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/TasksRecyclerViewAdapter.java b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/TasksRecyclerViewAdapter.java new file mode 100644 index 00000000..916fc83c --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/TasksRecyclerViewAdapter.java @@ -0,0 +1,254 @@ +package ca.pkay.rcloneexplorer.RecyclerViewAdapters; + + +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v7.widget.PopupMenu; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import ca.pkay.rcloneexplorer.Database.DatabaseHandler; +import ca.pkay.rcloneexplorer.Database.Task; +import ca.pkay.rcloneexplorer.Dialogs.TaskDialog; +import ca.pkay.rcloneexplorer.Items.RemoteItem; +import ca.pkay.rcloneexplorer.Items.SyncDirectionObject; +import ca.pkay.rcloneexplorer.R; +import ca.pkay.rcloneexplorer.Services.SyncService; +import ca.pkay.rcloneexplorer.Services.TaskStartService; +import es.dmoral.toasty.Toasty; + +public class TasksRecyclerViewAdapter extends RecyclerView.Adapter{ + + + private static String clipboardID="rclone_explorer_task_id"; + + private List tasks; + private View view; + private Context context; + + + public TasksRecyclerViewAdapter(List tasks, Context context) { + this.tasks = tasks; + this.context = context; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_tasks_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { + final Task selectedTask = tasks.get(position); + String remoteName = selectedTask.getTitle(); + + holder.taskName.setText(remoteName); + + RemoteItem remote = new RemoteItem(selectedTask.getRemote_id(), String.valueOf(selectedTask.getRemote_type())); + + holder.taskIcon.setImageDrawable(view.getResources().getDrawable(remote.getRemoteIcon())); + + int direction = selectedTask.getDirection(); + + if(direction == SyncDirectionObject.SYNC_LOCAL_TO_REMOTE || direction == SyncDirectionObject.COPY_LOCAL_TO_REMOTE){ + holder.fromID.setVisibility(View.GONE); + holder.fromPath.setText(selectedTask.getLocal_path()); + + holder.toID.setText(String.format("@%s", selectedTask.getRemote_id())); + holder.toPath.setText(selectedTask.getRemote_path()); + } + + if(direction == SyncDirectionObject.SYNC_REMOTE_TO_LOCAL || direction == SyncDirectionObject.COPY_REMOTE_TO_LOCAL){ + holder.fromID.setText(String.format("@%s", selectedTask.getRemote_id())); + holder.fromPath.setText(selectedTask.getRemote_path()); + + holder.toID.setVisibility(View.GONE); + holder.toPath.setText(selectedTask.getLocal_path()); + } + + switch (direction){ + case SyncDirectionObject.SYNC_REMOTE_TO_LOCAL: holder.taskSyncDirection.setText(view.getResources().getString(R.string.sync)); break; + case SyncDirectionObject.COPY_LOCAL_TO_REMOTE: holder.taskSyncDirection.setText(view.getResources().getString(R.string.copy)); break; + case SyncDirectionObject.COPY_REMOTE_TO_LOCAL: holder.taskSyncDirection.setText(view.getResources().getString(R.string.copy)); break; + default: holder.taskSyncDirection.setText(view.getResources().getString(R.string.sync)); break; + } + + holder.fileOptions.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showFileMenu(v, selectedTask); + } + }); + + } + + public void addTask(Task data) { + tasks.add(data); + notifyDataSetChanged(); + } + + public void setList(ArrayList data) { + tasks=data; + notifyDataSetChanged(); + } + + private void startTask(Task task){ + String path = task.getLocal_path(); + RemoteItem ri = new RemoteItem(task.getRemote_id(), task.getRemote_type(), ""); + Intent intent = new Intent(context, SyncService.class); + intent.putExtra(SyncService.REMOTE_ARG, ri); + intent.putExtra(SyncService.LOCAL_PATH_ARG, path); + intent.putExtra(SyncService.SYNC_DIRECTION_ARG, task.getDirection()); + intent.putExtra(SyncService.REMOTE_PATH_ARG, task.getRemote_path()); + context.startService(intent); + } + + private void editTask(Task task){ + new TaskDialog(context, this, task).show(); + } + + + public void removeItem(Task task) { + int index = tasks.indexOf(task); + if (index >= 0) { + tasks.remove(index); + notifyItemRemoved(index); + } + } + + @Override + public int getItemCount() { + if (tasks == null) { + return 0; + } + return tasks.size(); + } + + private void showFileMenu(View view, final Task task) { + PopupMenu popupMenu = new PopupMenu(context, view); + popupMenu.getMenuInflater().inflate(R.menu.task_item_menu, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_start_task: + startTask(task); + break; + case R.id.action_edit_task: + editTask(task); + break; + case R.id.action_delete_task: + new DatabaseHandler(context).deleteEntry(task.getId()); + notifyDataSetChanged(); + removeItem(task); + break; + case R.id.action_copy_id_task: + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(clipboardID, task.getId().toString()); + clipboard.setPrimaryClip(clip); + Toasty.info(context, context.getResources().getString(R.string.task_copied_id_to_clipboard), Toast.LENGTH_SHORT, true).show(); + break; + case R.id.action_add_to_home_screen: + createShortcut(context, task); + break; + default: + return false; + } + return true; + } + }); + popupMenu.show(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + final View view; + final ImageView taskIcon; + final TextView taskName; + final TextView toID; + final TextView fromID; + final TextView toPath; + final TextView fromPath; + final ImageButton fileOptions; + final TextView taskSyncDirection; + + ViewHolder(View itemView) { + super(itemView); + this.view = itemView; + this.taskIcon = view.findViewById(R.id.taskIcon); + this.taskName = view.findViewById(R.id.taskName); + this.toID = view.findViewById(R.id.toID); + this.fromID = view.findViewById(R.id.fromID); + this.toPath = view.findViewById(R.id.toPath); + this.fromPath = view.findViewById(R.id.fromPath); + this.taskSyncDirection = view.findViewById(R.id.task_sync_direction); + + this.fileOptions = view.findViewById(R.id.file_options); + } + } + + private static void createShortcut(Context c, Task t) { + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ShortcutManager shortcutManager = c.getSystemService(ShortcutManager.class); + + + Intent i = new Intent(c, TaskStartService.class); + i.putExtra("task", t.getId()); + i.setAction(TaskStartService.TASK_ACTION); + + ShortcutInfo shortcut = new ShortcutInfo.Builder(c, String.valueOf(t.getId())) + .setShortLabel(t.getTitle()) + .setLongLabel(t.getRemote_path()) + .setIcon(Icon.createWithResource(c, R.mipmap.ic_launcher)) + .setIntent(i) + .build(); + + shortcutManager.setDynamicShortcuts(Arrays.asList(shortcut)); + + if (shortcutManager.isRequestPinShortcutSupported()) { + // Assumes there's already a shortcut with the ID "my-shortcut". + // The shortcut must be enabled. + + // Create the PendingIntent object only if your app needs to be notified + // that the user allowed the shortcut to be pinned. Note that, if the + // pinning operation fails, your app isn't notified. We assume here that the + // app has implemented a method called createShortcutResultIntent() that + // returns a broadcast intent. + Intent pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(shortcut); + + // Configure the intent so that your app's broadcast receiver gets + // the callback successfully.For details, see PendingIntent.getBroadcast(). + PendingIntent successCallback = PendingIntent.getBroadcast(c, 0, pinnedShortcutCallbackIntent, 0); + + shortcutManager.requestPinShortcut(shortcut, successCallback.getIntentSender()); + } + + } + } + +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseIdService.java b/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseIdService.java index be81be79..81c0af7b 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseIdService.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseIdService.java @@ -2,5 +2,5 @@ import com.google.firebase.iid.FirebaseInstanceIdService; -public class FirebaseIdService extends FirebaseInstanceIdService { +public class FirebaseIdService extends FirebaseInstanceIdService{ } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseMessagingService.java b/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseMessagingService.java index 58eb1448..e2c792c2 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseMessagingService.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Services/FirebaseMessagingService.java @@ -11,7 +11,6 @@ import android.support.v4.app.NotificationManagerCompat; import com.google.firebase.messaging.RemoteMessage; - import ca.pkay.rcloneexplorer.R; public class FirebaseMessagingService extends com.google.firebase.messaging.FirebaseMessagingService { diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Services/SyncService.java b/app/src/main/java/ca/pkay/rcloneexplorer/Services/SyncService.java index bb6c022a..9dffc5cd 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Services/SyncService.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Services/SyncService.java @@ -18,7 +18,6 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.content.LocalBroadcastManager; -import android.util.Log; import java.io.BufferedReader; import java.io.IOException; @@ -36,11 +35,14 @@ public class SyncService extends IntentService { public static final String REMOTE_PATH_ARG = "ca.pkay.rcexplorer.SYNC_SERVICE_REMOTE_PATH_ARG"; public static final String LOCAL_PATH_ARG = "ca.pkay.rcexplorer.SYNC_LOCAL_PATH_ARG"; public static final String SYNC_DIRECTION_ARG = "ca.pkay.rcexplorer.SYNC_DIRECTION_ARG"; + public static final String SHOW_RESULT_NOTIFICATION = "ca.pkay.rcexplorer.SHOW_RESULT_NOTIFICATION"; private final String OPERATION_FAILED_GROUP = "ca.pkay.rcexplorer.OPERATION_FAILED_GROUP"; + private final String OPERATION_SUCCESS_GROUP = "ca.pkay.rcexplorer.OPERATION_SUCCESS_GROUP"; private final String CHANNEL_ID = "ca.pkay.rcexplorer.sync_service"; private final String CHANNEL_NAME = "Sync service"; private final int PERSISTENT_NOTIFICATION_ID_FOR_SYNC = 162; private final int OPERATION_FAILED_NOTIFICATION_ID = 89; + private final int OPERATION_SUCCESS_NOTIFICATION_ID = 698; private final int CONNECTIVITY_CHANGE_NOTIFICATION_ID = 462; private Rclone rclone; private Log2File log2File; @@ -84,6 +86,8 @@ protected void onHandleIntent(@Nullable Intent intent) { final String localPath = intent.getStringExtra(LOCAL_PATH_ARG); final int syncDirection = intent.getIntExtra(SYNC_DIRECTION_ARG, 1); + final boolean silentRun = intent.getBooleanExtra(SHOW_RESULT_NOTIFICATION, true); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); Boolean isLoggingEnable = sharedPreferences.getBoolean(getString(R.string.pref_key_logs), false); @@ -150,13 +154,19 @@ protected void onHandleIntent(@Nullable Intent intent) { sendUploadFinishedBroadcast(remoteItem.getName(), remotePath); - if (transferOnWiFiOnly && connectivityChanged) { + int notificationId = (int)System.currentTimeMillis(); + + if(silentRun){ + if (transferOnWiFiOnly && connectivityChanged) { showConnectivityChangedNotification(); } else if (currentProcess == null || currentProcess.exitValue() != 0) { - String errorTitle = "Sync operation failed"; - int notificationId = (int)System.currentTimeMillis(); - showFailedNotification(errorTitle, title, notificationId); + String errorTitle = getString(R.string.notification_sync_failed); + showFailedNotification(errorTitle, title, notificationId); + }else{ + showSuccessNotification(title, notificationId); } + } + NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(this); notificationManagerCompat.cancel(PERSISTENT_NOTIFICATION_ID_FOR_SYNC); @@ -247,7 +257,7 @@ private void showFailedNotification(String title, String content, int notificati createSummaryNotificationForFailed(); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(android.R.drawable.stat_sys_warning) + .setSmallIcon(R.drawable.ic_notification_error) .setContentTitle(title) .setContentText(content) .setGroup(OPERATION_FAILED_GROUP) @@ -258,6 +268,22 @@ private void showFailedNotification(String title, String content, int notificati } + private void showSuccessNotification(String content, int notificationId) { + createSummaryNotificationForSuccess(); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_success) + .setContentTitle(getString(R.string.operation_success)) + .setContentText(content) + .setGroup(OPERATION_SUCCESS_GROUP) + .setPriority(NotificationCompat.PRIORITY_LOW); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(notificationId, builder.build()); + + } + + private void showConnectivityChangedNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(android.R.drawable.stat_sys_warning) @@ -275,7 +301,7 @@ private void createSummaryNotificationForFailed() { .setContentTitle(getString(R.string.operation_failed)) //set content text to support devices running API level < 24 .setContentText(getString(R.string.operation_failed)) - .setSmallIcon(android.R.drawable.stat_sys_warning) + .setSmallIcon(R.drawable.ic_notification_error) .setGroup(OPERATION_FAILED_GROUP) .setGroupSummary(true) .setAutoCancel(true) @@ -285,6 +311,22 @@ private void createSummaryNotificationForFailed() { notificationManager.notify(OPERATION_FAILED_NOTIFICATION_ID, summaryNotification); } + private void createSummaryNotificationForSuccess() { + Notification summaryNotification = + new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.operation_success)) + //set content text to support devices running API level < 24 + .setContentText(getString(R.string.operation_success)) + .setSmallIcon(R.drawable.ic_notification_success) + .setGroup(OPERATION_SUCCESS_GROUP) + .setGroupSummary(true) + .setAutoCancel(true) + .build(); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(OPERATION_SUCCESS_NOTIFICATION_ID, summaryNotification); + } + private void setNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Create the NotificationChannel, but only on API 26+ because diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Services/TaskStartService.java b/app/src/main/java/ca/pkay/rcloneexplorer/Services/TaskStartService.java new file mode 100644 index 00000000..0b7f404b --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Services/TaskStartService.java @@ -0,0 +1,91 @@ +package ca.pkay.rcloneexplorer.Services; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Intent; +import android.content.Context; +import android.os.Build; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import ca.pkay.rcloneexplorer.Database.DatabaseHandler; +import ca.pkay.rcloneexplorer.Database.Task; +import ca.pkay.rcloneexplorer.Items.RemoteItem; + + +/** + * An {@link IntentService} subclass for handling asynchronous task requests in + * a service on a separate handler thread. + *

+ * TODO: Customize class - update intent actions, extra parameters and static + * helper methods. + */ +public class TaskStartService extends IntentService { + + public static String TASK_ACTION= "START_TASK"; + private static String EXTRA_TASK_ID= "task"; + private static String EXTRA_TASK_SILENT= "notification"; + + public TaskStartService() { + super("TaskStartService"); + Log.e("Service", "Start service intent!"); + } + + @Override + public void onCreate() { + super.onCreate(); + createPersistentNotification(); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent != null) { + final String action = intent.getAction(); + if (action.equals(TASK_ACTION)) { + DatabaseHandler db = new DatabaseHandler(this); + for (Task task: db.getAllTasks()){ + if(task.getId()==intent.getIntExtra(EXTRA_TASK_ID, -1)){ + String path = task.getLocal_path(); + + boolean silentRun =intent.getBooleanExtra(EXTRA_TASK_SILENT, true); + + RemoteItem remoteItem = new RemoteItem(task.getRemote_id(), task.getRemote_type(), ""); + Intent taskIntent = new Intent(); + taskIntent.setClass(this.getApplicationContext(), ca.pkay.rcloneexplorer.Services.SyncService.class); + + taskIntent.putExtra(SyncService.REMOTE_ARG, remoteItem); + taskIntent.putExtra(SyncService.LOCAL_PATH_ARG, path); + taskIntent.putExtra(SyncService.SYNC_DIRECTION_ARG, task.getDirection()); + taskIntent.putExtra(SyncService.REMOTE_PATH_ARG, task.getRemote_path()); + taskIntent.putExtra(SyncService.SHOW_RESULT_NOTIFICATION, silentRun); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(taskIntent); + }else { + startService(taskIntent); + } + } + } + } + } + } + + /** + * This can be called when an intent is recieved. If no notification is created when a service is started via startForegroundService(), the service is beeing killed by + * android after 5 seconds. In this case, we need to create a persistent notification because otherwise we cant start the sync task. + */ + private void createPersistentNotification() { + if (Build.VERSION.SDK_INT >= 26) { + String CHANNEL_ID = "task_intent_notification"; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel for intent notifications", NotificationManager.IMPORTANCE_DEFAULT); + + ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel); + + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle("").setContentText("").build(); + + startForeground(1, notification); + } + } + +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/NotificationsSettingsFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/NotificationsSettingsFragment.java index 09ef9d86..d85c8d3b 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/NotificationsSettingsFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/NotificationsSettingsFragment.java @@ -16,7 +16,6 @@ import android.widget.Toast; import com.google.firebase.messaging.FirebaseMessaging; - import ca.pkay.rcloneexplorer.R; import es.dmoral.toasty.Toasty; @@ -171,7 +170,6 @@ private void onBetaAppUpdatesClicked(boolean isChecked) { } else { FirebaseMessaging.getInstance().unsubscribeFromTopic(getString(R.string.firebase_msg_beta_app_updates_topic)); } - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putBoolean(getString(R.string.pref_key_app_updates_beta), isChecked); diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_error.png b/app/src/main/res/drawable-xxhdpi/ic_notification_error.png new file mode 100644 index 00000000..4efe14a4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_error.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_success.png b/app/src/main/res/drawable-xxhdpi/ic_notification_success.png new file mode 100644 index 00000000..c231de91 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_success.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_sync.png b/app/src/main/res/drawable-xxhdpi/ic_notification_sync.png new file mode 100644 index 00000000..57d19b5e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_sync.png differ diff --git a/app/src/main/res/drawable/ic_cloud_download_black_24dp.xml b/app/src/main/res/drawable/ic_cloud_download_black_24dp.xml new file mode 100644 index 00000000..261c3121 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_download_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_black.xml b/app/src/main/res/drawable/ic_delete_black.xml new file mode 100644 index 00000000..125a58f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_black.xml b/app/src/main/res/drawable/ic_edit_black.xml new file mode 100644 index 00000000..28787a15 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_tasks.xml b/app/src/main/res/layout/fragment_tasks.xml new file mode 100644 index 00000000..8ca9ce29 --- /dev/null +++ b/app/src/main/res/layout/fragment_tasks.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tasks_item.xml b/app/src/main/res/layout/fragment_tasks_item.xml new file mode 100644 index 00000000..71832abb --- /dev/null +++ b/app/src/main/res/layout/fragment_tasks_item.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/task_dialog.xml b/app/src/main/res/layout/task_dialog.xml new file mode 100644 index 00000000..eec84981 --- /dev/null +++ b/app/src/main/res/layout/task_dialog.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +