diff --git a/shaky/src/main/java/com/linkedin/android/shaky/AttachmentData.java b/shaky/src/main/java/com/linkedin/android/shaky/AttachmentData.java new file mode 100644 index 0000000..e7f2c26 --- /dev/null +++ b/shaky/src/main/java/com/linkedin/android/shaky/AttachmentData.java @@ -0,0 +1,43 @@ +package com.linkedin.android.shaky; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +class AttachmentData { + + final Uri uri; + final String displayName; + final boolean removable; + + AttachmentData(@NonNull Uri uri) { + this(uri, null, false); + } + + AttachmentData(@NonNull Uri uri, @Nullable String displayName, boolean removable) { + this.uri = uri; + this.displayName = displayName; + this.removable = removable; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AttachmentData that = (AttachmentData) o; + + return uri.equals(that.uri); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + + +} diff --git a/shaky/src/main/java/com/linkedin/android/shaky/AttachmentListAdapter.java b/shaky/src/main/java/com/linkedin/android/shaky/AttachmentListAdapter.java new file mode 100644 index 0000000..26b8cca --- /dev/null +++ b/shaky/src/main/java/com/linkedin/android/shaky/AttachmentListAdapter.java @@ -0,0 +1,65 @@ +package com.linkedin.android.shaky; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import java.util.ArrayList; + +class AttachmentListAdapter extends ArrayAdapter { + + private final AttachmentViewHolder.OnAttachmentClickListener onAttachmentClickListener; + + AttachmentListAdapter(@NonNull Context context, + @NonNull AttachmentViewHolder.OnAttachmentClickListener onAttachmentClickListener) { + super(context, 0); + this.onAttachmentClickListener = onAttachmentClickListener; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(parent.getContext()).inflate( + R.layout.shaky_attachment_view, parent, false); + AttachmentViewHolder viewHolder = new AttachmentViewHolder(convertView, onAttachmentClickListener); + convertView.setTag(viewHolder); + } + AttachmentViewHolder viewHolder = (AttachmentViewHolder) convertView.getTag(); + if (viewHolder != null) { + viewHolder.bind(getItem(position)); + } + return convertView; + } + + boolean containsAttachmentUri(@NonNull Uri uri) { + int size = getCount(); + AttachmentData data; + for (int i = 1; i < size; i++) { + data = getItem(i); + if (data != null && uri.equals(data.uri)) { + return true; + } + } + return false; + } + + ArrayList getAttachmentUriList() { + int size = getCount(); + AttachmentData data; + ArrayList list = new ArrayList<>(size); + // Do not add screenshot + for (int i = 1; i < size; i++) { + data = getItem(i); + if (data != null) { + list.add(data.uri); + } + } + return list; + } +} diff --git a/shaky/src/main/java/com/linkedin/android/shaky/AttachmentViewHolder.java b/shaky/src/main/java/com/linkedin/android/shaky/AttachmentViewHolder.java new file mode 100644 index 0000000..19e00fe --- /dev/null +++ b/shaky/src/main/java/com/linkedin/android/shaky/AttachmentViewHolder.java @@ -0,0 +1,87 @@ +package com.linkedin.android.shaky; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Html; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +class AttachmentViewHolder { + + interface OnAttachmentClickListener { + + void onRemoved(@NonNull Uri fileUri); + + void onClicked(@NonNull Uri fileUri); + } + + private static final String IMAGE_PREFIX = "image/"; + private final TextView filenameView; + private final ImageView thumbnailView; + private final ImageView deleteIcon; + private final View rootView; + + AttachmentViewHolder(@NonNull View view, @NonNull final OnAttachmentClickListener listener) { + filenameView = (TextView) view.findViewById(R.id.filename_view); + deleteIcon = (ImageView) view.findViewById(R.id.delete_icon); + thumbnailView = (ImageView) view.findViewById(R.id.thumbnail_view); + rootView = view; + filenameView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getTag() instanceof Uri) { + listener.onClicked((Uri) v.getTag()); + } + } + }); + thumbnailView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getTag() instanceof Uri) { + listener.onClicked((Uri) v.getTag()); + } + } + }); + deleteIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getTag() instanceof Uri) { + listener.onRemoved((Uri) v.getTag()); + } + } + }); + } + + private void bind(@NonNull Uri fileUri, @Nullable String defaultFilename, boolean removable) { + filenameView.setText(Html.fromHtml(getFilename(fileUri, defaultFilename))); + deleteIcon.setVisibility(removable ? View.VISIBLE : View.GONE); + filenameView.setTag(fileUri); + deleteIcon.setTag(fileUri); + thumbnailView.setTag(fileUri); + String mimeType = Utils.getMimeType(rootView.getContext(), fileUri); + if (mimeType != null && mimeType.startsWith(IMAGE_PREFIX)) { + thumbnailView.setVisibility(View.VISIBLE); + thumbnailView.setImageURI(fileUri); + } else { + thumbnailView.setVisibility(View.GONE); + } + } + + void bind(@Nullable AttachmentData item) { + if (item == null) { + // edge case + rootView.setVisibility(View.GONE); + } else { + rootView.setVisibility(View.VISIBLE); + bind(item.uri, item.displayName, item.removable); + } + } + + private String getFilename(@NonNull Uri fileUri, @Nullable String defaultFilename) { + return TextUtils.isEmpty(defaultFilename) ? Utils.getFilename(rootView.getContext(), fileUri) + : defaultFilename; + } +} \ No newline at end of file diff --git a/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java b/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java index 15063e7..dd88323 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java @@ -21,6 +21,7 @@ import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; +import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; @@ -29,6 +30,8 @@ import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AppCompatActivity; +import java.util.ArrayList; + /** * The main activity used capture and send feedback. */ @@ -40,18 +43,27 @@ public class FeedbackActivity extends AppCompatActivity { static final String MESSAGE = "message"; static final String TITLE = "title"; static final String USER_DATA = "userData"; + static final String EXTRA_ATTACHMENTS = "extraAttachments"; + static final String ADD_ATTACHMENT = "addAttachment"; private Uri imageUri; private @FeedbackItem.FeedbackType int feedbackType; private Bundle userData; + private boolean addAttachmentShown; + + // Replace a fragment won't call fragment.onSaveInstanceState() so we need to have a activity scope states + // to maintain the fragment's states, such as attachments + final Bundle fragmentStates = new Bundle(); @NonNull public static Intent newIntent(@NonNull Context context, @Nullable Uri screenshotUri, - @Nullable Bundle userData) { + @Nullable Bundle userData, + boolean addAttachmentShown) { Intent intent = new Intent(context, FeedbackActivity.class); intent.putExtra(SCREENSHOT_URI, screenshotUri); intent.putExtra(USER_DATA, userData); + intent.putExtra(ADD_ATTACHMENT, addAttachmentShown); return intent; } @@ -63,7 +75,7 @@ public void onCreate(Bundle savedInstanceState) { imageUri = getIntent().getParcelableExtra(SCREENSHOT_URI); userData = getIntent().getBundleExtra(USER_DATA); - + addAttachmentShown = getIntent().getBooleanExtra(ADD_ATTACHMENT, false); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() @@ -110,7 +122,7 @@ private void changeToFragment(@NonNull Fragment fragment) { private void startFormFragment(@FeedbackItem.FeedbackType int feedbackType) { String title = getString(getTitleResId(feedbackType)); String hint = getString(getHintResId(feedbackType)); - changeToFragment(FormFragment.newInstance(title, hint, imageUri)); + changeToFragment(FormFragment.newInstance(title, hint, imageUri, addAttachmentShown)); } /** @@ -142,18 +154,23 @@ public void onReceive(Context context, Intent intent) { } else if (DrawFragment.ACTION_DRAWING_COMPLETE.equals(intent.getAction())) { onBackPressed(); } else if (FormFragment.ACTION_SUBMIT_FEEDBACK.equals(intent.getAction())) { - submitFeedbackIntent(intent.getStringExtra(FormFragment.EXTRA_USER_MESSAGE)); + submitFeedbackIntent(intent.getStringExtra(FormFragment.EXTRA_USER_MESSAGE), + intent.getParcelableArrayListExtra(FormFragment.EXTRA_ATTACHMENTS)); } } }; - private void submitFeedbackIntent(@Nullable String userMessage) { + private void submitFeedbackIntent(@Nullable String userMessage, + @Nullable ArrayList attachments) { Intent intent = new Intent(ACTION_END_FEEDBACK_FLOW); intent.putExtra(SCREENSHOT_URI, imageUri); intent.putExtra(TITLE, getString(getTitleResId(feedbackType))); intent.putExtra(MESSAGE, userMessage); intent.putExtra(USER_DATA, userData); + if (attachments != null && !attachments.isEmpty()) { + intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, attachments); + } LocalBroadcastManager.getInstance(this).sendBroadcast(intent); finish(); diff --git a/shaky/src/main/java/com/linkedin/android/shaky/FormFragment.java b/shaky/src/main/java/com/linkedin/android/shaky/FormFragment.java index 1a914ad..4cd9d14 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/FormFragment.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/FormFragment.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,9 +15,13 @@ */ package com.linkedin.android.shaky; +import android.app.Activity; +import android.content.ClipData; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -29,8 +33,13 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; /** * The main form used to send feedback. @@ -41,20 +50,44 @@ public class FormFragment extends Fragment { static final String ACTION_EDIT_IMAGE = "ActionEditImage"; static final String EXTRA_USER_MESSAGE = "ExtraUserMessage"; + static final String EXTRA_ATTACHMENTS = "ExtraAttachments"; private static final String KEY_SCREENSHOT_URI = "ScreenshotUri"; private static final String KEY_TITLE = "title"; private static final String KEY_HINT = "hint"; + private static final String KEY_ADD_ATTACHMENT = "addAttachment"; + private static final String ALL = "*/*"; + private static final int ATTACHMENT_REQUEST_CODE = 0x1234; + private static final String SCREENSHOT = "screenshot.png"; + private final AttachmentViewHolder.OnAttachmentClickListener onAttachmentClickListener + = new AttachmentViewHolder.OnAttachmentClickListener() { + @Override + public void onRemoved(@NonNull Uri fileUri) { + removeAttachment(fileUri); + } + + @Override + public void onClicked(@NonNull Uri fileUri) { + if (fileUri.equals(screenshotUri)) { + editScreenshot(); + } + } + }; + private Uri screenshotUri; + private AttachmentListAdapter attachmentListAdapter; public static FormFragment newInstance(@NonNull String title, @NonNull String hint, - @Nullable Uri screenshotUri) { + @Nullable Uri screenshotUri, + boolean addAttachmentShown) { Bundle args = new Bundle(); args.putParcelable(KEY_SCREENSHOT_URI, screenshotUri); args.putString(KEY_TITLE, title); args.putString(KEY_HINT, hint); + args.putBoolean(KEY_ADD_ATTACHMENT, addAttachmentShown); FormFragment fragment = new FormFragment(); + fragment.setRetainInstance(true); fragment.setArguments(args); return fragment; } @@ -71,10 +104,12 @@ public void onViewCreated(View view, Bundle savedInstanceState) { Toolbar toolbar = (Toolbar) view.findViewById(R.id.shaky_toolbar); EditText messageEditText = (EditText) view.findViewById(R.id.shaky_form_message); + ListView attachmentsView = (ListView) view.findViewById(R.id.attachments_view); ImageView attachmentImageView = (ImageView) view.findViewById(R.id.shaky_form_attachment); - - Uri screenshotUri = getArguments().getParcelable(KEY_SCREENSHOT_URI); - + Button addAttachmentButton = (Button) view.findViewById(R.id.add_attachment_button); + View screenshotView = view.findViewById(R.id.screenshot_view); + screenshotUri = getArguments().getParcelable(KEY_SCREENSHOT_URI); + boolean addAttachmentShown = getArguments().getBoolean(KEY_ADD_ATTACHMENT, false); String title = getArguments().getString(KEY_TITLE); toolbar.setTitle(title); toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp); @@ -86,8 +121,113 @@ public void onViewCreated(View view, Bundle savedInstanceState) { messageEditText.setHint(hint); messageEditText.requestFocus(); - attachmentImageView.setImageURI(screenshotUri); - attachmentImageView.setOnClickListener(createNavigationClickListener()); + if (addAttachmentShown) { + attachmentsView.setVisibility(View.VISIBLE); + addAttachmentButton.setVisibility(View.VISIBLE); + addAttachmentButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showDocumentPicker(); + } + }); + screenshotView.setVisibility(View.GONE); + attachmentListAdapter = new AttachmentListAdapter(getContext(), onAttachmentClickListener); + attachmentsView.setAdapter(attachmentListAdapter); + addAttachment(screenshotUri, SCREENSHOT, false); + if (savedInstanceState == null) { + // try to restore from activity-scope states + if (getActivity() instanceof FeedbackActivity) { + restoreAttachmentsState(((FeedbackActivity) getActivity()).fragmentStates); + } + } else { + // restore from instance states + restoreAttachmentsState(savedInstanceState); + } + } else { + screenshotView.setVisibility(View.VISIBLE); + addAttachmentButton.setVisibility(View.GONE); + attachmentImageView.setImageURI(screenshotUri); + attachmentImageView.setOnClickListener(createNavigationClickListener()); + attachmentsView.setVisibility(View.GONE); + } + } + + private void showDocumentPicker() { + Intent intent = new Intent(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + ? Intent.ACTION_OPEN_DOCUMENT + : Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(ALL); + // only choosing from local storage due to permission issues + // and lack of loading progress when downloading from a network location + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } + startActivityForResult(intent, ATTACHMENT_REQUEST_CODE); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (outState != null && attachmentListAdapter != null) { + saveAttachmentsState(outState); + } + } + + private void saveAttachmentsState(@NonNull Bundle outState) { + outState.putParcelableArrayList(EXTRA_ATTACHMENTS, attachmentListAdapter.getAttachmentUriList()); + } + + private void restoreAttachmentsState(@NonNull Bundle states) { + ArrayList attachments = states.getParcelableArrayList(EXTRA_ATTACHMENTS); + if (attachments != null) { + for (Uri uri : attachments) { + addAttachment(uri, null, true); + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == ATTACHMENT_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + List list = new ArrayList<>(); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN + && intent.getClipData() != null) { + ClipData clipData = intent.getClipData(); + for (int i = 0, size = clipData.getItemCount(); i < size; i++) { + list.add(clipData.getItemAt(i).getUri()); + } + } else if (intent.getData() != null) { + list.add(intent.getData()); + } + Context context = getContext(); + for (Uri uri : list) { + addAttachment(uri, null, true); + } + } + } else { + super.onActivityResult(requestCode, resultCode, intent); + } + } + + private void addAttachment(@Nullable Uri fileUri, @Nullable String defaultFilename, boolean removable) { + if (fileUri == null || attachmentListAdapter == null) { + return; + } + if (!attachmentListAdapter.containsAttachmentUri(fileUri)) { + // avoid duplicated attachments + attachmentListAdapter.add(new AttachmentData(fileUri, defaultFilename, removable)); + } + } + + private void removeAttachment(@NonNull Uri fileUri) { + if (attachmentListAdapter != null) { + attachmentListAdapter.remove(new AttachmentData(fileUri)); + } } @NonNull @@ -95,12 +235,20 @@ private View.OnClickListener createNavigationClickListener() { return new View.OnClickListener() { @Override public void onClick(View v) { - Intent intent = new Intent(ACTION_EDIT_IMAGE); - LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); + editScreenshot(); } }; } + private void editScreenshot() { + // replace fragments won't call onSaveInstanceStates() so we want to store the attachments within activity + if (attachmentListAdapter != null && getActivity() instanceof FeedbackActivity) { + saveAttachmentsState(((FeedbackActivity) getActivity()).fragmentStates); + } + Intent intent = new Intent(ACTION_EDIT_IMAGE); + LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); + } + @NonNull private Toolbar.OnMenuItemClickListener createMenuClickListener(@NonNull final EditText messageEditText) { return new Toolbar.OnMenuItemClickListener() { @@ -112,6 +260,10 @@ public boolean onMenuItemClick(MenuItem item) { if (validate(message)) { Intent intent = new Intent(ACTION_SUBMIT_FEEDBACK); intent.putExtra(EXTRA_USER_MESSAGE, message); + if (attachmentListAdapter != null) { + intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, + attachmentListAdapter.getAttachmentUriList()); + } LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); return true; } @@ -126,15 +278,16 @@ public boolean onMenuItemClick(MenuItem item) { */ private boolean validate(@NonNull String message) { if (message.trim().length() == 0) { - AlertDialog alertDialog = new AlertDialog.Builder(getActivity()).create(); - alertDialog.setMessage(getString(R.string.shaky_empty_feedback_message)); - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.shaky_empty_feedback_confirm), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - alertDialog.show(); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), + R.style.Theme_AppCompat_Light_Dialog_Alert); + builder.setMessage(getString(R.string.shaky_empty_feedback_message)); + builder.setPositiveButton(getString(R.string.shaky_empty_feedback_confirm), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + builder.show(); return false; } diff --git a/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java b/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java index e0fb791..794e457 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java @@ -69,4 +69,11 @@ public void collectData(Activity activity, Result data) { * This method can be overridden to send data to a custom URL endpoint, etc. */ public abstract void submit(Activity activity, Result result); + + /** + * @return true if more attachments are allowed to be attached to the reports + */ + public boolean isAddAttachmentFeatureEnabled() { + return false; + } } diff --git a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java index 457457b..f60d307 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java @@ -24,6 +24,7 @@ import android.graphics.Bitmap; import android.hardware.SensorManager; import android.net.Uri; +import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; @@ -224,7 +225,8 @@ public void onDataReady(@Nullable Result result) { * Launches the main feedback activity with the bundle extra data. */ private void startFeedbackActivity(@NonNull Result result) { - Intent intent = FeedbackActivity.newIntent(activity, result.getScreenshotUri(), result.getData()); + Intent intent = FeedbackActivity.newIntent(activity, result.getScreenshotUri(), result.getData(), + delegate.isAddAttachmentFeatureEnabled()); activity.startActivity(intent); } @@ -239,6 +241,15 @@ private Result unpackResult(Intent intent) { for (Uri attachment : result.getAttachments()) { fileProviderAttachments.add(Utils.getProviderUri(activity, attachment)); } + // add extra attachments provided by the user + ArrayList extraAttachments = intent.getParcelableArrayListExtra(FeedbackActivity.EXTRA_ATTACHMENTS); + if (extraAttachments != null && !extraAttachments.isEmpty()) { + for (Parcelable parcelable: extraAttachments) { + if (parcelable instanceof Uri) { + fileProviderAttachments.add((Uri) parcelable); + } + } + } result.setAttachments(fileProviderAttachments); return result; diff --git a/shaky/src/main/java/com/linkedin/android/shaky/Utils.java b/shaky/src/main/java/com/linkedin/android/shaky/Utils.java index aefce4e..4ecdb29 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/Utils.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/Utils.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,27 +15,37 @@ */ package com.linkedin.android.shaky; +import android.content.ContentResolver; import android.content.Context; -import android.content.res.Configuration; +import android.content.Intent; +import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import android.provider.OpenableColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.support.v4.content.FileProvider; import android.util.Log; import android.view.View; +import android.webkit.MimeTypeMap; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Locale; final class Utils { + private static final String TAG = Utils.class.getSimpleName(); - private static final String FILE_NAME_TEMPLATE = "%s_%s.jpg"; + private static final String FILE_NAME_TEMPLATE = "%s_%s.png"; private static final String BITMAP_PREFIX = "bitmap"; private static final String FILE_PROVIDER_SUFFIX = ".fileprovider"; @@ -130,4 +140,97 @@ static Uri getProviderUri(@NonNull Context context, @NonNull Uri uri) { File file = new File(uri.getPath()); return getProviderUri(context, file); } + + /** + * @param uri The uri to evaluate. + * @return Whether or not the given uri points to resource on the device. This check is basic, so it does not + * guarantee the resource actually exists. + */ + static boolean isLocalUri(@Nullable Uri uri) { + if (uri != null) { + String scheme = uri.getScheme(); + return ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(scheme) + || ContentResolver.SCHEME_ANDROID_RESOURCE.equalsIgnoreCase(scheme) + || ContentResolver.SCHEME_FILE.equalsIgnoreCase(scheme); + } + return false; + } + + /** + * Tries to determine the current mime type of the pending attachment by reading the file at it's uri. There's + * fallback logic included. + * + * @param context A context used to read the file at the attachment's uri. + * @param uri Media uri + */ + @Nullable + static String getMimeType(@NonNull Context context, @NonNull Uri uri) { + if (!isLocalUri(uri)) { + return null; + } + String newMediaType = null; + + // Try to decode the bitmap to get its mime type. In some cases on Android M, the decoder may not return a + // mime type. Fallback logic kicks in afterwards. + InputStream inputStream = null; + try { + inputStream = context.getContentResolver().openInputStream(uri); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(inputStream, null, options); + + if (options.outMimeType != null) { + newMediaType = options.outMimeType; + } + } catch (FileNotFoundException e) { + Log.e(TAG, "Error getting mediaType for : " + uri.toString(), e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } + } + + // Fallback to the content resolver if the bitmap decoding hasn't worked. + if (newMediaType == null) { + newMediaType = context.getContentResolver().getType(uri); + } + + // Fallback to the file extension if everything else fails. + if (newMediaType == null) { + String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + if (extension != null) { + newMediaType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + } + + return newMediaType; + } + + @NonNull + static String getFilename(@NonNull Context context, @NonNull Uri uri) { + String scheme = uri.getScheme(); + String filename = null; + if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + filename = cursor.getString(nameIndex); + } + } finally { + cursor.close(); + } + } + } + // use the last path segment as the filename + if (filename == null) { + filename = uri.getLastPathSegment(); + } + return filename; + } } diff --git a/shaky/src/main/res/layout-v17/shaky_form.xml b/shaky/src/main/res/layout-v17/shaky_form.xml index 6a05e8e..4f108b3 100644 --- a/shaky/src/main/res/layout-v17/shaky_form.xml +++ b/shaky/src/main/res/layout-v17/shaky_form.xml @@ -50,11 +50,26 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/shaky_message_hint" + android:maxLines="5" android:inputType="textCapSentences|textMultiLine" android:scrollbars="vertical" android:textColor="@color/dark_gray"/> +