mirror of
https://github.com/AU-COVIDSafe/mobile-android.git
synced 2025-04-27 17:05:16 +00:00
COVIDSafe code from version 1.0.16
This commit is contained in:
commit
b827cf3cce
341 changed files with 28036 additions and 0 deletions
17
feedback-android/src/main/AndroidManifest.xml
Normal file
17
feedback-android/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.atlassian.mobilekit.module.feedback">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.atlassian.mobilekit.module.feedback.FeedbackActivity"
|
||||
android:windowSoftInputMode="stateUnchanged|adjustResize"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,108 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
public class ActivityTracker implements Application.ActivityLifecycleCallbacks, UiInfo {
|
||||
|
||||
private WeakReference<Activity> activityRef = new WeakReference<>(null);
|
||||
private boolean isAppVisible = false;
|
||||
private final CopyOnWriteArraySet<UiInfoListener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
public ActivityTracker(Application application) {
|
||||
application.registerActivityLifecycleCallbacks(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Activity getCurrentActivity() {
|
||||
Activity currentActivity = activityRef.get();
|
||||
|
||||
if (currentActivity != null
|
||||
&& (currentActivity.isFinishing()
|
||||
|| currentActivity.isChangingConfigurations())) {
|
||||
currentActivity = null;
|
||||
activityRef = new WeakReference<>(null);
|
||||
}
|
||||
|
||||
return currentActivity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAppVisible() {
|
||||
return isAppVisible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerListener(UiInfoListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListener(UiInfoListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
private void notifyAppVisible() {
|
||||
isAppVisible = true;
|
||||
for (UiInfoListener listener : listeners) {
|
||||
listener.onAppVisible();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAppNotVisible() {
|
||||
isAppVisible = false;
|
||||
for (UiInfoListener listener : listeners) {
|
||||
listener.onAppNotVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@NotNull Activity activity, Bundle bundle) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(@NotNull Activity activity) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(@NotNull Activity activity) {
|
||||
final boolean wasEmpty = (activityRef.get() == null);
|
||||
activityRef = new WeakReference<>(activity);
|
||||
|
||||
if (wasEmpty) {
|
||||
notifyAppVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(@NotNull Activity activity) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(@NotNull Activity activity) {
|
||||
if (activityRef.get() == activity) {
|
||||
activityRef = new WeakReference<>(null);
|
||||
notifyAppNotVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(@NotNull Activity activity, @NotNull Bundle bundle) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(@NotNull Activity activity) {
|
||||
if (activityRef.get() == activity) {
|
||||
activityRef = new WeakReference<>(null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
public class AndroidUiNotifier implements UiNotifier {
|
||||
|
||||
private final Handler uiHandler;
|
||||
|
||||
public AndroidUiNotifier() {
|
||||
uiHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post(Runnable runnable) {
|
||||
uiHandler.post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postDelayed(Runnable runnable, int delay) {
|
||||
uiHandler.postDelayed(runnable, delay);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
/**
|
||||
* Synonym interface, only to emphasize its usage.
|
||||
*/
|
||||
public interface Command extends Runnable {
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class DeviceInfo {
|
||||
|
||||
private static final String NAMESPACE = "com.atlassian.mobilekit.module.core";
|
||||
private static final String STORE_NAME = NAMESPACE + ".preferences";
|
||||
private static final String UUID_KEY = NAMESPACE + ".UUID";
|
||||
private static final String ANDROID_OS = "Android OS";
|
||||
|
||||
private final Context ctx;
|
||||
private final SharedPreferences store;
|
||||
|
||||
// These use lazy initialization
|
||||
private String uuid;
|
||||
private String udid;
|
||||
private String appVersionName;
|
||||
private int appVersionCode = -1;
|
||||
|
||||
public DeviceInfo(Context ctx) {
|
||||
this.ctx = ctx.getApplicationContext();
|
||||
store = ctx.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private synchronized String initUdid() {
|
||||
String androidId = Settings.Secure.getString(ctx.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
if (androidId == null) {
|
||||
throw new AssertionError("ANDROID_ID setting was null");
|
||||
}
|
||||
return androidId;
|
||||
}
|
||||
|
||||
private synchronized String initUuid() {
|
||||
String uuidStr = store.getString(UUID_KEY, null);
|
||||
if (uuidStr == null) {
|
||||
uuidStr = UUID.randomUUID().toString();
|
||||
SharedPreferences.Editor edit = store.edit();
|
||||
edit.putString(UUID_KEY, uuidStr);
|
||||
edit.apply();
|
||||
}
|
||||
return uuidStr;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
if (uuid == null) {
|
||||
uuid = initUuid();
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getUdid() {
|
||||
if (udid == null) {
|
||||
udid = initUdid();
|
||||
}
|
||||
|
||||
return udid;
|
||||
}
|
||||
|
||||
public String getAppPkgName() {
|
||||
return ctx.getPackageName();
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return ctx.getPackageManager()
|
||||
.getApplicationLabel(ctx.getApplicationInfo())
|
||||
.toString();
|
||||
}
|
||||
|
||||
public String getAppVersionName() {
|
||||
if (appVersionName == null) {
|
||||
PackageInfo pInfo = getPackageInfo();
|
||||
appVersionName = pInfo.versionName;
|
||||
}
|
||||
|
||||
return appVersionName;
|
||||
}
|
||||
|
||||
public int getAppVersionCode() {
|
||||
if (appVersionCode == -1) {
|
||||
PackageInfo pInfo = getPackageInfo();
|
||||
appVersionCode = pInfo.versionCode;
|
||||
}
|
||||
|
||||
return appVersionCode;
|
||||
}
|
||||
|
||||
public String getSystemVersion() {
|
||||
return Build.VERSION.RELEASE;
|
||||
}
|
||||
|
||||
public String getSystemName() {
|
||||
return ANDROID_OS;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
return Build.DEVICE;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return Build.MODEL;
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return Locale.getDefault().getDisplayLanguage();
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
Locale locale = Locale.getDefault();
|
||||
return locale.getLanguage() + "_" + locale.getCountry();
|
||||
}
|
||||
|
||||
public boolean hasConnectivity() {
|
||||
ConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cMgr == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NetworkInfo nwInfo = cMgr.getActiveNetworkInfo();
|
||||
return nwInfo != null && nwInfo.isConnected();
|
||||
}
|
||||
|
||||
|
||||
private PackageInfo getPackageInfo() {
|
||||
try {
|
||||
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
|
||||
public class FeedbackBaseActivity extends AppCompatActivity {
|
||||
|
||||
private boolean isPaused;
|
||||
private long pausedAt;
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
isPaused = true;
|
||||
pausedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
isPaused = false;
|
||||
pausedAt = 0;
|
||||
}
|
||||
|
||||
protected boolean isPaused() {
|
||||
return isPaused;
|
||||
}
|
||||
|
||||
protected long getPausedDuration() {
|
||||
return System.currentTimeMillis() - pausedAt;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
|
||||
public final class JobQueue {
|
||||
|
||||
private final ExecutorService executor;
|
||||
|
||||
public JobQueue() {
|
||||
executor = Executors.newFixedThreadPool(5);
|
||||
}
|
||||
|
||||
public final void enqueue(Runnable r) {
|
||||
executor.execute(r);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Executor getExecutor() {
|
||||
return executor;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.atlassian.mobilekit.module.feedback.R;
|
||||
|
||||
public class MobileKitDialogViewBuilder {
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
private final ViewGroup container;
|
||||
private int titleResId;
|
||||
private int msgResId;
|
||||
private int posBtnResId;
|
||||
private int negBtnResId;
|
||||
|
||||
private View.OnClickListener posClickListener;
|
||||
private View.OnClickListener negClickListener;
|
||||
|
||||
public MobileKitDialogViewBuilder(LayoutInflater inflater, ViewGroup container) {
|
||||
this.inflater = inflater;
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
public MobileKitDialogViewBuilder title(int titleResId) {
|
||||
this.titleResId = titleResId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MobileKitDialogViewBuilder message(int msgResId) {
|
||||
this.msgResId = msgResId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MobileKitDialogViewBuilder positiveButton(
|
||||
int posBtnResId, View.OnClickListener onClickListener) {
|
||||
|
||||
this.posBtnResId = posBtnResId;
|
||||
posClickListener = onClickListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MobileKitDialogViewBuilder negativeButton(
|
||||
int negBtnResId, View.OnClickListener onClickListener) {
|
||||
|
||||
this.negBtnResId = negBtnResId;
|
||||
negClickListener = onClickListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public View build() {
|
||||
|
||||
View dialogView = inflater.inflate(R.layout.mk_feedback_dialog_container, container, false);
|
||||
FrameLayout frameLayout = (FrameLayout) dialogView.findViewById(R.id.dialog_container);
|
||||
inflater.inflate(R.layout.mk_feedback_dialog_content, frameLayout);
|
||||
|
||||
final AppCompatTextView titleView = (AppCompatTextView) dialogView.findViewById(R.id.title);
|
||||
if (titleResId == 0) {
|
||||
titleView.setVisibility(View.GONE);
|
||||
} else {
|
||||
titleView.setText(titleResId);
|
||||
}
|
||||
|
||||
final AppCompatTextView msgView = (AppCompatTextView) dialogView.findViewById(R.id.message);
|
||||
if (msgResId == 0) {
|
||||
msgView.setVisibility(View.GONE);
|
||||
} else {
|
||||
msgView.setText(msgResId);
|
||||
}
|
||||
|
||||
final Button posBtn = (Button) dialogView.findViewById(R.id.positive_btn);
|
||||
if (posBtnResId == 0) {
|
||||
posBtn.setVisibility(View.GONE);
|
||||
} else {
|
||||
posBtn.setText(posBtnResId);
|
||||
posBtn.setOnClickListener(posClickListener);
|
||||
}
|
||||
|
||||
final Button negBtn = (Button) dialogView.findViewById(R.id.negative_btn);
|
||||
if (negBtnResId == 0) {
|
||||
negBtn.setVisibility(View.GONE);
|
||||
} else {
|
||||
negBtn.setText(negBtnResId);
|
||||
negBtn.setOnClickListener(negClickListener);
|
||||
}
|
||||
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
public interface Receiver<T> {
|
||||
|
||||
void receive(T data);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
public interface UiInfo {
|
||||
|
||||
Activity getCurrentActivity();
|
||||
|
||||
boolean isAppVisible();
|
||||
|
||||
void registerListener(UiInfoListener listener);
|
||||
|
||||
void unregisterListener(UiInfoListener listener);
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
|
||||
public interface UiInfoListener {
|
||||
|
||||
void onAppVisible();
|
||||
|
||||
void onAppNotVisible();
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
|
||||
public interface UiNotifier {
|
||||
|
||||
void post(Runnable runnable);
|
||||
|
||||
void postDelayed(Runnable runnable, int delay);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.atlassian.mobilekit.module.core;
|
||||
|
||||
/**
|
||||
* Notifications to this receiver will be delivered on Ui/Main Thread
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
public interface UiReceiver<T> extends Receiver<T> {
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.atlassian.mobilekit.module.core.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* This class exists as a workaround for Unit Test issues where TextUtils cannot be mocked
|
||||
* Refer: http://tools.android.com/tech-docs/unit-testing-support
|
||||
*/
|
||||
public final class StringUtils {
|
||||
|
||||
public static final String EOL = "\n";
|
||||
private static final String ELLIPSIS = "\u2026";
|
||||
private static final int ELLIPSIS_LEN = ELLIPSIS.length();
|
||||
|
||||
private StringUtils() {
|
||||
}
|
||||
|
||||
public static String ellipsize(String input, int maxLen) {
|
||||
return (TextUtils.isEmpty(input) || input.length() <= (maxLen + ELLIPSIS_LEN))
|
||||
? input
|
||||
: input.substring(0, maxLen) + ELLIPSIS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.atlassian.mobilekit.module.core.utils;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import static android.content.Context.INPUT_METHOD_SERVICE;
|
||||
|
||||
/**
|
||||
* Utils to interact with Android system APIs.
|
||||
*/
|
||||
public class SystemUtils {
|
||||
|
||||
/**
|
||||
* Hides soft keyboard
|
||||
*/
|
||||
public static void hideSoftKeyboard(View target) {
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) target.getContext().getSystemService(INPUT_METHOD_SERVICE);
|
||||
if (inputMethodManager != null) {
|
||||
inputMethodManager.hideSoftInputFromWindow(target.getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Patterns;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.DeviceInfo;
|
||||
import com.atlassian.mobilekit.module.core.utils.SystemUtils;
|
||||
import com.atlassian.mobilekit.module.feedback.commands.Result;
|
||||
|
||||
public class FeedbackActivity extends AppCompatActivity
|
||||
implements ProgressDialogActions, FinishAction, SendFeedbackListener {
|
||||
|
||||
private EditText feedbackEt;
|
||||
private EditText feedbackEmailEt;
|
||||
private MenuItem sendMenuItem = null;
|
||||
private DeviceInfo deviceInfo = null;
|
||||
|
||||
public static Intent getIntent(Context src) {
|
||||
Intent intent = new Intent(src, FeedbackActivity.class);
|
||||
if (!(src instanceof Activity)) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// TO make sure scroll works with editTexts
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
setContentView(R.layout.activity_feedback);
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
}
|
||||
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
feedbackEt = (EditText) findViewById(R.id.feedbackIssueDescriptionEditText);
|
||||
feedbackEt.addTextChangedListener(new TextWatcherAdapter() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
updateSendButtonState();
|
||||
}
|
||||
});
|
||||
feedbackEmailEt = (EditText) findViewById(R.id.feedbackIssueEmailEditText);
|
||||
feedbackEmailEt.addTextChangedListener(new TextWatcherAdapter() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
updateSendButtonState();
|
||||
}
|
||||
});
|
||||
feedbackEmailEt.setOnEditorActionListener((v, actionId, event) -> {
|
||||
onOptionsItemSelected(sendMenuItem);
|
||||
return true;
|
||||
});
|
||||
|
||||
View immediateParentView = findViewById(R.id.feedback_content_parent);
|
||||
if (immediateParentView != null) {
|
||||
immediateParentView.setOnClickListener(view -> {
|
||||
// Show keyboard when user clicks on
|
||||
// Large white area on the screen beside the screenshot
|
||||
focusOnFeedbackEditText();
|
||||
});
|
||||
}
|
||||
|
||||
View rootView = findViewById(android.R.id.content);
|
||||
if (rootView != null) {
|
||||
rootView.setOnClickListener(view -> {
|
||||
// Show keyboard when user clicks on
|
||||
// Large white area on the screen below the screenshot
|
||||
focusOnFeedbackEditText();
|
||||
});
|
||||
}
|
||||
|
||||
if (null == savedInstanceState) {
|
||||
focusOnFeedbackEditText();
|
||||
}
|
||||
|
||||
deviceInfo = new DeviceInfo(getApplicationContext());
|
||||
|
||||
FeedbackModule.registerSendFeedbackListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
FeedbackModule.unregisterSendFeedbackListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_feedback, menu);
|
||||
sendMenuItem = menu.findItem(R.id.action_send);
|
||||
sendMenuItem.setEnabled(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_send) {
|
||||
String msg = feedbackEt.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(msg)) {
|
||||
Toast.makeText(this, R.string.mk_fb_feedback_empty, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
String email = feedbackEmailEt.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(email) || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
|
||||
Toast.makeText(this, R.string.mk_fb_invalid_email_address, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!deviceInfo.hasConnectivity()) {
|
||||
Toast.makeText(this, R.string.mk_fb_device_offline, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
sendMenuItem = item;
|
||||
SystemUtils.hideSoftKeyboard(feedbackEt);
|
||||
SystemUtils.hideSoftKeyboard(feedbackEmailEt);
|
||||
sendFeedback(msg, email);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void updateSendButtonState() {
|
||||
String feedback = feedbackEt.getText().toString();
|
||||
String email = feedbackEmailEt.getText().toString();
|
||||
if (!TextUtils.isEmpty(feedback) && Patterns.EMAIL_ADDRESS.matcher(email).matches() && sendMenuItem != null) {
|
||||
sendMenuItem.setEnabled(true);
|
||||
} else if (sendMenuItem != null) {
|
||||
sendMenuItem.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void showProgressDialog() {
|
||||
final ProgressDialogFragment progressDialog = new ProgressDialogFragment();
|
||||
progressDialog.show(getSupportFragmentManager(), ProgressDialogFragment.class.getSimpleName());
|
||||
}
|
||||
|
||||
public void dismissProgressDialog() {
|
||||
Fragment dialogFragment = getSupportFragmentManager().findFragmentByTag(ProgressDialogFragment.class.getSimpleName());
|
||||
if (dialogFragment != null
|
||||
&& dialogFragment instanceof ProgressDialogFragment) {
|
||||
((ProgressDialogFragment) dialogFragment).dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFinish() {
|
||||
finish();
|
||||
}
|
||||
|
||||
private void focusOnFeedbackEditText() {
|
||||
feedbackEt.requestFocus();
|
||||
showKeyboard();
|
||||
}
|
||||
|
||||
private void sendFeedback(final String msg, final String email) {
|
||||
FeedbackModule.sendFeedback(msg, email);
|
||||
showProgressDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendCompleted(Result result) {
|
||||
if (Result.SUCCESS == result) {
|
||||
|
||||
// Don't allow any more changes
|
||||
if (sendMenuItem != null) {
|
||||
sendMenuItem.setEnabled(false);
|
||||
}
|
||||
feedbackEt.setEnabled(false);
|
||||
}
|
||||
|
||||
boolean isPaused = !getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED);
|
||||
|
||||
if (isPaused) {
|
||||
finish(); // Cannot show any notification to user. So just finish.
|
||||
}
|
||||
}
|
||||
|
||||
private void showKeyboard() {
|
||||
feedbackEt.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(feedbackEt, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
private abstract class TextWatcherAdapter implements TextWatcher {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// unused
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// unused
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
// unused
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.Command;
|
||||
import com.atlassian.mobilekit.module.core.DeviceInfo;
|
||||
import com.atlassian.mobilekit.module.core.JobQueue;
|
||||
import com.atlassian.mobilekit.module.core.Receiver;
|
||||
import com.atlassian.mobilekit.module.core.UiInfo;
|
||||
import com.atlassian.mobilekit.module.core.UiInfoListener;
|
||||
import com.atlassian.mobilekit.module.core.UiNotifier;
|
||||
import com.atlassian.mobilekit.module.feedback.commands.Result;
|
||||
import com.atlassian.mobilekit.module.feedback.commands.SendFeedbackCommand;
|
||||
import com.atlassian.mobilekit.module.feedback.model.CreateIssueRequest;
|
||||
import com.atlassian.mobilekit.module.feedback.model.FeedbackConfig;
|
||||
import com.atlassian.mobilekit.module.feedback.network.BaseApiParams;
|
||||
import com.atlassian.mobilekit.module.feedback.network.JmcRestClient;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Commands supported by this client
|
||||
* Show Feedback
|
||||
* Send Feedback
|
||||
*/
|
||||
class FeedbackClient implements Receiver<FeedbackConfig>, UiInfoListener {
|
||||
|
||||
private static final String LOG_TAG = FeedbackClient.class.getSimpleName();
|
||||
private static final String PROTOCOL_HTTPS = "https://";
|
||||
|
||||
private static final Set<Class<?>> IGNORE_ACTIVITIES = new HashSet<>();
|
||||
|
||||
static {
|
||||
IGNORE_ACTIVITIES.add(FeedbackActivity.class);
|
||||
}
|
||||
|
||||
private final JmcRestClient restClient;
|
||||
private final JobQueue jobQueue;
|
||||
private final DeviceInfo deviceInfo;
|
||||
private final Map<String, String> baseQueryMap = new HashMap<>();
|
||||
private final UiNotifier uiNotifier;
|
||||
private final UiInfo uiInfo;
|
||||
private final FeedbackSettings settings;
|
||||
private final AtomicInteger notificationViewId = new AtomicInteger(0);
|
||||
private final CopyOnWriteArraySet<FeedbackNotificationListener> notificationListeners = new CopyOnWriteArraySet<>();
|
||||
private final CopyOnWriteArraySet<SendFeedbackListener> sendFeedbackListeners = new CopyOnWriteArraySet<>();
|
||||
private FeedbackDataProvider feedbackDataProvider;
|
||||
|
||||
private FeedbackConfig feedbackConfig;
|
||||
|
||||
FeedbackClient(@NonNull JmcRestClient restClient,
|
||||
@NonNull DeviceInfo deviceInfo,
|
||||
@NonNull JobQueue jobQueue,
|
||||
@NonNull UiNotifier uiNotifier,
|
||||
@NonNull UiInfo uiInfo,
|
||||
@NonNull FeedbackSettings settings) {
|
||||
|
||||
this.restClient = restClient;
|
||||
this.jobQueue = jobQueue;
|
||||
this.deviceInfo = deviceInfo;
|
||||
this.uiNotifier = uiNotifier;
|
||||
this.uiInfo = uiInfo;
|
||||
this.settings = settings;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
uiInfo.registerListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(FeedbackConfig data) {
|
||||
feedbackConfig = data;
|
||||
restClient.init(PROTOCOL_HTTPS, data.getHost());
|
||||
|
||||
baseQueryMap.put(BaseApiParams.API_KEY, data.getApiKey());
|
||||
baseQueryMap.put(BaseApiParams.PROJECT, data.getProjectKey());
|
||||
}
|
||||
|
||||
final void sendFeedback(String message, String email) {
|
||||
|
||||
final CreateIssueRequest.Builder requestBuilder =
|
||||
new CreateIssueRequest.Builder()
|
||||
.summary(message)
|
||||
.description(message)
|
||||
.isCrash(false)
|
||||
.udid(deviceInfo.getUdid())
|
||||
.uuid(deviceInfo.getUuid())
|
||||
.appName(deviceInfo.getAppName())
|
||||
.appId(deviceInfo.getAppPkgName())
|
||||
.systemName(deviceInfo.getSystemName())
|
||||
.deviceName(deviceInfo.getDeviceName())
|
||||
.language(deviceInfo.getLanguage())
|
||||
.components(Arrays.asList(feedbackConfig.getComponents()));
|
||||
|
||||
setFeedbackDataProvider(new FeedbackDataProvider() {
|
||||
@Override
|
||||
public String getAdditionalDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JiraIssueType getIssueType() {
|
||||
return JiraIssueType.SUPPORT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getCustomFieldsData() {
|
||||
HashMap<String, Object> map = new HashMap<>();
|
||||
map.put("E-mail", email);
|
||||
map.put("OS version", deviceInfo.getSystemVersion());
|
||||
map.put("App version", deviceInfo.getAppVersionName());
|
||||
map.put("Phone model", deviceInfo.getModel());
|
||||
return map;
|
||||
}
|
||||
});
|
||||
|
||||
final Command cmd = new SendFeedbackCommand(
|
||||
baseQueryMap, requestBuilder, feedbackDataProvider,
|
||||
restClient,
|
||||
new SnackbarReceiver(uiInfo, uiNotifier, message, email),
|
||||
uiNotifier);
|
||||
|
||||
jobQueue.enqueue(cmd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppVisible() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppNotVisible() {
|
||||
}
|
||||
|
||||
final void showFeedback() {
|
||||
|
||||
final Activity curActivity = uiInfo.getCurrentActivity();
|
||||
if (curActivity == null) {
|
||||
Log.e(LOG_TAG, "No usable current activity. Abort Feedback.");
|
||||
return;
|
||||
} else if (IGNORE_ACTIVITIES.contains(curActivity.getClass())) {
|
||||
Log.e(LOG_TAG, "User is already in Feedback flow. Abort.");
|
||||
return;
|
||||
}
|
||||
|
||||
final Context appCtx = curActivity.getApplicationContext();
|
||||
|
||||
uiNotifier.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
launchFeedbackScreen(curActivity, appCtx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void launchFeedbackScreen(Activity activity, Context appCtx) {
|
||||
final Context useCtx = activity.isFinishing() || activity.isChangingConfigurations()
|
||||
? appCtx : activity;
|
||||
|
||||
Intent intent = FeedbackActivity.getIntent(useCtx);
|
||||
useCtx.startActivity(intent);
|
||||
}
|
||||
|
||||
private void setFeedbackDataProvider(FeedbackDataProvider feedbackDataProvider) {
|
||||
this.feedbackDataProvider = feedbackDataProvider;
|
||||
}
|
||||
|
||||
final int getNotificationViewId() {
|
||||
return notificationViewId.get();
|
||||
}
|
||||
|
||||
final void registerSendFeedbackListener(SendFeedbackListener listener) {
|
||||
sendFeedbackListeners.add(listener);
|
||||
}
|
||||
|
||||
final void unregisterSendFeedbackListener(SendFeedbackListener listener) {
|
||||
sendFeedbackListeners.remove(listener);
|
||||
}
|
||||
|
||||
final void notifySendCompleted(Result result) {
|
||||
for (SendFeedbackListener listener : sendFeedbackListeners) {
|
||||
listener.onSendCompleted(result);
|
||||
}
|
||||
}
|
||||
|
||||
final void notificationStarted() {
|
||||
for (FeedbackNotificationListener fnl : notificationListeners) {
|
||||
fnl.onNotificationStarted();
|
||||
}
|
||||
}
|
||||
|
||||
final void notificationDismissed() {
|
||||
for (FeedbackNotificationListener fnl : notificationListeners) {
|
||||
fnl.onNotificationDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
final void setEnableDialogDisplayed() {
|
||||
settings.setEnableDialogDisplayed();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import java.util.Map;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This is used to provide more information which will be used when creating the feedback JIRA issue.
|
||||
*/
|
||||
public interface FeedbackDataProvider {
|
||||
|
||||
/**
|
||||
* This string will be appended to the description of Feedback JIRA issue.
|
||||
* It may contain standard wiki markup that is accepted by the JIRA Instance in description field.
|
||||
* Encoding supported: UTF-8
|
||||
* @return
|
||||
*/
|
||||
String getAdditionalDescription();
|
||||
|
||||
/**
|
||||
* See {@link JiraIssueType} for available options.
|
||||
* If this returns null, then the library will default to {@link JiraIssueType#TASK}
|
||||
* An admin must pre-configure the JIRA Project to accept this type.
|
||||
* If not, resultant issue will be of default type as per the project.
|
||||
* @return
|
||||
*/
|
||||
JiraIssueType getIssueType();
|
||||
|
||||
/**
|
||||
* This data will be passed to the feedback client to create custom fields in the Jira issue
|
||||
* @return map of Jira field name and respective values
|
||||
*/
|
||||
@Nullable
|
||||
Map<String, Object> getCustomFieldsData();
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.ActivityTracker;
|
||||
import com.atlassian.mobilekit.module.core.AndroidUiNotifier;
|
||||
import com.atlassian.mobilekit.module.core.Command;
|
||||
import com.atlassian.mobilekit.module.core.DeviceInfo;
|
||||
import com.atlassian.mobilekit.module.core.JobQueue;
|
||||
import com.atlassian.mobilekit.module.core.UiInfo;
|
||||
import com.atlassian.mobilekit.module.core.UiNotifier;
|
||||
import com.atlassian.mobilekit.module.feedback.commands.LoadFeedbackConfigCommand;
|
||||
import com.atlassian.mobilekit.module.feedback.commands.Result;
|
||||
import com.atlassian.mobilekit.module.feedback.network.JmcRestClient;
|
||||
|
||||
public final class FeedbackModule {
|
||||
|
||||
private static final String NAMESPACE = "com.atlassian.mobilekit.module.feedback";
|
||||
private static final String STORE_NAME = NAMESPACE + ".preferences";
|
||||
|
||||
private static FeedbackClient feedbackClient = null;
|
||||
private static JobQueue jobQueue = null;
|
||||
private static UiInfo activityTracker = null;
|
||||
|
||||
private static UiNotifier androidUiNotifier = new AndroidUiNotifier();
|
||||
|
||||
private FeedbackModule() {
|
||||
throw new AssertionError("Instances of this class are not allowed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes using Application object
|
||||
*
|
||||
* @param application
|
||||
*/
|
||||
public static void init(@NonNull Application application) {
|
||||
// Build a Feedback Client here and _start_ its initialization.
|
||||
// Initialization will happen asynchronously in a background thread.
|
||||
if (feedbackClient == null) {
|
||||
jobQueue = new JobQueue();
|
||||
activityTracker = new ActivityTracker(application);
|
||||
|
||||
final SharedPreferences store = application.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
feedbackClient = new FeedbackClient(
|
||||
new JmcRestClient(),
|
||||
new DeviceInfo(application.getApplicationContext()),
|
||||
jobQueue,
|
||||
androidUiNotifier,
|
||||
activityTracker,
|
||||
new FeedbackSettings(store));
|
||||
}
|
||||
|
||||
Command loadConfigCommand = new LoadFeedbackConfigCommand(
|
||||
application.getApplicationContext(),
|
||||
feedbackClient, androidUiNotifier);
|
||||
|
||||
jobQueue.enqueue(loadConfigCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a screen to prompt user for feedback
|
||||
*/
|
||||
public static void showFeedbackScreen() {
|
||||
feedbackClient.showFeedback();
|
||||
}
|
||||
|
||||
static void notificationStarted() {
|
||||
feedbackClient.notificationStarted();
|
||||
}
|
||||
|
||||
static void notificationDismissed() {
|
||||
feedbackClient.notificationDismissed();
|
||||
}
|
||||
|
||||
static int getNotificationViewId() {
|
||||
return feedbackClient.getNotificationViewId();
|
||||
}
|
||||
|
||||
static void sendFeedback(@NonNull String message, @NonNull String email) {
|
||||
feedbackClient.sendFeedback(message, email);
|
||||
}
|
||||
|
||||
static void setEnableDialogDisplayed() {
|
||||
feedbackClient.setEnableDialogDisplayed();
|
||||
}
|
||||
|
||||
static void registerSendFeedbackListener(SendFeedbackListener listener) {
|
||||
feedbackClient.registerSendFeedbackListener(listener);
|
||||
}
|
||||
|
||||
static void unregisterSendFeedbackListener(SendFeedbackListener listener) {
|
||||
feedbackClient.unregisterSendFeedbackListener(listener);
|
||||
}
|
||||
|
||||
static void notifySendCompleted(Result result) {
|
||||
feedbackClient.notifySendCompleted(result);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
/**
|
||||
* This listener is notified when Feedback success/error prompts are displayed to the user. <br/>
|
||||
* These api are guaranteed to be invoked on the Main thread.
|
||||
*/
|
||||
public interface FeedbackNotificationListener {
|
||||
|
||||
void onNotificationStarted();
|
||||
|
||||
void onNotificationDismissed();
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
class FeedbackSettings {
|
||||
|
||||
private static final String KEY_ENABLE_DIALOG_SHOWN = "enable_dialog_shown";
|
||||
|
||||
private final SharedPreferences store;
|
||||
|
||||
FeedbackSettings(SharedPreferences store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
final void setEnableDialogDisplayed() {
|
||||
SharedPreferences.Editor editor = store.edit();
|
||||
editor.putBoolean(KEY_ENABLE_DIALOG_SHOWN, true);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
public interface FinishAction {
|
||||
|
||||
void doFinish();
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
public enum JiraIssueType {
|
||||
|
||||
BUG("Bug"),
|
||||
EPIC("Epic"),
|
||||
IMPROVEMENT("Improvement"),
|
||||
STORY("Story"),
|
||||
SUPPORT("Support"),
|
||||
TASK("Task");
|
||||
|
||||
private String type;
|
||||
|
||||
JiraIssueType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
public interface ProgressDialogActions {
|
||||
|
||||
void showProgressDialog();
|
||||
|
||||
void dismissProgressDialog();
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDialogFragment;
|
||||
|
||||
public class ProgressDialogFragment extends AppCompatDialogFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setCancelable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
ProgressDialog dialog = new ProgressDialog(getActivity(), getTheme());
|
||||
dialog.setTitle(null);
|
||||
dialog.setMessage(getString(R.string.mk_fb_sending));
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
return dialog;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import com.atlassian.mobilekit.module.feedback.commands.Result;
|
||||
|
||||
/**
|
||||
* This listener is notified when Feedback sending completes successfully or with an error
|
||||
*/
|
||||
public interface SendFeedbackListener {
|
||||
|
||||
void onSendCompleted(Result result);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class SnackbarBuilder {
|
||||
|
||||
private SnackbarBuilder() {
|
||||
// intentionally empty
|
||||
}
|
||||
|
||||
public static Snackbar build(Activity activity, int resId) {
|
||||
return Snackbar.make(getNotificationView(activity),
|
||||
applyColorSpan(activity.getString(resId)),
|
||||
Snackbar.LENGTH_LONG);
|
||||
}
|
||||
|
||||
private static SpannableStringBuilder applyColorSpan(String txt) {
|
||||
// Force text color, otherwise it may show up using odd color in the app.
|
||||
final ForegroundColorSpan whiteSpan = new ForegroundColorSpan(Color.WHITE);
|
||||
final SpannableStringBuilder spanText = new SpannableStringBuilder(txt);
|
||||
spanText.setSpan(whiteSpan, 0, spanText.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
return spanText;
|
||||
}
|
||||
|
||||
private static View getNotificationView(Activity activity) {
|
||||
int id = FeedbackModule.getNotificationViewId();
|
||||
if (id == 0) {
|
||||
id = android.R.id.content;
|
||||
}
|
||||
|
||||
View v = activity.findViewById(id);
|
||||
if (v == null) {
|
||||
v = activity.findViewById(android.R.id.content);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
|
||||
public class SnackbarCallback extends Snackbar.Callback {
|
||||
|
||||
// Handle multiple onDismissed calls
|
||||
// Refer: https://code.google.com/p/android/issues/detail?id=214547
|
||||
private boolean isDismissed = false;
|
||||
|
||||
@Override
|
||||
public void onDismissed(Snackbar snackbar, int event) {
|
||||
super.onDismissed(snackbar, event);
|
||||
if (isDismissed) {
|
||||
return;
|
||||
}
|
||||
isDismissed = true;
|
||||
FeedbackModule.notificationDismissed();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package com.atlassian.mobilekit.module.feedback;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.UiInfo;
|
||||
import com.atlassian.mobilekit.module.core.UiNotifier;
|
||||
import com.atlassian.mobilekit.module.core.UiReceiver;
|
||||
import com.atlassian.mobilekit.module.feedback.commands.Result;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class SnackbarReceiver implements UiReceiver<Result> {
|
||||
|
||||
private final UiInfo uiInfo;
|
||||
private final UiNotifier uiNotifier;
|
||||
private final String message;
|
||||
private final String email;
|
||||
|
||||
SnackbarReceiver(@NonNull UiInfo uiInfo, @NonNull UiNotifier uiNotifier,
|
||||
@NonNull String message, @NonNull String email) {
|
||||
this.uiInfo = uiInfo;
|
||||
this.uiNotifier = uiNotifier;
|
||||
this.message = message;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Result data) {
|
||||
switch (data) {
|
||||
case SUCCESS:
|
||||
showSuccessRunnable.run();
|
||||
break;
|
||||
|
||||
case FAIL:
|
||||
showFailureRunnable.run();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void showProgressBar(Activity activity) {
|
||||
if (activity instanceof ProgressDialogActions) {
|
||||
((ProgressDialogActions) activity).showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private static void dismissProgressBar(Activity activity) {
|
||||
if (activity instanceof ProgressDialogActions) {
|
||||
((ProgressDialogActions) activity).dismissProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private static void doFinish(Activity activity) {
|
||||
if (activity instanceof FinishAction) {
|
||||
((FinishAction) activity).doFinish();
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable showFailureRunnable = new Runnable() {
|
||||
|
||||
private int numOfRetries;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final Activity activity = uiInfo.getCurrentActivity();
|
||||
|
||||
if (!uiInfo.isAppVisible()) {
|
||||
FeedbackModule.notifySendCompleted(Result.FAIL);
|
||||
return;
|
||||
} else if (null == activity) {
|
||||
if (numOfRetries < 3) {
|
||||
numOfRetries++;
|
||||
uiNotifier.postDelayed(this, 200);
|
||||
} else {
|
||||
FeedbackModule.notifySendCompleted(Result.FAIL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the code has reached here, then the activity is visible.
|
||||
FeedbackModule.notifySendCompleted(Result.FAIL);
|
||||
|
||||
final Snackbar snackbar = SnackbarBuilder.build(activity, R.string.mk_fb_feedback_failed);
|
||||
final SnackbarCallback callback = new SnackbarCallback();
|
||||
snackbar.addCallback(callback);
|
||||
snackbar.setAction(R.string.mk_fb_retry, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
FeedbackModule.sendFeedback(message, email);
|
||||
// We remove the callback here so we don't release the screenshot
|
||||
snackbar.removeCallback(callback);
|
||||
|
||||
showProgressBar(activity);
|
||||
|
||||
// We have to handle any notifications that the callback would have.
|
||||
FeedbackModule.notificationDismissed();
|
||||
}
|
||||
});
|
||||
|
||||
// Notify Listeners Early of intent
|
||||
FeedbackModule.notificationStarted();
|
||||
dismissProgressBar(activity);
|
||||
|
||||
snackbar.show();
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable showSuccessRunnable = new Runnable() {
|
||||
|
||||
private int numOfRetries;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final Activity activity = uiInfo.getCurrentActivity();
|
||||
|
||||
if (!uiInfo.isAppVisible()) {
|
||||
FeedbackModule.notifySendCompleted(Result.SUCCESS);
|
||||
return;
|
||||
} else if (null == activity) {
|
||||
if (numOfRetries < 3) {
|
||||
numOfRetries++;
|
||||
uiNotifier.postDelayed(this, 200);
|
||||
} else {
|
||||
FeedbackModule.notifySendCompleted(Result.SUCCESS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the code has reached here, then the activity is visible.
|
||||
FeedbackModule.notifySendCompleted(Result.SUCCESS);
|
||||
|
||||
final Snackbar snackbar = SnackbarBuilder.build(activity, R.string.mk_fb_feedback_sent);
|
||||
snackbar.addCallback(new SnackbarCallback() {
|
||||
@Override
|
||||
public void onDismissed(Snackbar snackbar, int event) {
|
||||
super.onDismissed(snackbar, event);
|
||||
doFinish(activity);
|
||||
}
|
||||
});
|
||||
|
||||
// Notify Listeners Early of intent
|
||||
FeedbackModule.notificationStarted();
|
||||
dismissProgressBar(activity);
|
||||
|
||||
snackbar.show();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.atlassian.mobilekit.module.feedback.commands;
|
||||
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.Command;
|
||||
import com.atlassian.mobilekit.module.core.Receiver;
|
||||
import com.atlassian.mobilekit.module.core.UiNotifier;
|
||||
import com.atlassian.mobilekit.module.core.UiReceiver;
|
||||
|
||||
abstract class AbstractCommand<T> implements Command {
|
||||
|
||||
private final Receiver<T> receiver;
|
||||
private final UiNotifier uiNotifier;
|
||||
|
||||
AbstractCommand(Receiver<T> receiver, UiNotifier uiNotifier) {
|
||||
this.receiver = receiver;
|
||||
this.uiNotifier = uiNotifier;
|
||||
}
|
||||
|
||||
void updateReceiver(final T data) {
|
||||
|
||||
if (receiver == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(receiver instanceof UiReceiver) || isMainThread()) {
|
||||
receiver.receive(data);
|
||||
} else {
|
||||
// Post runnable
|
||||
uiNotifier.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
receiver.receive(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMainThread() {
|
||||
return Looper.getMainLooper().getThread() == Thread.currentThread();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.atlassian.mobilekit.module.feedback.commands;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.Receiver;
|
||||
import com.atlassian.mobilekit.module.core.UiNotifier;
|
||||
import com.atlassian.mobilekit.module.feedback.model.FeedbackConfig;
|
||||
import com.atlassian.mobilekit.module.feedback.R;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public final class LoadFeedbackConfigCommand extends AbstractCommand<FeedbackConfig> {
|
||||
|
||||
private static final String LOG_TAG = LoadFeedbackConfigCommand.class.getSimpleName();
|
||||
private final Context context;
|
||||
|
||||
public LoadFeedbackConfigCommand(Context ctx,
|
||||
Receiver<FeedbackConfig> receiver,
|
||||
UiNotifier uiNotifier) {
|
||||
super(receiver, uiNotifier);
|
||||
context = ctx;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
FeedbackConfig config = new FeedbackConfig(
|
||||
context.getString(R.string.mp_feedback_host),
|
||||
context.getString(R.string.mp_feedback_apikey),
|
||||
context.getString(R.string.mp_feedback_projectkey),
|
||||
context.getResources().getStringArray(R.array.mp_feedback_components)
|
||||
);
|
||||
|
||||
String errMsg = errorCheck(config);
|
||||
if (errMsg != null) {
|
||||
// This will crash the app, so that developers can correct their code.
|
||||
throw new IllegalStateException(errMsg);
|
||||
}
|
||||
|
||||
updateReceiver(config);
|
||||
}
|
||||
|
||||
private String errorCheck(FeedbackConfig config) {
|
||||
|
||||
StringBuilder errMsg = new StringBuilder();
|
||||
|
||||
if (TextUtils.isEmpty(config.getHost())) {
|
||||
errMsg.append(getConfigEmptyErrMsg(R.string.mp_feedback_host));
|
||||
} else if (!isValidHost(config.getHost())) {
|
||||
errMsg.append(getConfigInvalidErrMsg(R.string.mp_feedback_host));
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(config.getApiKey())) {
|
||||
errMsg.append(getConfigEmptyErrMsg(R.string.mp_feedback_apikey));
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(config.getProjectKey())) {
|
||||
errMsg.append(getConfigEmptyErrMsg(R.string.mp_feedback_projectkey));
|
||||
}
|
||||
|
||||
if (errMsg.length() > 0) {
|
||||
errMsg.append(context.getString(R.string.mk_fb_config_err_help));
|
||||
return errMsg.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getConfigEmptyErrMsg(int resId) {
|
||||
return context.getString(R.string.mk_fb_no_config_property,
|
||||
context.getResources().getResourceEntryName(resId));
|
||||
}
|
||||
|
||||
private String getConfigInvalidErrMsg(int resId) {
|
||||
return context.getString(R.string.mk_fb_invalid_config_property,
|
||||
context.getResources().getResourceEntryName(resId));
|
||||
}
|
||||
|
||||
private boolean isValidHost(String input) {
|
||||
|
||||
if (TextUtils.isEmpty(input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.indexOf("/") >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = new URI("scheme://" + input);
|
||||
String host = uri.getHost();
|
||||
int port = uri.getPort();
|
||||
|
||||
if (TextUtils.isEmpty(host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(host);
|
||||
|
||||
if (port != -1) {
|
||||
sb.append(":").append(port);
|
||||
}
|
||||
|
||||
return input.equals(sb.toString());
|
||||
|
||||
} catch (URISyntaxException use) {
|
||||
Log.e(LOG_TAG, "URI Validation Failed.", use);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.atlassian.mobilekit.module.feedback.commands;
|
||||
|
||||
|
||||
public enum Result {
|
||||
SUCCESS, FAIL
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package com.atlassian.mobilekit.module.feedback.commands;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.Receiver;
|
||||
import com.atlassian.mobilekit.module.core.UiNotifier;
|
||||
import com.atlassian.mobilekit.module.feedback.FeedbackDataProvider;
|
||||
import com.atlassian.mobilekit.module.feedback.JiraIssueType;
|
||||
import com.atlassian.mobilekit.module.feedback.model.CreateIssueRequest;
|
||||
import com.atlassian.mobilekit.module.feedback.model.CreateIssueResponse;
|
||||
import com.atlassian.mobilekit.module.feedback.network.JmcRestClient;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public final class SendFeedbackCommand extends AbstractCommand<Result> {
|
||||
|
||||
|
||||
private static final String LOG_TAG = SendFeedbackCommand.class.getSimpleName();
|
||||
|
||||
private final Map<String, String> queryMap;
|
||||
private final CreateIssueRequest.Builder requestBuilder;
|
||||
|
||||
private final JmcRestClient restClient;
|
||||
private final FeedbackDataProvider feedbackDataProvider;
|
||||
|
||||
public SendFeedbackCommand(Map<String, String> queryMap,
|
||||
CreateIssueRequest.Builder requestBuilder,
|
||||
FeedbackDataProvider feedbackDataProvider,
|
||||
JmcRestClient restClient,
|
||||
Receiver<Result> receiver,
|
||||
UiNotifier uiNotifier) {
|
||||
|
||||
super(receiver, uiNotifier);
|
||||
this.queryMap = queryMap;
|
||||
this.requestBuilder = requestBuilder;
|
||||
this.feedbackDataProvider = feedbackDataProvider;
|
||||
this.restClient = restClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
JiraIssueType issueType = JiraIssueType.TASK;
|
||||
|
||||
List<MultipartBody.Part> customFieldsPart = new ArrayList<>();
|
||||
if (feedbackDataProvider != null) {
|
||||
final String appendDesc = feedbackDataProvider.getAdditionalDescription();
|
||||
if (!TextUtils.isEmpty(appendDesc)) {
|
||||
requestBuilder.appendToDescription(appendDesc);
|
||||
}
|
||||
|
||||
final JiraIssueType typeFromProvider = feedbackDataProvider.getIssueType();
|
||||
if (typeFromProvider != null) {
|
||||
issueType = typeFromProvider;
|
||||
}
|
||||
|
||||
final Map<String, Object> customFieldsData = feedbackDataProvider.getCustomFieldsData();
|
||||
if(customFieldsData != null) {
|
||||
RequestBody customFieldRequestBody =
|
||||
RequestBody.create(MediaType.parse("application/json"), new Gson().toJson(customFieldsData));
|
||||
|
||||
MultipartBody.Part customFieldPart =
|
||||
MultipartBody.Part.createFormData("customfields", "customfields.json", customFieldRequestBody);
|
||||
customFieldsPart.add(customFieldPart);
|
||||
}
|
||||
|
||||
}
|
||||
requestBuilder.issueType(issueType.toString());
|
||||
|
||||
final CreateIssueRequest request = requestBuilder.build();
|
||||
|
||||
Call<CreateIssueResponse> call = restClient.getJmcApi().createIssue(queryMap, request, Collections.emptyList(), customFieldsPart);
|
||||
try {
|
||||
Response<CreateIssueResponse> response = call.execute();
|
||||
Log.d(LOG_TAG, String.format("Response code %1$d\nmessage %2$s\nbody %3$s",
|
||||
response.code(), response.message(), response.body()));
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
CreateIssueResponse body = response.body();
|
||||
if (body == null) {
|
||||
Log.e(LOG_TAG, "Bad api response. Empty body.");
|
||||
} else if (TextUtils.isEmpty(body.getKey())) {
|
||||
Log.e(LOG_TAG, "Bad api response. Missing Issue Key.");
|
||||
} else {
|
||||
Log.d(LOG_TAG, String.format("New Issue Created %s", body.getKey()));
|
||||
updateReceiver(Result.SUCCESS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.e(LOG_TAG,"Failed to create new issue.", ioe);
|
||||
}
|
||||
|
||||
updateReceiver(Result.FAIL);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
package com.atlassian.mobilekit.module.feedback.model;
|
||||
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.atlassian.mobilekit.module.core.utils.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
public final class CreateIssueRequest {
|
||||
|
||||
@Keep
|
||||
public static class Builder {
|
||||
|
||||
private String type;
|
||||
private String summary;
|
||||
private String description;
|
||||
private boolean isCrash;
|
||||
private String udid;
|
||||
private String uuid;
|
||||
|
||||
private String appName;
|
||||
private String appId;
|
||||
private String appVersion;
|
||||
|
||||
private String systemVersion;
|
||||
private String systemName;
|
||||
private String deviceName;
|
||||
private String model;
|
||||
|
||||
private String language;
|
||||
private List<String> components;
|
||||
|
||||
public Builder() {
|
||||
|
||||
}
|
||||
|
||||
public Builder issueType(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder summary(String summary) {
|
||||
this.summary = summary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder appendToDescription(String moreInfo) {
|
||||
if (!TextUtils.isEmpty(description)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(StringUtils.EOL).append(StringUtils.EOL).append(moreInfo);
|
||||
description += sb.toString();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder isCrash(boolean crash) {
|
||||
isCrash = crash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder udid(String udid) {
|
||||
this.udid = udid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder uuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder appName(String appName) {
|
||||
this.appName = appName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder appId(String appId) {
|
||||
this.appId = appId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder appVersion(String appVersion) {
|
||||
this.appVersion = appVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder systemVersion(String systemVersion) {
|
||||
this.systemVersion = systemVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder systemName(String systemName) {
|
||||
this.systemName = systemName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder deviceName(String devName) {
|
||||
this.deviceName = devName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder model(String model) {
|
||||
this.model = model;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder language(String language) {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder components(List<String> components) {
|
||||
this.components = components;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CreateIssueRequest build() {
|
||||
return new CreateIssueRequest(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final int MAX_SUMMARY_LENGTH = 240;
|
||||
private final String type;
|
||||
private final String summary;
|
||||
private final String description;
|
||||
private final boolean isCrash;
|
||||
private final String udid;
|
||||
private final String uuid;
|
||||
|
||||
private final String appName;
|
||||
private final String appId;
|
||||
private final String appVersion;
|
||||
|
||||
private final String systemVersion;
|
||||
private final String systemName;
|
||||
|
||||
// This is actually DeviceName.
|
||||
// *** But this declaration cannot be changed since the Server API expects it to be 'devName'
|
||||
private final String devName;
|
||||
|
||||
private final String model;
|
||||
|
||||
private final String language;
|
||||
|
||||
private final List<String> components;
|
||||
|
||||
public CreateIssueRequest(Builder builder) {
|
||||
this.type = builder.type;
|
||||
this.summary = StringUtils.ellipsize(builder.summary, MAX_SUMMARY_LENGTH);
|
||||
this.description = builder.description;
|
||||
this.isCrash = builder.isCrash;
|
||||
this.udid = builder.udid;
|
||||
this.uuid = builder.uuid;
|
||||
this.appName = builder.appName;
|
||||
this.appId = builder.appId;
|
||||
this.appVersion = builder.appVersion;
|
||||
this.systemVersion = builder.systemVersion;
|
||||
this.systemName = builder.systemName;
|
||||
this.devName = builder.deviceName;
|
||||
this.model = builder.model;
|
||||
this.language = builder.language;
|
||||
this.components = builder.components;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isCrash() {
|
||||
return isCrash;
|
||||
}
|
||||
|
||||
public String getUdid() {
|
||||
return udid;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return appName;
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
public String getAppVersion() {
|
||||
return appVersion;
|
||||
}
|
||||
|
||||
public String getSystemVersion() {
|
||||
return systemVersion;
|
||||
}
|
||||
|
||||
public String getSystemName() {
|
||||
return systemName;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
return devName;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public List<String> getComponents() {
|
||||
return components;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.atlassian.mobilekit.module.feedback.model;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
public final class CreateIssueResponse {
|
||||
|
||||
private String key;
|
||||
private String status;
|
||||
private String summary;
|
||||
private String description;
|
||||
private long dateUpdated;
|
||||
private long dateCreated;
|
||||
private boolean hasUpdates;
|
||||
private List<String> comments;
|
||||
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public long getDateUpdated() {
|
||||
return dateUpdated;
|
||||
}
|
||||
|
||||
public long getDateCreated() {
|
||||
return dateCreated;
|
||||
}
|
||||
|
||||
public boolean hasUpdates() {
|
||||
return hasUpdates;
|
||||
}
|
||||
|
||||
public List<String> getComments() {
|
||||
return comments;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.atlassian.mobilekit.module.feedback.model;
|
||||
|
||||
|
||||
public final class FeedbackConfig {
|
||||
|
||||
private final String host;
|
||||
private final String apiKey;
|
||||
private final String projectKey;
|
||||
private final String[] components;
|
||||
|
||||
public FeedbackConfig(String host, String apiKey, String projectKey, String[] components) {
|
||||
this.host = host;
|
||||
this.apiKey = apiKey;
|
||||
this.projectKey = projectKey;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public String getProjectKey() {
|
||||
return projectKey;
|
||||
}
|
||||
|
||||
public String[] getComponents() {
|
||||
return components;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.atlassian.mobilekit.module.feedback.network;
|
||||
|
||||
|
||||
public interface BaseApiParams {
|
||||
|
||||
String API_KEY = "apikey";
|
||||
String PROJECT = "project";
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.atlassian.mobilekit.module.feedback.network;
|
||||
|
||||
|
||||
import com.atlassian.mobilekit.module.feedback.model.CreateIssueRequest;
|
||||
import com.atlassian.mobilekit.module.feedback.model.CreateIssueResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Part;
|
||||
import retrofit2.http.QueryMap;
|
||||
|
||||
public interface JmcApi {
|
||||
|
||||
@Multipart
|
||||
@POST("rest/jconnect/latest/issue/create")
|
||||
@Keep
|
||||
Call<CreateIssueResponse> createIssue(
|
||||
@QueryMap Map<String, String> params,
|
||||
@Part("issue") CreateIssueRequest request,
|
||||
@Part List<MultipartBody.Part> screenshotPart,
|
||||
@Part List<MultipartBody.Part> customFields);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.atlassian.mobilekit.module.feedback.network;
|
||||
|
||||
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public final class JmcRestClient {
|
||||
|
||||
private JmcApi jmcApi = null;
|
||||
|
||||
public JmcRestClient() {
|
||||
|
||||
}
|
||||
|
||||
public void init(String protocol, String host) {
|
||||
|
||||
final String baseUrl = new StringBuilder()
|
||||
.append(protocol)
|
||||
.append(host)
|
||||
.append("/")
|
||||
.toString();
|
||||
|
||||
final Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build();
|
||||
|
||||
jmcApi = retrofit.create(JmcApi.class);
|
||||
}
|
||||
|
||||
public JmcApi getJmcApi() {
|
||||
return jmcApi;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Disabled background -->
|
||||
<item android:state_enabled="false"
|
||||
android:color="@color/N70"/>
|
||||
|
||||
<!-- Enabled background -->
|
||||
<item android:color="@color/B300"/>
|
||||
</selector>
|
16
feedback-android/src/main/res/drawable/ic_send.xml
Normal file
16
feedback-android/src/main/res/drawable/ic_send.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M0,0h24v24h-24z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.01"/>
|
||||
<path
|
||||
android:pathData="M6.9782,19.477C6.9782,20.848 8.6232,21.519 9.5552,20.527L21.5582,7.723C22.5182,6.695 21.8032,5 20.4092,5H3.4962C2.1782,5 1.5052,6.607 2.4182,7.572L6.9782,12.4V19.477ZM8.9712,18.212V11.585L3.8562,6.169C4.1592,6.491 3.9342,7.026 3.4962,7.026H19.4592L8.9712,18.212Z"
|
||||
android:fillColor="#024B7E"/>
|
||||
<path
|
||||
android:pathData="M8.4162,12.902L12.4262,10.952C12.6667,10.8303 12.8505,10.6199 12.9389,10.3653C13.0274,10.1106 13.0135,9.8316 12.9002,9.587C12.8471,9.4665 12.7703,9.3579 12.6743,9.2677C12.5784,9.1775 12.4653,9.1075 12.3418,9.0619C12.2183,9.0163 12.0868,8.9959 11.9553,9.0022C11.8238,9.0084 11.6948,9.041 11.5762,9.098L7.5662,11.048C7.0692,11.29 6.8562,11.901 7.0912,12.412C7.1442,12.5327 7.2209,12.6415 7.3169,12.7319C7.4128,12.8223 7.526,12.8925 7.6497,12.9382C7.7733,12.9839 7.9049,13.0043 8.0366,12.9981C8.1683,12.9918 8.2974,12.9592 8.4162,12.902Z"
|
||||
android:fillColor="#024B7E"/>
|
||||
</vector>
|
18
feedback-android/src/main/res/drawable/ic_send_disabled.xml
Normal file
18
feedback-android/src/main/res/drawable/ic_send_disabled.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:pathData="M6.9782,19.477C6.9782,20.848 8.6232,21.519 9.5552,20.527L21.5582,7.723C22.5182,6.695 21.8032,5 20.4092,5H3.4962C2.1782,5 1.5052,6.607 2.4182,7.572L6.9782,12.4V19.477ZM8.9712,18.212V11.585L3.8562,6.169C4.1592,6.491 3.9342,7.026 3.4962,7.026H19.4592L8.9712,18.212Z"
|
||||
android:fillColor="#024B7E"/>
|
||||
<path
|
||||
android:pathData="M8.4162,12.902L12.4262,10.952C12.6667,10.8303 12.8505,10.6199 12.9389,10.3653C13.0274,10.1106 13.0135,9.8316 12.9002,9.587C12.8471,9.4665 12.7703,9.3579 12.6743,9.2677C12.5784,9.1775 12.4653,9.1075 12.3418,9.0619C12.2183,9.0163 12.0868,8.9959 11.9553,9.0022C11.8238,9.0084 11.6948,9.041 11.5762,9.098L7.5662,11.048C7.0692,11.29 6.8562,11.901 7.0912,12.412C7.1442,12.5327 7.2209,12.6415 7.3169,12.7319C7.4128,12.8223 7.526,12.8925 7.6497,12.9382C7.7733,12.9839 7.9049,13.0043 8.0366,12.9981C8.1683,12.9918 8.2974,12.9592 8.4162,12.902Z"
|
||||
android:fillColor="#024B7E"/>
|
||||
|
||||
<path
|
||||
android:pathData="M0,0h24v24h-24z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.5"/>
|
||||
</vector>
|
7
feedback-android/src/main/res/drawable/send_selector.xml
Normal file
7
feedback-android/src/main/res/drawable/send_selector.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:state_enabled="true" android:drawable="@drawable/ic_send" />
|
||||
|
||||
<item android:state_enabled="false" android:drawable="@drawable/ic_send_disabled" />
|
||||
</selector>
|
25
feedback-android/src/main/res/layout/activity_feedback.xml
Normal file
25
feedback-android/src/main/res/layout/activity_feedback.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.atlassian.mobilekit.module.feedback.FeedbackActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@android:color/white"
|
||||
app:title="@string/report_issue"
|
||||
app:popupTheme="@style/MobileKit.FeedbackTheme.PopupOverlay" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<include layout="@layout/content_feedback" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
87
feedback-android/src/main/res/layout/content_feedback.xml
Normal file
87
feedback-android/src/main/res/layout/content_feedback.xml
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:context="com.atlassian.mobilekit.module.feedback.FeedbackActivity"
|
||||
tools:showIn="@layout/activity_feedback">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/feedback_content_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
tools:minHeight="200dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/feedback_content_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/feedback_found_an_issue_in_the_covidsafe_app"
|
||||
android:textColor="#141515"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/feedbackIssueDescriptionLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/feedback_content_title">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/feedbackIssueDescriptionEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/feedback_please_describe_an_issue"
|
||||
android:inputType="textMultiLine|textAutoComplete|textAutoCorrect|textCapSentences"
|
||||
android:textSize="16sp"
|
||||
tools:text="This applications is so awesome, just wanted to express my gratitude" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/feedbackIssueEmailLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:hintEnabled="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/feedbackIssueDescriptionLayout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/feedbackIssueEmailEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/feedback_email_address_required"
|
||||
android:imeOptions="actionSend"
|
||||
android:inputType="textWebEmailAddress"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
android:autofillHints="emailAddress"
|
||||
tools:text="ylaguta@atlassian.com" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingBottom="32dp"
|
||||
android:text="@string/feedback_we_may_reach_out_to_you_for_further_details_about_your_feedback"
|
||||
android:textColor="#788586"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/feedbackIssueEmailLayout" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/positive_btn"
|
||||
style="@style/MobileKit.DialogButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="@dimen/mk_button_right_margin"
|
||||
android:layout_marginEnd="@dimen/mk_button_right_margin"
|
||||
android:layout_below="@id/dialog_container"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:text="" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/negative_btn"
|
||||
style="@style/MobileKit.DialogButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="@dimen/mk_button_right_margin"
|
||||
android:layout_marginEnd="@dimen/mk_button_right_margin"
|
||||
android:layout_below="@id/dialog_container"
|
||||
android:layout_toLeftOf="@id/positive_btn"
|
||||
android:layout_toStartOf="@id/positive_btn"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:text=""
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include layout="@layout/mk_feedback_dialog_title" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/MobileKit.Text.Dialog"
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@id/title"
|
||||
android:paddingBottom="@dimen/mk_content_bottom_padding"
|
||||
android:paddingEnd="@dimen/mk_title_horizontal_padding"
|
||||
android:paddingLeft="@dimen/mk_title_horizontal_padding"
|
||||
android:paddingRight="@dimen/mk_title_horizontal_padding"
|
||||
android:paddingStart="@dimen/mk_title_horizontal_padding"
|
||||
android:text="" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignBottom="@id/title"
|
||||
android:layout_alignLeft="@id/title"
|
||||
android:layout_alignRight="@id/title"
|
||||
android:layout_alignTop="@id/title"
|
||||
android:background="@android:color/transparent"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/title"
|
||||
style="@style/MobileKit.TitleStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:paddingBottom="@dimen/mk_title_bottom_padding"
|
||||
android:paddingEnd="@dimen/mk_title_horizontal_padding"
|
||||
android:paddingLeft="@dimen/mk_title_horizontal_padding"
|
||||
android:paddingRight="@dimen/mk_title_horizontal_padding"
|
||||
android:paddingStart="@dimen/mk_title_horizontal_padding"
|
||||
android:paddingTop="@dimen/mk_title_top_padding"
|
||||
android:text=""
|
||||
android:textColor="@android:color/black" />
|
||||
</merge>
|
10
feedback-android/src/main/res/menu/menu_feedback.xml
Normal file
10
feedback-android/src/main/res/menu/menu_feedback.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="com.atlassian.mobilekit.module.feedback.FeedbackActivity">
|
||||
<item
|
||||
android:id="@+id/action_send"
|
||||
android:icon="@drawable/send_selector"
|
||||
android:title="@string/mk_fb_send"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
6
feedback-android/src/main/res/values-w820dp/dimens.xml
Normal file
6
feedback-android/src/main/res/values-w820dp/dimens.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<resources>
|
||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||
(such as screen margins) for screens with more than 820dp of available width. This
|
||||
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
|
||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||
</resources>
|
6
feedback-android/src/main/res/values/colors.xml
Normal file
6
feedback-android/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="dialog_content_text">#5a697c</color>
|
||||
<color name="N70">#A5ADBA</color>
|
||||
<color name="B300">#0065FF</color>
|
||||
</resources>
|
13
feedback-android/src/main/res/values/dimens.xml
Normal file
13
feedback-android/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
||||
<dimen name="mk_title_horizontal_padding">24dp</dimen>
|
||||
<dimen name="mk_title_top_padding">24dp</dimen>
|
||||
<dimen name="mk_title_bottom_padding">20dp</dimen>
|
||||
<dimen name="mk_content_bottom_padding">24dp</dimen>
|
||||
|
||||
<dimen name="mk_button_right_margin">8dp</dimen>
|
||||
|
||||
</resources>
|
4
feedback-android/src/main/res/values/fonts.xml
Normal file
4
feedback-android/src/main/res/values/fonts.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="font_fontFamily_medium" translatable="false">sans-serif</string>
|
||||
</resources>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources tools:ignore="MissingTranslation" xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="mp_feedback_host" />
|
||||
<string name="mp_feedback_apikey" />
|
||||
<string name="mp_feedback_projectkey" />
|
||||
<string-array name="mp_feedback_components">
|
||||
<item>Android</item>
|
||||
</string-array>
|
||||
</resources>
|
25
feedback-android/src/main/res/values/strings.xml
Normal file
25
feedback-android/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<resources>
|
||||
|
||||
<!-- Configurations -->
|
||||
<string name="mk_fb_no_config_property">\'%s\' configuration property value is empty.</string>
|
||||
<string name="mk_fb_config_err_help">Cannot create feedback. Please check mp_feedback_config.xml in your application resources.</string>
|
||||
<string name="mk_fb_invalid_config_property">\'%s\' configuration property value is invalid.</string>
|
||||
|
||||
<!-- Internal strings -->
|
||||
<string name="report_issue">Report an issue</string>
|
||||
<string name="mk_fb_send">Send</string>
|
||||
<string name="mk_fb_feedback_empty">Tell us something before sending.</string>
|
||||
|
||||
<string name="mk_fb_feedback_sent">Feedback sent</string>
|
||||
<string name="mk_fb_feedback_failed">Sending feedback failed</string>
|
||||
<string name="mk_fb_retry">Retry</string>
|
||||
|
||||
<string name="mk_fb_device_offline">Your device is offline.</string>
|
||||
<string name="mk_fb_invalid_email_address">Invalid email address</string>
|
||||
<string name="mk_fb_sending">Sending…</string>
|
||||
<string name="feedback_found_an_issue_in_the_covidsafe_app">Found an issue in the COVIDSafe app?</string>
|
||||
<string name="feedback_please_describe_an_issue">Please describe an issue</string>
|
||||
<string name="feedback_email_address_required">Email address (Required)</string>
|
||||
<string name="feedback_we_may_reach_out_to_you_for_further_details_about_your_feedback">We may reach out to you for further details about your feedback.\nYour email address won`t be used for any other purpose.</string>
|
||||
|
||||
</resources>
|
31
feedback-android/src/main/res/values/styles.xml
Normal file
31
feedback-android/src/main/res/values/styles.xml
Normal file
|
@ -0,0 +1,31 @@
|
|||
<resources>
|
||||
|
||||
<style name="MobileKit.FeedbackTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||
|
||||
<style name="MobileKit.TitleStyle" parent="TextAppearance.AppCompat">
|
||||
<item name="android:fontFamily">@string/font_fontFamily_medium</item>
|
||||
<item name="android:textSize">20sp</item>
|
||||
<item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
|
||||
</style>
|
||||
|
||||
<style name="MobileKit.DialogButton" parent="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog">
|
||||
<item name="android:textColor">@drawable/feedback_button_text_state</item>
|
||||
<item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
|
||||
<item name="android:textAllCaps">true</item>
|
||||
<item name="android:textStyle">normal</item>
|
||||
</style>
|
||||
|
||||
<style name="MobileKit.Text" parent="TextAppearance.AppCompat">
|
||||
<!-- Roboto Regular -->
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
|
||||
<style name="MobileKit.Text.Dialog" parent="MobileKit.Text">
|
||||
<!-- Roboto Regular -->
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textColor">@color/dialog_content_text</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue