diff --git a/.gitignore b/.gitignore
index af31524e..6b04ae92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@
/build
/captures
.externalNativeBuild
-*.apk
\ No newline at end of file
+*.apk
+*.exe
\ No newline at end of file
diff --git a/.idea/dictionaries/Stardust.xml b/.idea/dictionaries/Stardust.xml
index 8244557f..792fd648 100644
--- a/.idea/dictionaries/Stardust.xml
+++ b/.idea/dictionaries/Stardust.xml
@@ -1,8 +1,10 @@
+ dismissable
interruptible
scriptable
+ tasker
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 935de354..c44fe40c 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -2,13 +2,14 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 91d4b6ff..8a664f4d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -63,7 +63,7 @@ dependencies {
compile 'moe.feng:AlipayZeroSdk:1.1'
compile 'com.jraska:console:0.4.3'
compile 'com.jraska:console-timber-tree:0.4.3'
- compile 'com.heinrichreimersoftware:android-issue-reporter:eae4c73cbeb91e98da83fe79a3dea088a8b97448'
+ compile 'com.heinrichreimersoftware:android-issue-reporter:1.3.1'
compile 'de.codecrafters.tableview:tableview:2.5.0'
compile group: "pl.openrnd.android", name: "multi-level-listview", version: "1.0.1"
compile 'de.psdev.licensesdialog:licensesdialog:1.8.1'
@@ -72,6 +72,7 @@ dependencies {
compile 'com.bignerdranch.android:expandablerecyclerview:3.0.0-RC1'
compile 'com.ashokvarma.android:bottom-navigation-bar:1.4.1'
compile 'com.github.hyb1996:node-android-lib:1.0.13'
+ compile 'com.yqritc:recyclerview-flexibledivider:1.4.0'
compile 'me.zhanghai.android.materialprogressbar:library:1.3.0'
compile group:'com.twofortyfouram', name:'android-plugin-client-sdk-for-locale', version:'[4.0.2, 5.0['
compile(name: 'libtermexec-release', ext: 'aar')
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c3c5c2f8..e5e601a1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -77,8 +77,8 @@
+ android:name=".ui.error.IssueReporterActivity"
+ android:theme="@style/IssueReporterTheme"/>
diff --git a/app/src/main/java/com/stardust/scriptdroid/Pref.java b/app/src/main/java/com/stardust/scriptdroid/Pref.java
index 08016160..6c0363cd 100644
--- a/app/src/main/java/com/stardust/scriptdroid/Pref.java
+++ b/app/src/main/java/com/stardust/scriptdroid/Pref.java
@@ -13,8 +13,6 @@ import com.stardust.scriptdroid.autojs.AutoJs;
public class Pref {
private static final SharedPreferences DISPOSABLE_BOOLEAN = App.getApp().getSharedPreferences("DISPOSABLE_BOOLEAN", Context.MODE_PRIVATE);
- public static final String KEY_DRAWER_HEADER_IMAGE_PATH = "KEY_DRAWER_HEADER_IMAGE_PATH";
- public static final String KEY_APP_BAR_IMAGE_PATH = "KEY_APP_BAR_IMAGE_PATH";
private static SharedPreferences.OnSharedPreferenceChangeListener onSharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
@@ -84,22 +82,6 @@ public class Pref {
return getDisposableBoolean("isFirstUsing", true);
}
- public static String getDrawerHeaderImagePath() {
- return def().getString(KEY_DRAWER_HEADER_IMAGE_PATH, null);
- }
-
- public static void setDrawerHeaderImagePath(String path) {
- def().edit().putString(KEY_DRAWER_HEADER_IMAGE_PATH, path).apply();
- }
-
- public static String getAppBarImagePath() {
- return def().getString(KEY_APP_BAR_IMAGE_PATH, null);
- }
-
- public static void setAppBarImagePath(String path) {
- def().edit().putString(KEY_APP_BAR_IMAGE_PATH, path).apply();
- }
-
static {
def().registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/CommonUtils.java b/app/src/main/java/com/stardust/scriptdroid/external/CommonUtils.java
index 8cd456ee..829dd885 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/CommonUtils.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/CommonUtils.java
@@ -1,5 +1,7 @@
package com.stardust.scriptdroid.external;
+import android.os.Bundle;
+
/**
* Created by Stardust on 2017/4/1.
*/
@@ -10,5 +12,8 @@ public class CommonUtils {
public static final String EXTRA_KEY_PREPARE_SCRIPT = "script";
+ public static boolean isTaskerBundleValid(Bundle bundle) {
+ return bundle.containsKey(CommonUtils.EXTRA_KEY_PATH);
+ }
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/tasker/FireSettingReceiver.java b/app/src/main/java/com/stardust/scriptdroid/external/tasker/FireSettingReceiver.java
index 8267ed32..16723242 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/tasker/FireSettingReceiver.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/tasker/FireSettingReceiver.java
@@ -23,19 +23,16 @@ public class FireSettingReceiver extends AbstractPluginSettingReceiver {
@Override
protected boolean isBundleValid(@NonNull Bundle bundle) {
- Log.v(TAG, "isBundleValid: " + bundle);
- return bundle.containsKey(CommonUtils.EXTRA_KEY_PATH);
+ return CommonUtils.isTaskerBundleValid(bundle);
}
@Override
protected boolean isAsync() {
- Log.v(TAG, "isAsync");
return true;
}
@Override
protected void firePluginSetting(@NonNull Context context, @NonNull Bundle bundle) {
- Log.v(TAG, "firePluginSetting:" + bundle);
context.startActivity(new Intent(App.getApp(), RunIntentActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(CommonUtils.EXTRA_KEY_PATH, bundle.getString(CommonUtils.EXTRA_KEY_PATH))
diff --git a/app/src/main/java/com/stardust/scriptdroid/external/tasker/TaskPrefEditActivity.java b/app/src/main/java/com/stardust/scriptdroid/external/tasker/TaskPrefEditActivity.java
index 4b4b19c9..37ad1217 100644
--- a/app/src/main/java/com/stardust/scriptdroid/external/tasker/TaskPrefEditActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/external/tasker/TaskPrefEditActivity.java
@@ -9,7 +9,8 @@ import android.util.Log;
import com.stardust.scriptdroid.scripts.ScriptFile;
import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.external.CommonUtils;
-import com.stardust.scriptdroid.ui.main.my_script_list.ScriptAndFolderListRecyclerView;
+import com.stardust.scriptdroid.ui.main.script_list.ScriptAndFolderListRecyclerView;
+import com.stardust.scriptdroid.ui.main.script_list.ScriptListWithProgressBarView;
import com.twofortyfouram.locale.sdk.client.ui.activity.AbstractPluginActivity;
/**
@@ -18,20 +19,19 @@ import com.twofortyfouram.locale.sdk.client.ui.activity.AbstractPluginActivity;
public class TaskPrefEditActivity extends AbstractPluginActivity {
- private static final String TAG = "TaskPrefEditActivity";
private String mSelectedScriptFilePath;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- Log.v(TAG, "onCreate");
setContentView(R.layout.activity_tasker_edit);
initScriptListRecyclerView();
}
private void initScriptListRecyclerView() {
- ScriptAndFolderListRecyclerView scriptListRecyclerView = (ScriptAndFolderListRecyclerView) findViewById(R.id.script_list);
- scriptListRecyclerView.setOnItemClickListener(new ScriptAndFolderListRecyclerView.OnScriptFileClickListener() {
+ ScriptListWithProgressBarView scriptListWithProgressBarView = (ScriptListWithProgressBarView) findViewById(R.id.script_list);
+ scriptListWithProgressBarView.setScriptFileOperationEnabled(false);
+ scriptListWithProgressBarView.setOnItemClickListener(new ScriptAndFolderListRecyclerView.OnScriptFileClickListener() {
@Override
public void onClick(ScriptFile file) {
mSelectedScriptFilePath = file.getPath();
@@ -43,22 +43,17 @@ public class TaskPrefEditActivity extends AbstractPluginActivity {
@Override
public boolean isBundleValid(@NonNull Bundle bundle) {
- boolean valid = bundle.getString(CommonUtils.EXTRA_KEY_PATH) != null;
- Log.v(TAG, "isBundleValid: " + valid);
- Context context;
- return valid;
+ return CommonUtils.isTaskerBundleValid(bundle);
}
@Override
public void onPostCreateWithPreviousResult(@NonNull Bundle bundle, @NonNull String s) {
- Log.v(TAG, "onPostCreateWithPreviousResult: bundle=" + bundle + " str=" + s);
mSelectedScriptFilePath = bundle.getString(CommonUtils.EXTRA_KEY_PATH);
}
@Nullable
@Override
public Bundle getResultBundle() {
- Log.v(TAG, "getResultBundle");
Bundle bundle = new Bundle();
bundle.putString(CommonUtils.EXTRA_KEY_PATH, mSelectedScriptFilePath);
return bundle;
@@ -67,7 +62,6 @@ public class TaskPrefEditActivity extends AbstractPluginActivity {
@NonNull
@Override
public String getResultBlurb(@NonNull Bundle bundle) {
- Log.v(TAG, "getResultBlurb");
return bundle.getString(CommonUtils.EXTRA_KEY_PATH, getString(R.string.text_path_is_empty));
}
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutHierarchyView.java b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutHierarchyView.java
index fad16d98..343e68ff 100644
--- a/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutHierarchyView.java
+++ b/app/src/main/java/com/stardust/scriptdroid/layout_inspector/view/LayoutHierarchyView.java
@@ -178,7 +178,7 @@ public class LayoutHierarchyView extends MultiLevelListView {
NodeInfo nodeInfo = (NodeInfo) object;
ViewHolder viewHolder;
if (convertView == null) {
- convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_hierarchy_view_item, null);
+ convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_hierarchy_view_item, LayoutHierarchyView.this, false);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
} else {
diff --git a/app/src/main/java/com/stardust/scriptdroid/scripts/StorageScriptProvider.java b/app/src/main/java/com/stardust/scriptdroid/scripts/StorageScriptProvider.java
index 1cde9310..011be748 100644
--- a/app/src/main/java/com/stardust/scriptdroid/scripts/StorageScriptProvider.java
+++ b/app/src/main/java/com/stardust/scriptdroid/scripts/StorageScriptProvider.java
@@ -6,9 +6,12 @@ import com.stardust.scriptdroid.App;
import com.stardust.scriptdroid.R;
import com.stardust.util.FileSorter;
import com.stardust.util.LimitedHashMap;
+import com.stardust.util.MapEntries;
import org.greenrobot.eventbus.EventBus;
+import java.util.Map;
+
/**
* Created by Stardust on 2017/3/31.
*/
@@ -29,11 +32,19 @@ public class StorageScriptProvider {
public static final ScriptFile DEFAULT_DIRECTORY = new ScriptFile(DEFAULT_DIRECTORY_PATH);
-
- private static StorageScriptProvider instance = new StorageScriptProvider();
+ private static StorageScriptProvider defaultProvider = new StorageScriptProvider();
public static StorageScriptProvider getDefault() {
- return instance;
+ return defaultProvider;
+ }
+
+ private static StorageScriptProvider externalStorageProvider;
+
+ public static StorageScriptProvider getExternalStorageProvider() {
+ if (externalStorageProvider == null) {
+ externalStorageProvider = new StorageScriptProvider(Environment.getExternalStorageDirectory().getPath(), 5);
+ }
+ return externalStorageProvider;
}
private EventBus mDirectoryEventBus = new EventBus();
@@ -47,7 +58,6 @@ public class StorageScriptProvider {
public StorageScriptProvider(ScriptFile initialDirectory, int cacheSize) {
mInitialDirectory = initialDirectory;
- mInitialDirectoryScriptFiles = getInitialDirectoryScriptFilesInner();
mScriptFileCache = new LimitedHashMap<>(cacheSize);
}
@@ -57,7 +67,7 @@ public class StorageScriptProvider {
public void notifyDirectoryChanged(ScriptFile directory) {
if (directory.equals(mInitialDirectory)) {
- mInitialDirectoryScriptFiles = getInitialDirectoryScriptFilesInner();
+ mInitialDirectoryScriptFiles = getInitialDirectoryScriptFiles();
} else {
clearCache(directory);
}
@@ -66,15 +76,28 @@ public class StorageScriptProvider {
public void notifyStoragePermissionGranted() {
mScriptFileCache.clear();
- mInitialDirectoryScriptFiles = getInitialDirectoryScriptFilesInner();
+ mInitialDirectoryScriptFiles = null;
mDirectoryEventBus.post(new DirectoryChangeEvent(mInitialDirectory));
}
+ public void refreshAll() {
+ Map files = (Map) mScriptFileCache.clone();
+ mScriptFileCache.clear();
+ mInitialDirectoryScriptFiles = null;
+ mDirectoryEventBus.post(new DirectoryChangeEvent(mInitialDirectory));
+ for (Map.Entry file : files.entrySet()) {
+ mDirectoryEventBus.post(new DirectoryChangeEvent(new ScriptFile(file.getKey())));
+ }
+ }
+
public ScriptFile getInitialDirectory() {
return mInitialDirectory;
}
public ScriptFile[] getInitialDirectoryScriptFiles() {
+ if (mInitialDirectoryScriptFiles == null) {
+ mInitialDirectoryScriptFiles = getInitialDirectoryScriptFilesInner();
+ }
return mInitialDirectoryScriptFiles;
}
@@ -84,7 +107,7 @@ public class StorageScriptProvider {
public ScriptFile[] getDirectoryScriptFiles(ScriptFile directory) {
if (directory.equals(mInitialDirectory)) {
- return mInitialDirectoryScriptFiles;
+ return getInitialDirectoryScriptFiles();
}
ScriptFile[] scriptFiles = getScriptFilesFromCache(directory);
if (scriptFiles == null) {
@@ -93,6 +116,10 @@ public class StorageScriptProvider {
return scriptFiles;
}
+ public void clearCacheExceptInitialDirectory() {
+ mScriptFileCache.clear();
+ }
+
private void clearCache(ScriptFile directory) {
mScriptFileCache.remove(directory.getPath());
}
@@ -119,7 +146,8 @@ public class StorageScriptProvider {
public void registerDirectoryChangeListener(Object subscriber) {
- mDirectoryEventBus.register(subscriber);
+ if (!mDirectoryEventBus.isRegistered(subscriber))
+ mDirectoryEventBus.register(subscriber);
}
public void unregisterDirectoryChangeListener(Object subscriber) {
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/DrawableSaver.java b/app/src/main/java/com/stardust/scriptdroid/tool/DrawableSaver.java
new file mode 100644
index 00000000..54f69067
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/DrawableSaver.java
@@ -0,0 +1,141 @@
+package com.stardust.scriptdroid.tool;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.stardust.app.OnActivityResultDelegate;
+import com.stardust.pio.PFile;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.Callable;
+
+/**
+ * Created by Stardust on 2017/4/3.
+ */
+
+public abstract class DrawableSaver {
+
+ private static final String PREFIX = "saved_drawable_";
+
+ protected Drawable mOriginalDrawble;
+ private Context mContext;
+ private String mName;
+
+ public DrawableSaver(Context context, String name, Drawable originalDrawble) {
+ mContext = context;
+ mName = PREFIX + name;
+ mOriginalDrawble = originalDrawble;
+ readImageAndApply();
+ }
+
+ private void readImageAndApply() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ InputStream inputStream = mContext.openFileInput(mName);
+ Drawable drawable = BitmapDrawable.createFromStream(inputStream, mName);
+ applyDrawableToView(drawable);
+ } catch (FileNotFoundException e) {
+
+ }
+ }
+ }).start();
+ }
+
+ public void setDrawable(InputStream inputStream) {
+ saveDrawable(inputStream, new Runnable() {
+ @Override
+ public void run() {
+ readImageAndApply();
+ }
+ });
+ }
+
+ private void saveDrawable(final InputStream inputStream, final Runnable callback) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ OutputStream outputStream = mContext.openFileOutput(mName, Context.MODE_PRIVATE);
+ PFile.write(inputStream, outputStream);
+ outputStream.close();
+ inputStream.close();
+ callback.run();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+
+ }
+
+ public void select(Activity activity, final OnActivityResultDelegate.Intermediary intermediary) {
+ new ImageSelector(activity, intermediary, new ImageSelector.ImageSelectorCallback() {
+ @Override
+ public void onImageSelected(ImageSelector selector, InputStream inputStream) {
+ if (inputStream != null)
+ setDrawable(inputStream);
+ intermediary.removeDelegate(selector);
+ }
+ }).select();
+ }
+
+ public void reset() {
+ applyDrawableToView(mOriginalDrawble);
+ mContext.deleteFile(mName);
+ }
+
+ public static void reset(Context context, String name) {
+ context.deleteFile(PREFIX + name);
+ }
+
+ protected abstract void applyDrawableToView(Drawable drawable);
+
+ public static class ViewBackgroundSaver extends DrawableSaver {
+
+ private View mView;
+
+ public ViewBackgroundSaver(Context activity, String name, View view) {
+ super(activity, name, view.getBackground());
+ mView = view;
+ }
+
+ @Override
+ protected void applyDrawableToView(final Drawable drawable) {
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.setBackground(drawable);
+ }
+ });
+ }
+ }
+
+ public static class ImageSaver extends DrawableSaver {
+
+ private ImageView mView;
+
+ public ImageSaver(Context activity, String name, ImageView view) {
+ super(activity, name, view.getDrawable());
+ mView = view;
+ }
+
+ @Override
+ protected void applyDrawableToView(final Drawable drawable) {
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.setImageDrawable(drawable);
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java b/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java
index e41030d2..cf5347fc 100644
--- a/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/ImageSelector.java
@@ -9,6 +9,9 @@ import android.provider.MediaStore;
import com.stardust.app.OnActivityResultDelegate;
import com.stardust.scriptdroid.R;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
/**
* Created by Stardust on 2017/3/5.
*/
@@ -16,7 +19,7 @@ import com.stardust.scriptdroid.R;
public class ImageSelector implements OnActivityResultDelegate {
public interface ImageSelectorCallback {
- void onImageSelected(ImageSelector selector, String path);
+ void onImageSelected(ImageSelector selector, InputStream path);
}
private static final int REQUEST_CODE = "LOVE EATING".hashCode() >> 16;
@@ -38,19 +41,16 @@ public class ImageSelector implements OnActivityResultDelegate {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (data == null)
+ if (data == null) {
+ mCallback.onImageSelected(this, null);
return;
- Uri selectedImageUri = data.getData();
- String[] filePathColumn = {MediaStore.Images.Media.DATA};
- Cursor cursor = mActivity.getContentResolver().query(selectedImageUri, filePathColumn, null, null, null);
- if (cursor != null) {
- cursor.moveToFirst();
- int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
- String filePath = cursor.getString(columnIndex);
- cursor.close();
- mCallback.onImageSelected(this, filePath);
}
-
+ try {
+ InputStream inputStream = mActivity.getContentResolver().openInputStream(data.getData());
+ mCallback.onImageSelected(this, inputStream);
+ } catch (FileNotFoundException e) {
+ mCallback.onImageSelected(this, null);
+ }
}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java
index 068b9818..ca302f32 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/BaseActivity.java
@@ -24,7 +24,7 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
* Created by Stardust on 2017/1/23.
*/
-public class BaseActivity extends AppCompatActivity {
+public abstract class BaseActivity extends AppCompatActivity {
protected static final int PERMISSION_REQUEST_CODE = 11186;
private BackPressedHandler.Observer mBackPressObserver = new BackPressedHandler.Observer();
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/edit/editor920/Editor920Activity.java b/app/src/main/java/com/stardust/scriptdroid/ui/edit/editor920/Editor920Activity.java
index 214e4736..2b8443b7 100644
--- a/app/src/main/java/com/stardust/scriptdroid/ui/edit/editor920/Editor920Activity.java
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/edit/editor920/Editor920Activity.java
@@ -17,7 +17,7 @@ import java.util.Map;
/**
* Created by Stardust on 2017/1/30.
*/
-public class Editor920Activity extends IActivity {
+public abstract class Editor920Activity extends IActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/error/AbstractIssueReporterActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/error/AbstractIssueReporterActivity.java
new file mode 100644
index 00000000..bb0d8ed8
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/error/AbstractIssueReporterActivity.java
@@ -0,0 +1,642 @@
+package com.stardust.scriptdroid.ui.error;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringDef;
+import android.support.annotation.StringRes;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.TextInputEditText;
+import android.support.design.widget.TextInputLayout;
+import android.support.v4.app.NavUtils;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.afollestad.materialdialogs.DialogAction;
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.github.aakira.expandablelayout.ExpandableRelativeLayout;
+import com.heinrichreimersoftware.androidissuereporter.model.DeviceInfo;
+import com.heinrichreimersoftware.androidissuereporter.model.Report;
+import com.heinrichreimersoftware.androidissuereporter.model.github.ExtraInfo;
+import com.heinrichreimersoftware.androidissuereporter.model.github.GithubLogin;
+import com.heinrichreimersoftware.androidissuereporter.model.github.GithubTarget;
+import com.heinrichreimersoftware.androidissuereporter.util.ColorUtils;
+import com.heinrichreimersoftware.androidissuereporter.util.ThemeUtils;
+
+import org.eclipse.egit.github.core.Issue;
+import org.eclipse.egit.github.core.client.GitHubClient;
+import org.eclipse.egit.github.core.client.RequestException;
+import org.eclipse.egit.github.core.service.IssueService;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+
+import static android.util.Patterns.EMAIL_ADDRESS;
+
+import com.heinrichreimersoftware.androidissuereporter.R;
+import com.stardust.theme.ThemeColorManager;
+
+/**
+ * Created by Stardust on 2017/4/3.
+ */
+
+public abstract class AbstractIssueReporterActivity extends AppCompatActivity {
+
+ private static final String TAG = AbstractIssueReporterActivity.class.getSimpleName();
+
+ private static final int STATUS_BAD_CREDENTIALS = 401;
+ private static final int STATUS_ISSUES_NOT_ENABLED = 410;
+ private boolean mCrash = false;
+ private boolean mReportFailed = true;
+
+ @StringDef({RESULT_OK, RESULT_BAD_CREDENTIALS, RESULT_INVALID_TOKEN, RESULT_ISSUES_NOT_ENABLED,
+ RESULT_UNKNOWN})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface Result {
+ }
+
+ private static final String RESULT_OK = "RESULT_OK";
+ private static final String RESULT_BAD_CREDENTIALS = "RESULT_BAD_CREDENTIALS";
+ private static final String RESULT_INVALID_TOKEN = "RESULT_INVALID_TOKEN";
+ private static final String RESULT_ISSUES_NOT_ENABLED = "RESULT_ISSUES_NOT_ENABLED";
+ private static final String RESULT_UNKNOWN = "RESULT_UNKNOWN";
+ private boolean emailRequired = false;
+ private int bodyMinChar = 0;
+ private Toolbar toolbar;
+ private TextInputEditText inputTitle;
+ private TextInputEditText inputDescription;
+ private TextView textDeviceInfo;
+ private ImageButton buttonDeviceInfo;
+ private ExpandableRelativeLayout layoutDeviceInfo;
+ private ExpandableRelativeLayout layoutAnonymous;
+ private TextInputEditText inputUsername;
+ private TextInputEditText inputPassword;
+ private TextInputEditText inputEmail;
+ private RadioButton optionUseAccount;
+ private RadioButton optionAnonymous;
+ private ExpandableRelativeLayout layoutLogin;
+ private FloatingActionButton buttonSend;
+
+ private Drawable optionUseAccountButtonDrawable = null;
+
+ private String token;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (TextUtils.isEmpty(getTitle()))
+ setTitle(R.string.air_title_report_issue);
+
+ setContentView(com.stardust.scriptdroid.R.layout.air_activity_issue_reporter);
+ findViews();
+
+ //noinspection deprecation
+ token = getGuestToken();
+
+ initViews();
+
+
+ DeviceInfo deviceInfo = new DeviceInfo(this);
+ textDeviceInfo.setText(deviceInfo.toString());
+
+ handleIntent();
+
+ optionAnonymous.post(new Runnable() {
+ @Override
+ public void run() {
+ optionAnonymous.performClick();
+ }
+ });
+ }
+
+ private void handleIntent() {
+ final String errorDetail = getIntent().getStringExtra("error");
+ if (errorDetail != null) {
+ inputDescription.setText(errorDetail);
+ String title = getFirstLine(errorDetail);
+ inputTitle.setText(title);
+ mCrash = true;
+ }
+ }
+
+ private String getFirstLine(String str) {
+ int i = str.indexOf('\n');
+ if (i < 0)
+ return str;
+ return str.substring(0, i);
+ }
+
+ private void findViews() {
+ toolbar = (Toolbar) findViewById(R.id.air_toolbar);
+
+ inputTitle = (TextInputEditText) findViewById(R.id.air_inputTitle);
+ inputDescription = (TextInputEditText) findViewById(R.id.air_inputDescription);
+ textDeviceInfo = (TextView) findViewById(R.id.air_textDeviceInfo);
+ buttonDeviceInfo = (ImageButton) findViewById(R.id.air_buttonDeviceInfo);
+ layoutDeviceInfo = (ExpandableRelativeLayout) findViewById(R.id.air_layoutDeviceInfo);
+
+ inputUsername = (TextInputEditText) findViewById(R.id.air_inputUsername);
+ inputPassword = (TextInputEditText) findViewById(R.id.air_inputPassword);
+ inputEmail = (TextInputEditText) findViewById(R.id.air_inputEmail);
+ optionUseAccount = (RadioButton) findViewById(R.id.air_optionUseAccount);
+ optionAnonymous = (RadioButton) findViewById(R.id.air_optionAnonymous);
+ layoutLogin = (ExpandableRelativeLayout) findViewById(R.id.air_layoutLogin);
+ layoutAnonymous = (ExpandableRelativeLayout) findViewById(R.id.air_layoutGuest);
+
+ buttonSend = (FloatingActionButton) findViewById(R.id.air_buttonSend);
+ }
+
+ private void initViews() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ getWindow().setStatusBarColor(ThemeColorManager.getColorPrimary());
+ }
+ setSupportActionBar(toolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(com.stardust.scriptdroid.R.string.text_issue_report);
+ }
+ toolbar.setBackgroundColor(ThemeColorManager.getColorPrimary());
+
+
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ buttonDeviceInfo.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ layoutDeviceInfo.toggle();
+ }
+ });
+
+
+ inputPassword.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEND) {
+ reportIssue();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ updateGuestTokenViews();
+
+ buttonSend.setImageResource(ColorUtils.isDark(ThemeUtils.getColorAccent(this)) ?
+ R.drawable.air_ic_send_dark : R.drawable.air_ic_send_light);
+ buttonSend.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ reportIssue();
+ } catch (Exception e) {
+ e.printStackTrace();
+ mReportFailed = true;
+ finish();
+ }
+ }
+ });
+
+ }
+
+ private void setOptionUseAccountMarginStart(int marginStart) {
+ LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) optionUseAccount.getLayoutParams();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ layoutParams.setMarginStart(marginStart);
+ } else {
+ layoutParams.leftMargin = marginStart;
+ }
+ optionUseAccount.setLayoutParams(layoutParams);
+ }
+
+ private void updateGuestTokenViews() {
+ if (TextUtils.isEmpty(token)) {
+ int baseline = getResources().getDimensionPixelSize(R.dimen.air_baseline);
+ int radioButtonPaddingStart = getResources().getDimensionPixelSize(R.dimen.air_radio_button_padding_start);
+ setOptionUseAccountMarginStart(-2 * baseline - radioButtonPaddingStart);
+ optionUseAccount.setEnabled(false);
+ optionAnonymous.setVisibility(View.GONE);
+ } else {
+ setOptionUseAccountMarginStart(0);
+ optionUseAccount.setEnabled(true);
+ optionUseAccount.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ layoutLogin.expand();
+ layoutAnonymous.collapse();
+ inputUsername.setEnabled(true);
+ inputPassword.setEnabled(true);
+ }
+ });
+ optionAnonymous.setVisibility(View.VISIBLE);
+ optionAnonymous.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ layoutLogin.collapse();
+ layoutAnonymous.expand();
+ inputUsername.setEnabled(false);
+ inputPassword.setEnabled(false);
+ }
+ });
+ }
+ }
+
+ private void reportIssue() {
+
+ if (!validateInput()) return;
+
+ if (optionUseAccount.isChecked()) {
+ String username = inputUsername.getText().toString();
+ String password = inputPassword.getText().toString();
+ sendBugReport(new GithubLogin(username, password), null);
+ } else {
+ if (TextUtils.isEmpty(token))
+ throw new IllegalStateException("You must provide a GitHub API Token.");
+
+ String email = null;
+ if (!TextUtils.isEmpty(inputEmail.getText()) &&
+ EMAIL_ADDRESS.matcher(inputEmail.getText().toString()).matches()) {
+ email = inputEmail.getText().toString();
+ }
+
+ sendBugReport(new GithubLogin(token), email);
+ }
+ }
+
+ private boolean validateInput() {
+ boolean hasErrors = false;
+
+ if (optionUseAccount.isChecked()) {
+ if (TextUtils.isEmpty(inputUsername.getText())) {
+ setError(inputUsername, R.string.air_error_no_username);
+ hasErrors = true;
+ } else {
+ removeError(inputUsername);
+ }
+
+ if (TextUtils.isEmpty(inputPassword.getText())) {
+ setError(inputPassword, R.string.air_error_no_password);
+ hasErrors = true;
+ } else {
+ removeError(inputPassword);
+ }
+ } else {
+ if (emailRequired) {
+ if (TextUtils.isEmpty(inputEmail.getText()) ||
+ !EMAIL_ADDRESS.matcher(inputEmail.getText().toString()).matches()) {
+ setError(inputEmail, R.string.air_error_no_email);
+ hasErrors = true;
+ } else {
+ removeError(inputEmail);
+ }
+ }
+ }
+
+ if (TextUtils.isEmpty(inputTitle.getText())) {
+ setError(inputTitle, R.string.air_error_no_title);
+ hasErrors = true;
+ } else {
+ removeError(inputTitle);
+ }
+
+ if (TextUtils.isEmpty(inputDescription.getText())) {
+ setError(inputDescription, R.string.air_error_no_description);
+ hasErrors = true;
+ } else {
+ if (bodyMinChar > 0) {
+ if (inputDescription.getText().toString().length() < bodyMinChar) {
+ setError(inputDescription, getResources().getQuantityString(R.plurals.air_error_short_description, bodyMinChar, bodyMinChar));
+ hasErrors = true;
+ } else {
+ removeError(inputDescription);
+ }
+ } else
+ removeError(inputDescription);
+ }
+ return !hasErrors;
+ }
+
+ private void setError(TextInputEditText editText, @StringRes int errorRes) {
+ try {
+ View layout = (View) editText.getParent();
+ while (!layout.getClass().getSimpleName().equals(TextInputLayout.class.getSimpleName()))
+ layout = (View) layout.getParent();
+ TextInputLayout realLayout = (TextInputLayout) layout;
+ realLayout.setError(getString(errorRes));
+ } catch (ClassCastException | NullPointerException e) {
+ Log.e(TAG, "Issue while setting error UI.", e);
+ }
+ }
+
+ private void setError(TextInputEditText editText, String error) {
+ try {
+ View layout = (View) editText.getParent();
+ while (!layout.getClass().getSimpleName().equals(TextInputLayout.class.getSimpleName()))
+ layout = (View) layout.getParent();
+ TextInputLayout realLayout = (TextInputLayout) layout;
+ realLayout.setError(error);
+ } catch (ClassCastException | NullPointerException e) {
+ Log.e(TAG, "Issue while setting error UI.", e);
+ }
+ }
+
+ private void removeError(TextInputEditText editText) {
+ try {
+ View layout = (View) editText.getParent();
+ while (!layout.getClass().getSimpleName().equals(TextInputLayout.class.getSimpleName()))
+ layout = (View) layout.getParent();
+ TextInputLayout realLayout = (TextInputLayout) layout;
+ realLayout.setError(null);
+ } catch (ClassCastException | NullPointerException e) {
+ Log.e(TAG, "Issue while removing error UI.", e);
+ }
+ }
+
+ private void sendBugReport(GithubLogin login, String email) {
+ if (!validateInput()) return;
+
+ String bugTitle = inputTitle.getText().toString();
+ String bugDescription = inputDescription.getText().toString();
+
+ DeviceInfo deviceInfo = new DeviceInfo(this);
+
+ ExtraInfo extraInfo = new ExtraInfo();
+ onSaveExtraInfo(extraInfo);
+
+ Report report = new Report(bugTitle, bugDescription, deviceInfo, extraInfo, email);
+ GithubTarget target = getTarget();
+
+ report(this, report, target, login);
+ }
+
+ protected final void setGuestEmailRequired(boolean required) {
+ this.emailRequired = required;
+ if (required) {
+ optionAnonymous.setText(R.string.air_label_use_email);
+ ((TextInputLayout) findViewById(R.id.air_inputEmailParent)).setHint(getString(R.string.air_label_email));
+ } else {
+ optionAnonymous.setText(R.string.air_label_use_guest);
+ ((TextInputLayout) findViewById(R.id.air_inputEmailParent)).setHint(getString(R.string.air_label_email_optional));
+ }
+ }
+
+ protected final void setMinimumDescriptionLength(int length) {
+ this.bodyMinChar = length;
+ }
+
+ protected void onSaveExtraInfo(ExtraInfo extraInfo) {
+ }
+
+ protected abstract GithubTarget getTarget();
+
+ @Deprecated
+ protected String getGuestToken() {
+ return null;
+ }
+
+ protected final void setGuestToken(String token) {
+ this.token = token;
+ Log.d(TAG, "GuestToken: " + token);
+ updateGuestTokenViews();
+ }
+
+ @Override
+ public void finish() {
+ if (mCrash) {
+ if (!mReportFailed) {
+ Toast.makeText(this, com.stardust.scriptdroid.R.string.text_report_succeed, Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this, com.stardust.scriptdroid.R.string.text_report_fail, Toast.LENGTH_SHORT).show();
+ }
+ finishAffinity();
+ } else {
+ super.finish();
+ }
+ }
+
+ private void report(Activity activity, Report report, GithubTarget target,
+ GithubLogin login) {
+ new ReportIssueTask(activity, report, target, login).execute();
+ }
+
+ private class ReportIssueTask extends DialogAsyncTask {
+ private final Report report;
+ private final GithubTarget target;
+ private final GithubLogin login;
+
+ private ReportIssueTask(Activity activity, Report report, GithubTarget target,
+ GithubLogin login) {
+ super(activity);
+ this.report = report;
+ this.target = target;
+ this.login = login;
+ }
+
+
+ @Override
+ protected Dialog createDialog(@NonNull Context context) {
+ return new MaterialDialog.Builder(context)
+ .progress(true, 0)
+ .progressIndeterminateStyle(true)
+ .title(R.string.air_dialog_title_loading)
+ .show();
+ }
+
+ @Override
+ @Result
+ protected String doInBackground(Void... params) {
+ GitHubClient client;
+ if (login.shouldUseApiToken()) {
+ client = new GitHubClient().setOAuth2Token(login.getApiToken());
+ } else {
+ client = new GitHubClient().setCredentials(login.getUsername(), login.getPassword());
+ }
+
+ Issue issue = new Issue().setTitle(report.getTitle()).setBody(report.getDescription());
+ try {
+ new IssueService(client).createIssue(target.getUsername(), target.getRepository(), issue);
+ return RESULT_OK;
+ } catch (RequestException e) {
+ switch (e.getStatus()) {
+ case STATUS_BAD_CREDENTIALS:
+ if (login.shouldUseApiToken())
+ return RESULT_INVALID_TOKEN;
+ return RESULT_BAD_CREDENTIALS;
+ case STATUS_ISSUES_NOT_ENABLED:
+ return RESULT_ISSUES_NOT_ENABLED;
+ default:
+ e.printStackTrace();
+ return RESULT_UNKNOWN;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return RESULT_UNKNOWN;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(@Result String result) {
+ super.onPostExecute(result);
+
+ Context context = getContext();
+ if (context == null) return;
+
+ switch (result) {
+ case RESULT_OK:
+ mReportFailed = false;
+ tryToFinishActivity();
+ break;
+ case RESULT_BAD_CREDENTIALS:
+ new MaterialDialog.Builder(context)
+ .title(R.string.air_dialog_title_failed)
+ .content(R.string.air_dialog_description_failed_wrong_credentials)
+ .positiveText(R.string.air_dialog_action_failed)
+ .show();
+ break;
+ case RESULT_INVALID_TOKEN:
+ new MaterialDialog.Builder(context)
+ .title(R.string.air_dialog_title_failed)
+ .content(R.string.air_dialog_description_failed_invalid_token)
+ .positiveText(R.string.air_dialog_action_failed)
+ .show();
+ break;
+ case RESULT_ISSUES_NOT_ENABLED:
+ new MaterialDialog.Builder(context)
+ .title(R.string.air_dialog_title_failed)
+ .content(R.string.air_dialog_description_failed_issues_not_available)
+ .positiveText(R.string.air_dialog_action_failed)
+ .show();
+ break;
+ default:
+ new MaterialDialog.Builder(context)
+ .title(R.string.air_dialog_title_failed)
+ .content(R.string.air_dialog_description_failed_unknown)
+ .positiveText(R.string.air_dialog_action_failed)
+ .onPositive(new MaterialDialog.SingleButtonCallback() {
+ @Override
+ public void onClick(@NonNull MaterialDialog dialog,
+ @NonNull DialogAction which) {
+ tryToFinishActivity();
+ }
+ })
+ .cancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ tryToFinishActivity();
+ }
+ })
+ .show();
+ break;
+ }
+ }
+
+ private void tryToFinishActivity() {
+ Context context = getContext();
+ if (context instanceof Activity && !((Activity) context).isFinishing()) {
+ ((Activity) context).finish();
+ }
+ }
+ }
+
+ private static abstract class DialogAsyncTask extends AsyncTask {
+ private WeakReference contextWeakReference;
+ private WeakReference