From 9fb0083fe1bb5360c2bd56134019b8a89b18065e Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Fri, 25 Aug 2017 08:51:15 +0200 Subject: [PATCH 01/49] this commit adds reply back & fixes #887 --- .../data/dao/CommentRequestModel.java | 6 +- .../viewholder/PullStatusViewHolder.java | 6 +- .../editor/comment/CommentEditorFragment.kt | 36 +++++++-- .../popup/EditorLinkImageDialogFragment.java | 2 +- .../timeline/IssueTimelineFragment.java | 17 +++- .../details/PullRequestPagerActivity.java | 2 + .../timeline/PullRequestTimelineFragment.java | 79 ++++++++++++------- .../timeline/PullRequestTimelineMvp.java | 5 +- .../PullRequestTimelinePresenter.java | 54 +++++-------- .../reviews/AddReviewDialogFragment.kt | 22 ++++-- .../reviews/changes/ReviewChangesActivity.kt | 39 ++++++--- .../reviews/changes/ReviewChangesMvp.kt | 3 +- .../layout/add_review_dialog_layout.xml | 15 +--- .../layout/review_comment_dialog_layout.xml | 16 +--- 14 files changed, 176 insertions(+), 126 deletions(-) diff --git a/app/src/main/java/com/fastaccess/data/dao/CommentRequestModel.java b/app/src/main/java/com/fastaccess/data/dao/CommentRequestModel.java index caf42eed8..d066b22f2 100644 --- a/app/src/main/java/com/fastaccess/data/dao/CommentRequestModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/CommentRequestModel.java @@ -24,13 +24,15 @@ public CommentRequestModel() {} @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + CommentRequestModel that = (CommentRequestModel) o; - return position == that.position && (path != null ? path.equals(that.path) : that.path == null); + return (path != null ? path.equals(that.path) : that.path == null) && + (position != null ? position.equals(that.position) : that.position == null); } @Override public int hashCode() { int result = path != null ? path.hashCode() : 0; - result = 31 * result + position; + result = 31 * result + (position != null ? position.hashCode() : 0); return result; } diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java index 0ba3815f8..1be01dbeb 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java @@ -72,16 +72,14 @@ public static PullStatusViewHolder newInstance(@NonNull ViewGroup parent) { if (pullRequestStatusModel.getStatuses() != null && !pullRequestStatusModel.getStatuses().isEmpty()) { SpannableBuilder builder = SpannableBuilder.builder(); Stream.of(pullRequestStatusModel.getStatuses()) - .filter(statusesModel -> statusesModel.getState() != null) + .filter(statusesModel -> statusesModel != null && statusesModel.getState() != null && statusesModel.getTargetUrl() != null) .forEach(statusesModel -> { - builder.append(ContextCompat.getDrawable(statuses.getContext(), statusesModel.getState().getDrawableRes())); if (!InputHelper.isEmpty(statusesModel.getTargetUrl())) { + builder.append(ContextCompat.getDrawable(statuses.getContext(), statusesModel.getState().getDrawableRes())); builder.append(" ") .append(statusesModel.getContext() != null ? statusesModel.getContext() + " " : "") .url(statusesModel.getDescription(), v -> SchemeParser.launchUri(v.getContext(), statusesModel.getTargetUrl())) .append("\n"); - } else { - builder.append("\n"); } }); if (!InputHelper.isEmpty(builder)) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt index 5d794f692..0effcdc21 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt @@ -18,11 +18,13 @@ import com.fastaccess.helper.Bundler import com.fastaccess.helper.InputHelper import com.fastaccess.helper.ViewHelper import com.fastaccess.provider.emoji.Emoji +import com.fastaccess.provider.markdown.MarkDownProvider import com.fastaccess.ui.base.BaseFragment import com.fastaccess.ui.base.mvp.BaseMvp import com.fastaccess.ui.base.mvp.presenter.BasePresenter import com.fastaccess.ui.modules.editor.EditorActivity import com.fastaccess.ui.modules.editor.emoji.EmojiMvp +import com.fastaccess.ui.modules.editor.popup.EditorLinkImageMvp import com.fastaccess.ui.widgets.markdown.MarkDownLayout import com.fastaccess.ui.widgets.markdown.MarkdownEditText @@ -30,12 +32,13 @@ import com.fastaccess.ui.widgets.markdown.MarkdownEditText * Created by kosh on 21/08/2017. */ class CommentEditorFragment : BaseFragment>(), MarkDownLayout.MarkdownListener, - EmojiMvp.EmojiCallback { + EmojiMvp.EmojiCallback, EditorLinkImageMvp.EditorLinkCallback { @BindView(R.id.commentBox) lateinit var commentBox: View @BindView(R.id.markdDownLayout) lateinit var markdDownLayout: MarkDownLayout @BindView(R.id.commentText) lateinit var commentText: MarkdownEditText @BindView(R.id.markdownBtnHolder) lateinit var markdownBtnHolder: View + @BindView(R.id.sendComment) lateinit var sendComment: View private var commentListener: CommentListener? = null @OnClick(R.id.sendComment) internal fun onComment() { @@ -81,6 +84,12 @@ class CommentEditorFragment : BaseFragment, diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java index b3c91c783..c5fa8d9ca 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java @@ -13,6 +13,7 @@ import com.fastaccess.data.dao.CommentRequestModel; import com.fastaccess.data.dao.EditReviewCommentModel; import com.fastaccess.data.dao.GroupedReviewModel; +import com.fastaccess.data.dao.PullRequestStatusModel; import com.fastaccess.data.dao.ReviewCommentModel; import com.fastaccess.data.dao.TimelineModel; import com.fastaccess.data.dao.model.Comment; @@ -23,7 +24,6 @@ import com.fastaccess.data.dao.types.ReactionTypes; import com.fastaccess.helper.ActivityHelper; import com.fastaccess.helper.BundleConstant; -import com.fastaccess.helper.Bundler; import com.fastaccess.helper.InputHelper; import com.fastaccess.provider.rest.RestProvider; import com.fastaccess.provider.scheme.SchemeParser; @@ -139,15 +139,14 @@ public class PullRequestTimelinePresenter extends BasePresenter { - }); - } else { - EditReviewCommentModel commentModel = bundle.getParcelable(BundleConstant.REVIEW_EXTRA); - if (commentModel != null) { - CommentRequestModel commentRequestModel = new CommentRequestModel(); - commentRequestModel.setBody(text); - commentRequestModel.setInReplyTo(commentModel.getInReplyTo()); - makeRestCall(RestProvider.getReviewService(isEnterprise()) - .submitComment(pullRequest.getLogin(), pullRequest.getRepoId(), pullRequest.getNumber(), commentRequestModel), - reviewCommentModel -> { - sendToView(view -> view.onAddReviewComment(reviewCommentModel, commentModel)); - }); - } + pullRequest.getNumber(), commentRequestModel), comment -> sendToView(view -> view.addComment(TimelineModel.constructComment(comment)))); } } } @@ -296,12 +282,7 @@ public class PullRequestTimelinePresenter extends BasePresenter> observable = Observable.zip(RestProvider.getIssueService(isEnterprise()) - .getTimeline(login, repoId, number, page), RestProvider.getReviewService(isEnterprise()) - .getPrReviewComments(login, repoId, number), - RestProvider.getPullRequestService(isEnterprise()).getPullStatus(login, repoId, parameter.getHead().getRef()), + Observable> observable = Observable.zip( + RestProvider.getIssueService(isEnterprise()).getTimeline(login, repoId, number, page), + RestProvider.getReviewService(isEnterprise()).getPrReviewComments(login, repoId, number), + RestProvider.getPullRequestService(isEnterprise()).getPullStatus(login, repoId, parameter.getHead().getRef()) + .onErrorReturn(throwable -> RestProvider.getPullRequestService(isEnterprise()).getPullStatus(login, repoId, + parameter.getBase().getRef()).blockingFirst(new PullRequestStatusModel())), (response, comments, status) -> { if (response != null) { lastPage = response.getLast(); List models = TimelineConverter.INSTANCE.convert(response.getItems(), comments); if (page == 1 && status != null) { - models.add(0, new TimelineModel(status)); + status.setMergable(parameter.isMergable()); + if (status.getState() != null && status.getStatuses() != null) models.add(0, new TimelineModel(status)); } return models; } else { diff --git a/app/src/main/java/com/fastaccess/ui/modules/reviews/AddReviewDialogFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/reviews/AddReviewDialogFragment.kt index 26b716a29..06e52ea8a 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/reviews/AddReviewDialogFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/reviews/AddReviewDialogFragment.kt @@ -3,7 +3,6 @@ package com.fastaccess.ui.modules.reviews import android.content.Context import android.graphics.Color import android.os.Bundle -import android.support.design.widget.TextInputLayout import android.support.v4.content.ContextCompat import android.support.v7.widget.Toolbar import android.view.View @@ -18,6 +17,7 @@ import com.fastaccess.helper.ViewHelper import com.fastaccess.ui.base.BaseDialogFragment import com.fastaccess.ui.base.mvp.BaseMvp import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import com.fastaccess.ui.modules.editor.comment.CommentEditorFragment import com.fastaccess.ui.modules.reviews.callback.ReviewCommentListener import com.fastaccess.ui.widgets.SpannableBuilder @@ -29,8 +29,10 @@ class AddReviewDialogFragment : BaseDialogFragment(BundleConstant.ITEM) lineNo.text = SpannableBuilder.builder() .append(if (item.leftLineNo >= 0) String.format("%s.", item.leftLineNo) else "") @@ -75,11 +84,12 @@ class AddReviewDialogFragment : BaseDialogFragment(), ReviewChangesMvp.View { - @BindView(R.id.toolbar) lateinit var toolbar: Toolbar @BindView(R.id.reviewMethod) lateinit var spinner: Spinner - @BindView(R.id.editText) lateinit var editText: TextInputLayout - @State var reviewRequest: ReviewRequestModel? = null - @State var repoId: String? = null - @State var owner: String? = null - @State var number: Long? = null - @State var isProgressShowing: Boolean = false - @State var isClosed: Boolean = false + @State + var reviewRequest: ReviewRequestModel? = null + @State + var repoId: String? = null + @State + var owner: String? = null + @State + var number: Long? = null + @State + var isProgressShowing: Boolean = false + @State + var isClosed: Boolean = false + + private var commentEditorFragment: CommentEditorFragment? = null override fun layout(): Int = R.layout.add_review_dialog_layout @@ -49,6 +55,8 @@ class ReviewChangesActivity : BaseActivity { - if (spinner.selectedItemPosition != 0 && editText.editText?.text.isNullOrEmpty()) { - editText.error = getString(R.string.required_field) + if (spinner.selectedItemPosition != 0 && commentEditorFragment?.getEditText()?.text.isNullOrEmpty()) { + commentEditorFragment?.getEditText()?.error = getString(R.string.required_field) } else { - editText.error = null - presenter.onSubmit(reviewRequest!!, repoId!!, owner!!, number!!, InputHelper.toString(editText), spinner.selectedItem as String) + commentEditorFragment?.getEditText()?.error = null + presenter.onSubmit(reviewRequest!!, repoId!!, owner!!, number!!, InputHelper.toString(commentEditorFragment?.getEditText()) + , spinner.selectedItem as String) } return true } @@ -133,6 +142,10 @@ class ReviewChangesActivity : BaseActivity - + tools:layout="@layout/comment_box_layout"/> - - \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/review_comment_dialog_layout.xml b/app/src/main/res/layouts/main_layouts/layout/review_comment_dialog_layout.xml index 16174b7ac..1805f9316 100644 --- a/app/src/main/res/layouts/main_layouts/layout/review_comment_dialog_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/review_comment_dialog_layout.xml @@ -41,20 +41,10 @@ - - - - + android:layout_height="wrap_content"/> \ No newline at end of file From 1152fbbe2c50144701d8d8b29ec5513f789c2b40 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Fri, 25 Aug 2017 09:41:20 +0200 Subject: [PATCH 02/49] this commit ships firebase job dispatcher within the app to fix an issue with NPE & casting crash --- app/build.gradle | 2 +- .../popup/EditorLinkImageDialogFragment.java | 8 +- .../PullRequestTimelinePresenter.java | 3 +- build.gradle | 2 +- jobdispatcher/build.gradle | 79 ++++ jobdispatcher/coverage.gradle | 40 ++ .../src/androidTest/AndroidManifest.xml | 35 ++ .../firebase/jobdispatcher/EndToEndTest.java | 69 ++++ jobdispatcher/src/main/AndroidManifest.xml | 32 ++ .../jobdispatcher/BundleProtocol.java | 49 +++ .../firebase/jobdispatcher/Constraint.java | 108 +++++ .../jobdispatcher/DefaultJobValidator.java | 288 +++++++++++++ .../com/firebase/jobdispatcher/Driver.java | 62 +++ .../jobdispatcher/ExecutionDelegator.java | 160 ++++++++ .../jobdispatcher/FirebaseJobDispatcher.java | 211 ++++++++++ .../GooglePlayCallbackExtractor.java | 247 ++++++++++++ .../jobdispatcher/GooglePlayDriver.java | 157 ++++++++ .../jobdispatcher/GooglePlayJobCallback.java | 56 +++ .../jobdispatcher/GooglePlayJobWriter.java | 198 +++++++++ .../GooglePlayMessageHandler.java | 114 ++++++ .../GooglePlayMessengerCallback.java | 61 +++ .../jobdispatcher/GooglePlayReceiver.java | 274 +++++++++++++ .../java/com/firebase/jobdispatcher/Job.java | 378 ++++++++++++++++++ .../firebase/jobdispatcher/JobCallback.java | 28 ++ .../com/firebase/jobdispatcher/JobCoder.java | 253 ++++++++++++ .../firebase/jobdispatcher/JobInvocation.java | 236 +++++++++++ .../firebase/jobdispatcher/JobParameters.java | 86 ++++ .../firebase/jobdispatcher/JobService.java | 257 ++++++++++++ .../jobdispatcher/JobServiceConnection.java | 82 ++++ .../firebase/jobdispatcher/JobTrigger.java | 70 ++++ .../firebase/jobdispatcher/JobValidator.java | 49 +++ .../com/firebase/jobdispatcher/Lifetime.java | 40 ++ .../firebase/jobdispatcher/ObservedUri.java | 83 ++++ .../firebase/jobdispatcher/RetryStrategy.java | 106 +++++ .../jobdispatcher/SimpleJobService.java | 90 +++++ .../com/firebase/jobdispatcher/Trigger.java | 73 ++++ .../firebase/jobdispatcher/TriggerReason.java | 33 ++ .../jobdispatcher/ValidationEnforcer.java | 132 ++++++ .../android/net/http/AndroidHttpClient.java | 10 + .../jobdispatcher/ConstraintTest.java | 66 +++ .../jobdispatcher/ContentUriTriggerTest.java | 52 +++ .../DefaultJobValidatorTest.java | 119 ++++++ .../jobdispatcher/ExecutionDelegatorTest.java | 224 +++++++++++ .../ExecutionWindowTriggerTest.java | 76 ++++ .../jobdispatcher/ExtendedShadowParcel.java | 40 ++ .../FirebaseJobDispatcherTest.java | 205 ++++++++++ .../GooglePlayCallbackExtractorTest.java | 153 +++++++ .../jobdispatcher/GooglePlayDriverTest.java | 203 ++++++++++ .../GooglePlayJobWriterTest.java | 268 +++++++++++++ .../GooglePlayMessageHandlerTest.java | 153 +++++++ .../GooglePlayMessengerCallbackTest.java | 63 +++ .../jobdispatcher/GooglePlayReceiverTest.java | 327 +++++++++++++++ .../jobdispatcher/ImmediateTriggerTest.java | 34 ++ .../jobdispatcher/JobBuilderTest.java | 65 +++ .../firebase/jobdispatcher/JobCoderTest.java | 158 ++++++++ .../jobdispatcher/JobInvocationTest.java | 83 ++++ .../JobServiceConnectionTest.java | 116 ++++++ .../jobdispatcher/JobServiceTest.java | 346 ++++++++++++++++ .../jobdispatcher/ValidationEnforcerTest.java | 187 +++++++++ .../jobdispatcher/NoopJobValidator.java | 44 ++ .../jobdispatcher/TestJobService.java | 71 ++++ .../com/firebase/jobdispatcher/TestUtil.java | 375 +++++++++++++++++ .../android/gms/gcm/PendingCallback.java | 61 +++ settings.gradle | 2 +- 64 files changed, 7745 insertions(+), 7 deletions(-) create mode 100644 jobdispatcher/build.gradle create mode 100644 jobdispatcher/coverage.gradle create mode 100644 jobdispatcher/src/androidTest/AndroidManifest.xml create mode 100644 jobdispatcher/src/androidTest/java/com/firebase/jobdispatcher/EndToEndTest.java create mode 100644 jobdispatcher/src/main/AndroidManifest.xml create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/BundleProtocol.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/Constraint.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/DefaultJobValidator.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/Driver.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/ExecutionDelegator.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/FirebaseJobDispatcher.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractor.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobCallback.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobWriter.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessageHandler.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessengerCallback.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayReceiver.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/Job.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCallback.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCoder.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobInvocation.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobParameters.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobServiceConnection.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobTrigger.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobValidator.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/Lifetime.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/ObservedUri.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/RetryStrategy.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/SimpleJobService.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/Trigger.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/TriggerReason.java create mode 100644 jobdispatcher/src/main/java/com/firebase/jobdispatcher/ValidationEnforcer.java create mode 100644 jobdispatcher/src/test/java/android/net/http/AndroidHttpClient.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/ConstraintTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/ContentUriTriggerTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/DefaultJobValidatorTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionDelegatorTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionWindowTriggerTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExtendedShadowParcel.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/FirebaseJobDispatcherTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractorTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayDriverTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayJobWriterTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessageHandlerTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessengerCallbackTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayReceiverTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/ImmediateTriggerTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobBuilderTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobCoderTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobInvocationTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceConnectionTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceTest.java create mode 100644 jobdispatcher/src/test/java/com/firebase/jobdispatcher/ValidationEnforcerTest.java create mode 100644 jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/NoopJobValidator.java create mode 100644 jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestJobService.java create mode 100644 jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestUtil.java create mode 100644 jobdispatcher/src/testLib/java/com/google/android/gms/gcm/PendingCallback.java diff --git a/app/build.gradle b/app/build.gradle index a1be537c8..6525676a4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -155,7 +155,6 @@ dependencies { implementation "com.google.android.gms:play-services-base:${gms}" implementation('com.github.b3er.rxfirebase:firebase-database-kotlin:11.2.0') { transitive = false } implementation('com.github.b3er.rxfirebase:firebase-database:11.2.0') { transitive = false } - implementation 'com.firebase:firebase-jobdispatcher:0.7.0' if (isProduction) implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { transitive = true } implementation "com.github.miguelbcr:RxBillingService:0.0.3" implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" @@ -165,6 +164,7 @@ dependencies { implementation 'com.apollographql.apollo:apollo-rx2-support:0.4.0' implementation 'com.jaredrummler:android-device-names:1.1.4' implementation 'net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.1.0' + implementation project(path: ':jobdispatcher') compileOnly "org.projectlombok:lombok:${lombokVersion}" kapt "org.projectlombok:lombok:${lombokVersion}" kapt "com.evernote:android-state-processor:${state_version}" diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImageDialogFragment.java b/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImageDialogFragment.java index 8e2839ee3..0d0549ad7 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImageDialogFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImageDialogFragment.java @@ -49,7 +49,9 @@ public static EditorLinkImageDialogFragment newInstance(boolean isLink) { @Override public void onAttach(Context context) { super.onAttach(context); - if (context instanceof EditorLinkImageMvp.EditorLinkCallback) { + if (getParentFragment() instanceof EditorLinkImageMvp.EditorLinkCallback) { + callback = (EditorLinkImageMvp.EditorLinkCallback) getParentFragment(); + } else if (context instanceof EditorLinkImageMvp.EditorLinkCallback) { callback = (EditorLinkImageMvp.EditorLinkCallback) context; } } @@ -62,7 +64,7 @@ public static EditorLinkImageDialogFragment newInstance(boolean isLink) { @Override public void onUploaded(@Nullable String title, @Nullable String link) { hideProgress(); if (callback != null) { - callback.onAppendLink(title, link, isLink()); + callback.onAppendLink(title, link != null ? link.replace("http:", "https:") : null, isLink()); } dismiss(); } @@ -108,7 +110,7 @@ public static EditorLinkImageDialogFragment newInstance(boolean isLink) { @OnClick(R.id.insert) public void onInsertClicked() { if (callback != null) { - callback.onAppendLink(InputHelper.toString(title), InputHelper.toString(link).replace("http://", "https://"), isLink()); + callback.onAppendLink(InputHelper.toString(title), InputHelper.toString(link), isLink()); } dismiss(); } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java index c5fa8d9ca..bb79ebaf7 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java @@ -248,7 +248,8 @@ public class PullRequestTimelinePresenter extends BasePresenter sendToView(view -> view.addComment(TimelineModel.constructComment(comment)))); + pullRequest.getNumber(), commentRequestModel), comment -> sendToView(view -> view.addComment(TimelineModel.constructComment + (comment)))); } } } diff --git a/build.gradle b/build.gradle index 3147699c6..8dee36c27 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { assertjVersion = '2.5.0' espresseVersion = '2.2.2' requery = '1.3.2' - kotlin_version = '1.1.4' + kotlin_version = '1.1.4-2' commonmark = '0.9.0' } repositories { diff --git a/jobdispatcher/build.gradle b/jobdispatcher/build.gradle new file mode 100644 index 000000000..d326ddf27 --- /dev/null +++ b/jobdispatcher/build.gradle @@ -0,0 +1,79 @@ +apply plugin: "com.android.library" + +android { + compileSdkVersion 26 + buildToolsVersion "26.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 26 + versionCode 1 + versionName "0.8.0" + testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' + } + + defaultPublishConfig "release" + publishNonDefault true + + buildTypes { + debug { + testCoverageEnabled true + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + } + + sourceSets { + // A set of testing helpers that are shared across test types + testLib { java.srcDir("src/main") } + test { java.srcDir("src/testLib") } // Robolectric tests + androidTest { java.srcDir("src/testLib") } // Android (e2e) tests + } +} + +dependencies { + // The main library only depends on the Android support lib + compile "com.android.support:support-v4:26.0.1" + + def junit = 'junit:junit:4.12' + def robolectric = 'org.robolectric:robolectric:3.3.2' + + // The common test library uses JUnit + testLibCompile junit + + // The unit tests are written using JUnit, Robolectric, and Mockito + testCompile junit + testCompile robolectric + testCompile 'org.mockito:mockito-core:2.2.5' + + // The Android (e2e) tests are written using JUnit and the test support lib + androidTestCompile junit + androidTestCompile 'com.android.support.test:runner:0.5' +} + +task javadocs(type: Javadoc) { + description "Generate Javadocs" + source = android.sourceSets.main.java.sourceFiles + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + classpath += configurations.compile + failOnError false +} + +task javadocsJar(type: Jar, dependsOn: javadocs) { + description "Package Javadocs into a jar" + classifier = "javadoc" + from javadocs.destinationDir +} + +task sourcesJar(type: Jar) { + description "Package sources into a jar" + classifier = "sources" + from android.sourceSets.main.java.sourceFiles +} + +task aar(dependsOn: "assembleRelease") { + group "artifact" + description "Builds the library AARs" +} diff --git a/jobdispatcher/coverage.gradle b/jobdispatcher/coverage.gradle new file mode 100644 index 000000000..204f2dc22 --- /dev/null +++ b/jobdispatcher/coverage.gradle @@ -0,0 +1,40 @@ +apply plugin: "jacoco" + +jacoco { + // see https://github.com/jacoco/jacoco/pull/288 and the top build.gradle + toolVersion "0.7.6.201602180812" +} + +android { + testOptions { + unitTests.all { + systemProperty "robolectric.logging.enabled", true + systemProperty "robolectric.logging", "stdout" + + jacoco { + includeNoLocationClasses = true + } + } + } +} + +// ignore these when generating coverage +def ignoredPrefixes = ['R$', 'R.class', 'BuildConfig.class'] + +task coverage(type: JacocoReport, dependsOn: ["testDebugUnitTest"]) { + group = "Reports" + description = "Generate a coverage report" + + classDirectories = fileTree( + dir: "${project.buildDir}/intermediates/classes/debug/com/firebase/", + exclude: { d -> ignoredPrefixes.any { p -> d.file.name.startsWith(p) } } + ) + sourceDirectories = files(["src/main/java/com/firebase/"]) + executionData = files("${project.buildDir}/jacoco/testDebugUnitTest.exec") + + reports { + xml.enabled = true + html.enabled = true + html.destination "${buildDir}/coverage_html" + } +} diff --git a/jobdispatcher/src/androidTest/AndroidManifest.xml b/jobdispatcher/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..c87f27fcf --- /dev/null +++ b/jobdispatcher/src/androidTest/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/jobdispatcher/src/androidTest/java/com/firebase/jobdispatcher/EndToEndTest.java b/jobdispatcher/src/androidTest/java/com/firebase/jobdispatcher/EndToEndTest.java new file mode 100644 index 000000000..48b1048bc --- /dev/null +++ b/jobdispatcher/src/androidTest/java/com/firebase/jobdispatcher/EndToEndTest.java @@ -0,0 +1,69 @@ +// Copyright 2017 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Basic end to end test for the JobDispatcher. Requires Google Play services be installed and + * available. + */ +@RunWith(AndroidJUnit4.class) +public final class EndToEndTest { + private Context appContext; + private FirebaseJobDispatcher dispatcher; + + @Before public void setUp() { + appContext = InstrumentationRegistry.getTargetContext(); + dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(appContext)); + TestJobService.reset(); + } + + @Test public void basicImmediateJob() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + TestJobService.setProxy(new TestJobService.JobServiceProxy() { + @Override + public boolean onStartJob(JobParameters params) { + latch.countDown(); + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + }); + + dispatcher.mustSchedule( + dispatcher.newJobBuilder() + .setService(TestJobService.class) + .setTrigger(Trigger.NOW) + .setTag("basic-immediate-job") + .build()); + + assertTrue("Latch wasn't counted down as expected", latch.await(120, TimeUnit.SECONDS)); + } +} diff --git a/jobdispatcher/src/main/AndroidManifest.xml b/jobdispatcher/src/main/AndroidManifest.xml new file mode 100644 index 000000000..71205bd86 --- /dev/null +++ b/jobdispatcher/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/BundleProtocol.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/BundleProtocol.java new file mode 100644 index 000000000..774a83401 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/BundleProtocol.java @@ -0,0 +1,49 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +final class BundleProtocol { + static final String PACKED_PARAM_BUNDLE_PREFIX = "com.firebase.jobdispatcher."; + + // PACKED_PARAM values are only read on the client side, so as long as the + // extraction process gets the same changes then it's fine. + static final String PACKED_PARAM_CONSTRAINTS = "constraints"; + static final String PACKED_PARAM_LIFETIME = "persistent"; + static final String PACKED_PARAM_RECURRING = "recurring"; + static final String PACKED_PARAM_SERVICE = "service"; + static final String PACKED_PARAM_TAG = "tag"; + static final String PACKED_PARAM_EXTRAS = "extras"; + static final String PACKED_PARAM_TRIGGER_TYPE = "trigger_type"; + static final String PACKED_PARAM_TRIGGER_WINDOW_END = "window_end"; + static final String PACKED_PARAM_TRIGGER_WINDOW_START = "window_start"; + static final int TRIGGER_TYPE_EXECUTION_WINDOW = 1; + static final int TRIGGER_TYPE_IMMEDIATE = 2; + static final int TRIGGER_TYPE_CONTENT_URI = 3; + static final String PACKED_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS = + "initial_backoff_seconds"; + static final String PACKED_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS = + "maximum_backoff_seconds"; + static final String PACKED_PARAM_RETRY_STRATEGY_POLICY = "retry_policy"; + static final String PACKED_PARAM_REPLACE_CURRENT = "replace_current"; + static final String PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY = "content_uri_flags_array"; + static final String PACKED_PARAM_CONTENT_URI_ARRAY = "content_uri_array"; + static final String PACKED_PARAM_TRIGGERED_URIS = "triggered_uris"; + static final String PACKED_PARAM_OBSERVED_URI = "observed_uris"; + + BundleProtocol() { + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Constraint.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Constraint.java new file mode 100644 index 000000000..ff413fb20 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Constraint.java @@ -0,0 +1,108 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.IntDef; +import android.support.annotation.VisibleForTesting; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A Constraint is a runtime requirement for a job. A job only becomes eligible to run once its + * trigger has been activated and all constraints are satisfied. + */ +public final class Constraint { + /** + * Only run the job when an unmetered network is available. + */ + public static final int ON_UNMETERED_NETWORK = 1; + + /** + * Only run the job when a network connection is available. If both this and + * {@link #ON_UNMETERED_NETWORK} is provided, {@link #ON_UNMETERED_NETWORK} will take + * precedence. + */ + public static final int ON_ANY_NETWORK = 1 << 1; + + /** + * Only run the job when the device is currently charging. + */ + public static final int DEVICE_CHARGING = 1 << 2; + + /** + * Only run the job when the device is idle. This is ignored for devices that don't expose the + * concept of an idle state. + */ + public static final int DEVICE_IDLE = 1 << 3; + + @VisibleForTesting + static final int[] ALL_CONSTRAINTS = { + ON_ANY_NETWORK, ON_UNMETERED_NETWORK, DEVICE_CHARGING, DEVICE_IDLE}; + + /** Constraint shouldn't ever be instantiated. */ + private Constraint() {} + + /** + * A tooling type-hint for any of the valid constraint values. + */ + @IntDef(flag = true, value = { + ON_ANY_NETWORK, + ON_UNMETERED_NETWORK, + DEVICE_CHARGING, + DEVICE_IDLE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface JobConstraint {} + + /** + * Compact a provided array of constraints into a single int. + * + * @see #uncompact(int) + */ + static int compact(@JobConstraint int[] constraints) { + int result = 0; + if (constraints == null) { + return result; + } + for (int c : constraints) { + result |= c; + } + return result; + } + + /** + * Unpack a single int into an array of constraints. + * + * @see #compact(int[]) + */ + static int[] uncompact(int compactConstraints) { + int length = 0; + for (int c : ALL_CONSTRAINTS) { + length += (compactConstraints & c) == c ? 1 : 0; + } + int[] list = new int[length]; + + int i = 0; + for (int c : ALL_CONSTRAINTS) { + if ((compactConstraints & c) == c) { + list[i++] = c; + } + } + + return list; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/DefaultJobValidator.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/DefaultJobValidator.java new file mode 100644 index 000000000..4804804e5 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/DefaultJobValidator.java @@ -0,0 +1,288 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.RetryStrategy.RETRY_POLICY_EXPONENTIAL; +import static com.firebase.jobdispatcher.RetryStrategy.RETRY_POLICY_LINEAR; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Parcel; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Validates Jobs according to some safe standards. + *

+ * Custom JobValidators should typically extend from this. + */ +public class DefaultJobValidator implements JobValidator { + + /** + * The maximum length of a tag, in characters (i.e. String.length()). Strings longer than this + * will cause validation to fail. + */ + public static final int MAX_TAG_LENGTH = 100; + + /** + * The maximum size, in bytes, that the provided extras bundle can be. Corresponds to + * {@link Parcel#dataSize()}. + */ + public final static int MAX_EXTRAS_SIZE_BYTES = 10 * 1024; + + /** Private ref to the Context. Necessary to check that the manifest is configured correctly. */ + private final Context context; + + public DefaultJobValidator(Context context) { + this.context = context; + } + + /** @see {@link #MAX_EXTRAS_SIZE_BYTES}. */ + private static int measureBundleSize(Bundle extras) { + Parcel p = Parcel.obtain(); + extras.writeToParcel(p, 0); + int sizeInBytes = p.dataSize(); + p.recycle(); + + return sizeInBytes; + } + + /** Combines two {@literal Lists} together. */ + @Nullable + private static List mergeErrorLists(@Nullable List errors, + @Nullable List newErrors) { + if (errors == null) { + return newErrors; + } + if (newErrors == null) { + return errors; + } + + errors.addAll(newErrors); + return errors; + } + + @Nullable + private static List addError(@Nullable List errors, String newError) { + if (newError == null) { + return errors; + } + if (errors == null) { + return getMutableSingletonList(newError); + } + + Collections.addAll(errors, newError); + + return errors; + } + + @Nullable + private static List addErrorsIf(boolean condition, List errors, String newErr) { + if (condition) { + return addError(errors, newErr); + } + + return errors; + } + + /** + * Attempts to validate the provided {@code JobParameters}. If the JobParameters is valid, null will be + * returned. If the JobParameters has errors, a list of those errors will be returned. + */ + @Nullable + @Override + @CallSuper + public List validate(JobParameters job) { + List errors = null; + + errors = mergeErrorLists(errors, validate(job.getTrigger())); + errors = mergeErrorLists(errors, validate(job.getRetryStrategy())); + + if (job.isRecurring() && job.getTrigger() == Trigger.NOW) { + errors = addError(errors, "ImmediateTriggers can't be used with recurring jobs"); + } + + errors = mergeErrorLists(errors, validateForTransport(job.getExtras())); + if (job.getLifetime() > Lifetime.UNTIL_NEXT_BOOT) { + //noinspection ConstantConditions + errors = mergeErrorLists(errors, validateForPersistence(job.getExtras())); + } + + errors = mergeErrorLists(errors, validateTag(job.getTag())); + errors = mergeErrorLists(errors, validateService(job.getService())); + + return errors; + } + + /** + * Attempts to validate the provided Trigger. If valid, null is returned. Otherwise a list of + * errors will be returned. + *

+ * Note that a Trigger that passes validation here is not necessarily valid in all permutations + * of a JobParameters. For example, an Immediate is never valid for a recurring job. + * @param trigger + */ + @Nullable + @Override + @CallSuper + public List validate(JobTrigger trigger) { + if (trigger != Trigger.NOW + && !(trigger instanceof JobTrigger.ExecutionWindowTrigger) + && !(trigger instanceof JobTrigger.ContentUriTrigger)) { + return getMutableSingletonList("Unknown trigger provided"); + } + + return null; + } + + /** + * Attempts to validate the provided RetryStrategy. If valid, null is returned. Otherwise a list + * of errors will be returned. + */ + @Nullable + @Override + @CallSuper + public List validate(RetryStrategy retryStrategy) { + List errors = null; + + int policy = retryStrategy.getPolicy(); + int initial = retryStrategy.getInitialBackoff(); + int maximum = retryStrategy.getMaximumBackoff(); + + errors = addErrorsIf(policy != RETRY_POLICY_EXPONENTIAL && policy != RETRY_POLICY_LINEAR, + errors, "Unknown retry policy provided"); + errors = addErrorsIf(maximum < initial, + errors, "Maximum backoff must be greater than or equal to initial backoff"); + errors = addErrorsIf(300 > maximum, + errors, "Maximum backoff must be greater than 300s (5 minutes)"); + errors = addErrorsIf(initial < 30, + errors, "Initial backoff must be at least 30s"); + + return errors; + } + + @Nullable + private List validateForPersistence(Bundle extras) { + List errors = null; + + if (extras != null) { + // check the types to make sure they're persistable + for (String k : extras.keySet()) { + errors = addError(errors, validateExtrasType(extras, k)); + } + } + + return errors; + } + + @Nullable + private List validateForTransport(Bundle extras) { + if (extras == null) { + return null; + } + + int bundleSizeInBytes = measureBundleSize(extras); + if (bundleSizeInBytes > MAX_EXTRAS_SIZE_BYTES) { + return getMutableSingletonList(String.format(Locale.US, + "Extras too large: %d bytes is > the max (%d bytes)", + bundleSizeInBytes, MAX_EXTRAS_SIZE_BYTES)); + } + + return null; + } + + @Nullable + private String validateExtrasType(Bundle extras, String key) { + Object o = extras.get(key); + + if (o == null + || o instanceof Integer + || o instanceof Long + || o instanceof Double + || o instanceof String + || o instanceof Boolean) { + return null; + } + + return String.format(Locale.US, + "Received value of type '%s' for key '%s', but only the" + + " following extra parameter types are supported:" + + " Integer, Long, Double, String, and Boolean", + o == null ? null : o.getClass(), key); + } + + private List validateService(String service) { + if (service == null || service.isEmpty()) { + return getMutableSingletonList("Service can't be empty"); + } + + if (context == null) { + return getMutableSingletonList("Context is null, can't query PackageManager"); + } + + PackageManager pm = context.getPackageManager(); + if (pm == null) { + return getMutableSingletonList("PackageManager is null, can't validate service"); + } + + final String msg = "Couldn't find a registered service with the name " + service + + ". Is it declared in the manifest with the right intent-filter?"; + + Intent executeIntent = new Intent(JobService.ACTION_EXECUTE); + executeIntent.setClassName(context, service); + List intentServices = pm.queryIntentServices(executeIntent, 0); + if (intentServices == null || intentServices.isEmpty()) { + return getMutableSingletonList(msg); + } + + for (ResolveInfo info : intentServices) { + if (info.serviceInfo != null && info.serviceInfo.enabled) { + // found a match! + return null; + } + } + + return getMutableSingletonList(msg); + } + + private List validateTag(String tag) { + if (tag == null) { + return getMutableSingletonList("Tag can't be null"); + } + + if (tag.length() > MAX_TAG_LENGTH) { + return getMutableSingletonList("Tag must be shorter than " + MAX_TAG_LENGTH); + } + + return null; + } + + @NonNull + private static List getMutableSingletonList(String msg) { + ArrayList strings = new ArrayList<>(); + strings.add(msg); + return strings; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Driver.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Driver.java new file mode 100644 index 000000000..fe832721c --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Driver.java @@ -0,0 +1,62 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.FirebaseJobDispatcher.CancelResult; +import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleResult; + +/** + * Driver represents a component that understands how to schedule, validate, and execute jobs. + */ +public interface Driver { + + /** + * Schedules the provided Job. + * + * @return one of the SCHEDULE_RESULT_ constants + */ + @ScheduleResult + int schedule(@NonNull Job job); + + /** + * Cancels the job with the provided tag and class. + * + * @return one of the CANCEL_RESULT_ constants. + */ + @CancelResult + int cancel(@NonNull String tag); + + /** + * Cancels all jobs registered with this Driver. + * + * @return one of the CANCEL_RESULT_ constants. + */ + @CancelResult + int cancelAll(); + + /** + * Returns a JobValidator configured for this backend. + */ + @NonNull + JobValidator getValidator(); + + /** + * Indicates whether the backend is available. + */ + boolean isAvailable(); +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ExecutionDelegator.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ExecutionDelegator.java new file mode 100644 index 000000000..1dbacb3cb --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ExecutionDelegator.java @@ -0,0 +1,160 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static android.content.Context.BIND_AUTO_CREATE; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.v4.util.SimpleArrayMap; +import android.util.Log; +import com.firebase.jobdispatcher.JobService.JobResult; +import java.lang.ref.WeakReference; + +/** + * ExecutionDelegator tracks local Binder connections to client JobServices and handles + * communication with those services. + */ +/* package */ class ExecutionDelegator { + @VisibleForTesting + static final int JOB_FINISHED = 1; + + static final String TAG = "FJD.ExternalReceiver"; + + interface JobFinishedCallback { + void onJobFinished(@NonNull JobInvocation jobInvocation, @JobResult int result); + } + + /** + * A mapping of {@link JobInvocation} to (local) binder connections. + * Synchronized by itself. + */ + private final SimpleArrayMap serviceConnections = + new SimpleArrayMap<>(); + private final ResponseHandler responseHandler = + new ResponseHandler(Looper.getMainLooper(), new WeakReference<>(this)); + private final Context context; + private final JobFinishedCallback jobFinishedCallback; + + ExecutionDelegator(Context context, JobFinishedCallback jobFinishedCallback) { + this.context = context; + this.jobFinishedCallback = jobFinishedCallback; + } + + /** + * Executes the provided {@code jobInvocation} by kicking off the creation of a new Binder + * connection to the Service. + * + * @return true if the service was bound successfully. + */ + boolean executeJob(JobInvocation jobInvocation) { + if (jobInvocation == null) { + return false; + } + + JobServiceConnection conn = new JobServiceConnection(jobInvocation, + responseHandler.obtainMessage(JOB_FINISHED)); + + synchronized (serviceConnections) { + JobServiceConnection oldConnection = serviceConnections.put(jobInvocation, conn); + if (oldConnection != null) { + Log.e(TAG, "Received execution request for already running job"); + } + return context.bindService(createBindIntent(jobInvocation), conn, BIND_AUTO_CREATE); + } + } + + @NonNull + private Intent createBindIntent(JobParameters jobParameters) { + Intent execReq = new Intent(JobService.ACTION_EXECUTE); + execReq.setClassName(context, jobParameters.getService()); + return execReq; + } + + void stopJob(JobInvocation job) { + synchronized (serviceConnections) { + JobServiceConnection jobServiceConnection = serviceConnections.remove(job); + if (jobServiceConnection != null) { + jobServiceConnection.onStop(); + safeUnbindService(jobServiceConnection); + } + } + } + + private void safeUnbindService(JobServiceConnection connection) { + if (connection != null && connection.isBound()) { + try { + context.unbindService(connection); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Error unbinding service: " + e.getMessage()); + } + } + } + + private void onJobFinishedMessage(JobInvocation jobInvocation, int result) { + synchronized (serviceConnections) { + JobServiceConnection connection = serviceConnections.remove(jobInvocation); + safeUnbindService(connection); + } + + jobFinishedCallback.onJobFinished(jobInvocation, result); + } + + private static class ResponseHandler extends Handler { + + /** + * We hold a WeakReference to the ExecutionDelegator because it holds a reference to a + * Service Context and Handlers are often kept in memory longer than you'd expect because + * any pending Messages can maintain references to them. + */ + private final WeakReference executionDelegatorReference; + + ResponseHandler(Looper looper, WeakReference executionDelegator) { + super(looper); + this.executionDelegatorReference = executionDelegator; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case JOB_FINISHED: + if (msg.obj instanceof JobInvocation) { + ExecutionDelegator delegator = this.executionDelegatorReference.get(); + if (delegator == null) { + Log.wtf(TAG, "handleMessage: service was unexpectedly GC'd" + + ", can't send job result"); + return; + } + + delegator.onJobFinishedMessage((JobInvocation) msg.obj, msg.arg1); + return; + } + + Log.wtf(TAG, "handleMessage: unknown obj returned"); + return; + + default: + Log.wtf(TAG, "handleMessage: unknown message type received: " + msg.what); + } + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/FirebaseJobDispatcher.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/FirebaseJobDispatcher.java new file mode 100644 index 000000000..1a76093f8 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/FirebaseJobDispatcher.java @@ -0,0 +1,211 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.RetryStrategy.RetryPolicy; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * The FirebaseJobDispatcher provides a driver-agnostic API for scheduling and cancelling Jobs. + * + * @see #FirebaseJobDispatcher(Driver) + * @see Driver + * @see JobParameters + */ +public final class FirebaseJobDispatcher { + /** + * Indicates the schedule request seems to have been successful. + */ + public final static int SCHEDULE_RESULT_SUCCESS = 0; + + /** + * Indicates the schedule request encountered an unknown error. + */ + public final static int SCHEDULE_RESULT_UNKNOWN_ERROR = 1; + + /** + * Indicates the schedule request failed because the driver was unavailable. + */ + public final static int SCHEDULE_RESULT_NO_DRIVER_AVAILABLE = 2; + + /** + * Indicates the schedule request failed because the Trigger was unsupported. + */ + public final static int SCHEDULE_RESULT_UNSUPPORTED_TRIGGER = 3; + + /** + * Indicates the schedule request failed because the service is not exposed or configured + * correctly. + */ + public final static int SCHEDULE_RESULT_BAD_SERVICE = 4; + + /** + * Indicates the cancel request seems to have been successful. + */ + public final static int CANCEL_RESULT_SUCCESS = 0; + /** + * Indicates the cancel request encountered an unknown error. + */ + public final static int CANCEL_RESULT_UNKNOWN_ERROR = 1; + /** + * Indicates the cancel request failed because the driver was unavailable. + */ + public final static int CANCEL_RESULT_NO_DRIVER_AVAILABLE = 2; + /** + * The backing Driver for this instance. + */ + private final Driver mDriver; + /** + * The ValidationEnforcer configured for the current Driver. + */ + private final ValidationEnforcer mValidator; + /** + * Single instance of a RetryStrategy.Builder, configured with the current driver's validation + * settings. We can do this because the RetryStrategy.Builder is stateless. + */ + private RetryStrategy.Builder mRetryStrategyBuilder; + + /** + * Instantiates a new FirebaseJobDispatcher using the provided Driver. + */ + public FirebaseJobDispatcher(Driver driver) { + mDriver = driver; + mValidator = new ValidationEnforcer(mDriver.getValidator()); + mRetryStrategyBuilder = new RetryStrategy.Builder(mValidator); + } + + /** + * Attempts to schedule the provided Job. + *

+ * Returns one of the SCHEDULE_RESULT_ constants. + */ + @ScheduleResult + public int schedule(@NonNull Job job) { + if (!mDriver.isAvailable()) { + return SCHEDULE_RESULT_NO_DRIVER_AVAILABLE; + } + + return mDriver.schedule(job); + } + + /** + * Attempts to cancel the Job that matches the provided tag and endpoint. + *

+ * Returns one of the CANCEL_RESULT_ constants. + */ + @CancelResult + public int cancel(@NonNull String tag) { + if (!mDriver.isAvailable()) { + return CANCEL_RESULT_NO_DRIVER_AVAILABLE; + } + + return mDriver.cancel(tag); + } + + /** + * Attempts to cancel all Jobs registered for this package. + *

+ * Returns one of the CANCEL_RESULT_ constants. + */ + @CancelResult + public int cancelAll() { + if (!mDriver.isAvailable()) { + return CANCEL_RESULT_NO_DRIVER_AVAILABLE; + } + + return mDriver.cancelAll(); + } + + /** + * Attempts to schedule the provided Job, throwing an exception if it fails. + * + * @throws ScheduleFailedException + */ + public void mustSchedule(Job job) { + if (schedule(job) != SCHEDULE_RESULT_SUCCESS) { + throw new ScheduleFailedException(); + } + } + + /** + * Returns a ValidationEnforcer configured for the current Driver. + */ + public ValidationEnforcer getValidator() { + return mValidator; + } + + /** + * Creates a new Job.Builder, configured with the current driver's validation settings. + */ + @NonNull + public Job.Builder newJobBuilder() { + return new Job.Builder(mValidator); + } + + /** + * Creates a new RetryStrategy from the provided parameters, validated with the current driver's + * {@link JobValidator}. + * + * @param policy the backoff policy to use. One of the {@link RetryPolicy} constants. + * @param initialBackoff the initial backoff, in seconds. + * @param maximumBackoff the maximum backoff, in seconds. + * @throws ValidationEnforcer.ValidationException + * @see RetryStrategy + */ + public RetryStrategy newRetryStrategy(@RetryPolicy int policy, int initialBackoff, + int maximumBackoff) { + + return mRetryStrategyBuilder.build(policy, initialBackoff, maximumBackoff); + } + + /** + * Results that can legally be returned from {@link #schedule(Job)} calls. + */ + @IntDef({ + SCHEDULE_RESULT_SUCCESS, + SCHEDULE_RESULT_UNKNOWN_ERROR, + SCHEDULE_RESULT_NO_DRIVER_AVAILABLE, + SCHEDULE_RESULT_UNSUPPORTED_TRIGGER, + SCHEDULE_RESULT_BAD_SERVICE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScheduleResult { + } + + /** + * Results that can legally be returned from {@link #cancel(String)} or {@link #cancelAll()} + * calls. + */ + @IntDef({ + CANCEL_RESULT_SUCCESS, + CANCEL_RESULT_UNKNOWN_ERROR, + CANCEL_RESULT_NO_DRIVER_AVAILABLE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CancelResult { + } + + /** + * Thrown when a {@link FirebaseJobDispatcher#schedule(com.firebase.jobdispatcher.Job)} call + * fails. + */ + public final static class ScheduleFailedException extends RuntimeException { + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractor.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractor.java new file mode 100644 index 000000000..03fca073d --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractor.java @@ -0,0 +1,247 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.util.Log; +import android.util.Pair; +import java.util.ArrayList; + +/** + * Responsible for extracting a JobCallback from a given Bundle. + * + *

Google Play services will send the Binder packed inside a simple strong Binder wrapper ({@link + * #PENDING_CALLBACK_CLASS}) under the key {@link #BUNDLE_KEY_CALLBACK "callback"}. + */ +/* package */ final class GooglePlayCallbackExtractor { + + private static final String TAG = GooglePlayReceiver.TAG; + private static final String ERROR_NULL_CALLBACK = "No callback received, terminating"; + private static final String ERROR_INVALID_CALLBACK = "Bad callback received, terminating"; + + /** The Parcelable class that wraps the Binder we need to access. */ + private static final String PENDING_CALLBACK_CLASS = + "com.google.android.gms.gcm.PendingCallback"; + /** The key for the wrapped Binder. */ + private static final String BUNDLE_KEY_CALLBACK = "callback"; + /** A magic number that indicates the following bytes belong to a Bundle. */ + private static final int BUNDLE_MAGIC = 0x4C444E42; + /** A magic number that indicates the following value is a Parcelable. */ + private static final int VAL_PARCELABLE = 4; + + // GuardedBy("GooglePlayCallbackExtractor.class") + private static Boolean shouldReadKeysAsStringsCached = null; + + public Pair extractCallback(@Nullable Bundle data) { + if (data == null) { + Log.e(TAG, ERROR_NULL_CALLBACK); + return null; + } + + return extractWrappedBinderFromParcel(data); + } + + /** + * Bundles are written out in the following format: + * A header, which consists of: + *

    + *
  1. length (int)
  2. + *
  3. magic number ({@link #BUNDLE_MAGIC}) (int)
  4. + *
  5. number of entries (int)
  6. + *
+ *

+ * Then the map values, each of which looks like this: + *

    + *
  1. string key
  2. + *
  3. int type marker
  4. + *
  5. (any) parceled value
  6. + *
+ *

+ * We're just going to iterate over the map looking for the right key (BUNDLE_KEY_CALLBACK) + * and try and read the IBinder straight from the parcelled data. This is entirely dependent + * on the implementation of Parcel, but these specific parts of Parcel / Bundle haven't + * changed since 2008 and newer versions of Android will ship with newer versions of Google + * Play services which embed the IBinder directly into the Bundle (no need to deal with the + * Parcelable issues). + */ + @Nullable + @SuppressLint("ParcelClassLoader") + private Pair extractWrappedBinderFromParcel(Bundle data) { + Bundle cleanBundle = new Bundle(); + Parcel serialized = toParcel(data); + JobCallback callback = null; + + try { + int length = serialized.readInt(); + if (length <= 0) { + // Empty Bundle + Log.w(TAG, ERROR_NULL_CALLBACK); + return null; + } + + int magic = serialized.readInt(); + if (magic != BUNDLE_MAGIC) { + // Not a Bundle + Log.w(TAG, ERROR_NULL_CALLBACK); + return null; + } + + int numEntries = serialized.readInt(); + for (int i = 0; i < numEntries; i++) { + String entryKey = readKey(serialized); + if (entryKey == null) { + continue; + } + + if (!(callback == null && BUNDLE_KEY_CALLBACK.equals(entryKey))) { + // If it's not the 'callback' key, we can just read it using the standard + // mechanisms because we're not afraid of rogue BadParcelableExceptions. + Object value = serialized.readValue(null /* class loader */); + if (value instanceof String) { + cleanBundle.putString(entryKey, (String) value); + } else if (value instanceof Boolean) { + cleanBundle.putBoolean(entryKey, (boolean) value); + } else if (value instanceof Integer) { + cleanBundle.putInt(entryKey, (int) value); + } else if (value instanceof ArrayList) { + // The only acceptable ArrayList in a Bundle is one that consists entirely + // of Parcelables, so this cast is safe. + @SuppressWarnings("unchecked") // safe by specification + ArrayList arrayList = (ArrayList) value; + cleanBundle.putParcelableArrayList(entryKey, arrayList); + } else if (value instanceof Bundle) { + cleanBundle.putBundle(entryKey, (Bundle) value); + } else if (value instanceof Parcelable) { + cleanBundle.putParcelable(entryKey, (Parcelable) value); + } + + // Move to the next key + continue; + } + + int typeTag = serialized.readInt(); + if (typeTag != VAL_PARCELABLE) { + // If the key is correct ("callback"), but it's not a Parcelable then something + // went wrong and we should bail. + Log.w(TAG, ERROR_INVALID_CALLBACK); + return null; + } + + String clsname = serialized.readString(); + if (!PENDING_CALLBACK_CLASS.equals(clsname)) { + // If it's a Parcelable, but not one we recognize then we should not try and + // unpack it. + Log.w(TAG, ERROR_INVALID_CALLBACK); + return null; + } + + // Instead of trying to instantiate clsname, we'll just read its single member. + IBinder remote = serialized.readStrongBinder(); + callback = new GooglePlayJobCallback(remote); + } + + if (callback == null) { + Log.w(TAG, ERROR_NULL_CALLBACK); + return null; + } + return Pair.create(callback, cleanBundle); + } finally { + serialized.recycle(); + } + } + + private static Parcel toParcel(Bundle data) { + Parcel serialized = Parcel.obtain(); + data.writeToParcel(serialized, 0); + serialized.setDataPosition(0); + return serialized; + } + + /** + * Reads the next key (String) from the provided {@code serialized} Parcel. + * + *

Naively using {@link Parcel#readString()} fails on versions of Android older than L, + * whereas {@link Parcel#readValue(ClassLoader)} works on older versions but fails on anything L + * or newer. + */ + private String readKey(Parcel serialized) { + if (shouldReadKeysAsStrings()) { + return serialized.readString(); + } + + // Older platforms require readValue + Object entryKeyObj = serialized.readValue(null /* Use the system ClassLoader */); + if (!(entryKeyObj instanceof String)) { + // Should never happen (Bundle keys are always Strings) + Log.w(TAG, ERROR_INVALID_CALLBACK); + return null; + } + + return (String) entryKeyObj; + } + + /** + * Checks whether {@link Parcel#readString()} or {@link Parcel#readValue()} should be used to + * access Bundle keys from a serialized Parcel. Commit {@link + * https://android.googlesource.com/platform/frameworks/base/+/9c3e74f + * I57bda9eb79ceaaa9c1b94ad49d9e462b52102149} (which only officially landed in Lollipop) changed + * from using writeValue to writeString for Bundle keys. Some OEMs have pulled this change into + * their KitKat fork, so we can't trust the SDK version check. Instead, we'll write a dummy + * Bundle to a Parcel and figure it out using that. + * + * The check is cached because the result can't change during runtime. + */ + private static synchronized boolean shouldReadKeysAsStrings() { + // We're pretty sure that readString() should always be used on L+, but if we shortcircuit + // this check then we have no evidence that this code is functioning correctly on KitKat + // devices that have the corresponding writeString() change. + if (shouldReadKeysAsStringsCached == null) { + final String expectedKey = "key"; + Bundle testBundle = new Bundle(); + testBundle.putString(expectedKey, "value"); + Parcel testParcel = toParcel(testBundle); + try { + // length + checkCondition(testParcel.readInt() > 0); + // magic + checkCondition(testParcel.readInt() == BUNDLE_MAGIC); + // num entries + checkCondition(testParcel.readInt() == 1); + + shouldReadKeysAsStringsCached = expectedKey.equals(testParcel.readString()); + } catch (RuntimeException e) { + shouldReadKeysAsStringsCached = Boolean.FALSE; + } finally { + testParcel.recycle(); + } + } + + return shouldReadKeysAsStringsCached; + } + + /** Throws an {@code IllegalStateException} if {@code condition} is false. */ + private static void checkCondition(boolean condition) { + if (!condition) { + throw new IllegalStateException(); + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java new file mode 100644 index 000000000..ae76808cd --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java @@ -0,0 +1,157 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleResult; + +/** + * GooglePlayDriver provides an implementation of Driver for devices with Google Play + * services installed. This backend does not do any availability checks and any uses should be + * guarded with a call to {@code GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)} + * + * @see GoogleApiAvailability + */ +public final class GooglePlayDriver implements Driver { + static final String BACKEND_PACKAGE = "com.google.android.gms"; + private final static String ACTION_SCHEDULE = "com.google.android.gms.gcm.ACTION_SCHEDULE"; + + private final static String BUNDLE_PARAM_SCHEDULER_ACTION = "scheduler_action"; + private final static String BUNDLE_PARAM_TAG = "tag"; + private final static String BUNDLE_PARAM_TOKEN = "app"; + private final static String BUNDLE_PARAM_COMPONENT = "component"; + + private final static String SCHEDULER_ACTION_SCHEDULE_TASK = "SCHEDULE_TASK"; + private final static String SCHEDULER_ACTION_CANCEL_TASK = "CANCEL_TASK"; + private final static String SCHEDULER_ACTION_CANCEL_ALL = "CANCEL_ALL"; + private static final String INTENT_PARAM_SOURCE = "source"; + private static final String INTENT_PARAM_SOURCE_VERSION = "source_version"; + + private static final int JOB_DISPATCHER_SOURCE_CODE = 1 << 3; + private static final int JOB_DISPATCHER_SOURCE_VERSION_CODE = 1; + + private final JobValidator mValidator; + /** + * The application Context. Used to send broadcasts. + */ + private final Context mContext; + /** + * A PendingIntent from this package. Passed inside the broadcast so the receiver can verify the + * sender's package. + */ + private final PendingIntent mToken; + /** + * Turns Jobs into Bundles. + */ + private final GooglePlayJobWriter mWriter; + /** + * This is hardcoded to true to avoid putting an unnecessary dependency on the Google Play + * services library. + */ + //TODO: this is an unsatisfying solution + private final boolean mAvailable = true; + + /** + * Instantiates a new GooglePlayDriver. + */ + public GooglePlayDriver(Context context) { + mContext = context; + mToken = PendingIntent.getBroadcast(context, 0, new Intent(), 0); + mWriter = new GooglePlayJobWriter(); + mValidator = new DefaultJobValidator(context); + } + + @Override + public boolean isAvailable() { + return mAvailable; + } + + /** + * Schedules the provided Job. + */ + @Override + @ScheduleResult + public int schedule(@NonNull Job job) { + mContext.sendBroadcast(createScheduleRequest(job)); + + return FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; + } + + @Override + public int cancel(@NonNull String tag) { + mContext.sendBroadcast(createCancelRequest(tag)); + + return FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; + } + + @Override + public int cancelAll() { + mContext.sendBroadcast(createBatchCancelRequest()); + + return FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; + } + + @NonNull + protected Intent createCancelRequest(@NonNull String tag) { + Intent cancelReq = createSchedulerIntent(SCHEDULER_ACTION_CANCEL_TASK); + cancelReq.putExtra(BUNDLE_PARAM_TAG, tag); + cancelReq.putExtra(BUNDLE_PARAM_COMPONENT, new ComponentName(mContext, getReceiverClass())); + return cancelReq; + } + + @NonNull + protected Intent createBatchCancelRequest() { + Intent cancelReq = createSchedulerIntent(SCHEDULER_ACTION_CANCEL_ALL); + cancelReq.putExtra(BUNDLE_PARAM_COMPONENT, new ComponentName(mContext, getReceiverClass())); + return cancelReq; + } + + @NonNull + protected Class getReceiverClass() { + return GooglePlayReceiver.class; + } + + @NonNull + @Override + public JobValidator getValidator() { + return mValidator; + } + + @NonNull + private Intent createScheduleRequest(JobParameters job) { + Intent scheduleReq = createSchedulerIntent(SCHEDULER_ACTION_SCHEDULE_TASK); + scheduleReq.putExtras(mWriter.writeToBundle(job, scheduleReq.getExtras())); + return scheduleReq; + } + + @NonNull + private Intent createSchedulerIntent(String schedulerAction) { + Intent scheduleReq = new Intent(ACTION_SCHEDULE); + + scheduleReq.setPackage(BACKEND_PACKAGE); + scheduleReq.putExtra(BUNDLE_PARAM_SCHEDULER_ACTION, schedulerAction); + scheduleReq.putExtra(BUNDLE_PARAM_TOKEN, mToken); + scheduleReq.putExtra(INTENT_PARAM_SOURCE, JOB_DISPATCHER_SOURCE_CODE); + scheduleReq.putExtra(INTENT_PARAM_SOURCE_VERSION, JOB_DISPATCHER_SOURCE_VERSION_CODE); + + return scheduleReq; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobCallback.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobCallback.java new file mode 100644 index 000000000..7f47fe598 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobCallback.java @@ -0,0 +1,56 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * Wraps the GooglePlay-specific callback class in a JobCallback-compatible interface. + */ +/* package */ final class GooglePlayJobCallback implements JobCallback { + + private static final String DESCRIPTOR = "com.google.android.gms.gcm.INetworkTaskCallback"; + /** The only supported transaction ID. */ + private static final int TRANSACTION_TASK_FINISHED = IBinder.FIRST_CALL_TRANSACTION + 1; + + private final IBinder mRemote; + + public GooglePlayJobCallback(IBinder binder) { + mRemote = binder; + } + + @Override + public void jobFinished(@JobService.JobResult int status) { + Parcel request = Parcel.obtain(); + Parcel response = Parcel.obtain(); + try { + request.writeInterfaceToken(DESCRIPTOR); + request.writeInt(status); + + mRemote.transact(TRANSACTION_TASK_FINISHED, request, response, 0); + + response.readException(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + request.recycle(); + response.recycle(); + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobWriter.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobWriter.java new file mode 100644 index 000000000..0389f90f7 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayJobWriter.java @@ -0,0 +1,198 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.IntDef; +import android.support.annotation.VisibleForTesting; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; +import com.firebase.jobdispatcher.RetryStrategy.RetryPolicy; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/* package */ final class GooglePlayJobWriter { + + static final String REQUEST_PARAM_UPDATE_CURRENT = "update_current"; + static final String REQUEST_PARAM_EXTRAS = "extras"; + static final String REQUEST_PARAM_PERSISTED = "persisted"; + static final String REQUEST_PARAM_REQUIRED_NETWORK = "requiredNetwork"; + static final String REQUEST_PARAM_REQUIRES_CHARGING = "requiresCharging"; + static final String REQUEST_PARAM_REQUIRES_IDLE = "requiresIdle"; + static final String REQUEST_PARAM_RETRY_STRATEGY = "retryStrategy"; + static final String REQUEST_PARAM_SERVICE = "service"; + static final String REQUEST_PARAM_TAG = "tag"; + + static final String REQUEST_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS = + "initial_backoff_seconds"; + static final String REQUEST_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS = + "maximum_backoff_seconds"; + static final String REQUEST_PARAM_RETRY_STRATEGY_POLICY = "retry_policy"; + + static final String REQUEST_PARAM_TRIGGER_TYPE = "trigger_type"; + static final String REQUEST_PARAM_TRIGGER_WINDOW_END = "window_end"; + static final String REQUEST_PARAM_TRIGGER_WINDOW_FLEX = "period_flex"; + static final String REQUEST_PARAM_TRIGGER_WINDOW_PERIOD = "period"; + static final String REQUEST_PARAM_TRIGGER_WINDOW_START = "window_start"; + + @VisibleForTesting + /* package */ static final int LEGACY_RETRY_POLICY_EXPONENTIAL = 0; + @VisibleForTesting + /* package */ static final int LEGACY_RETRY_POLICY_LINEAR = 1; + @VisibleForTesting + /* package */ final static int LEGACY_NETWORK_UNMETERED = 1; + @VisibleForTesting + /* package */ final static int LEGACY_NETWORK_CONNECTED = 0; + @VisibleForTesting + /* package */ final static int LEGACY_NETWORK_ANY = 2; + + private JobCoder jobCoder = new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, false); + + private static void writeExecutionWindowTriggerToBundle(JobParameters job, Bundle b, + JobTrigger.ExecutionWindowTrigger trigger) { + + b.putInt(REQUEST_PARAM_TRIGGER_TYPE, BundleProtocol.TRIGGER_TYPE_EXECUTION_WINDOW); + + if (job.isRecurring()) { + b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_PERIOD, + trigger.getWindowEnd()); + b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_FLEX, + trigger.getWindowEnd() - trigger.getWindowStart()); + } else { + b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_START, + trigger.getWindowStart()); + b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_END, + trigger.getWindowEnd()); + } + } + + private static void writeImmediateTriggerToBundle(Bundle b) { + b.putInt(REQUEST_PARAM_TRIGGER_TYPE, BundleProtocol.TRIGGER_TYPE_IMMEDIATE); + b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_START, 0); + b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_END, 30); + } + + private void writeContentUriTriggerToBundle(Bundle data, ContentUriTrigger uriTrigger) { + data.putInt(BundleProtocol.PACKED_PARAM_TRIGGER_TYPE, + BundleProtocol.TRIGGER_TYPE_CONTENT_URI); + + int size = uriTrigger.getUris().size(); + int[] flagsArray = new int[size]; + Uri[] uriArray = new Uri[size]; + for (int i = 0; i < size; i++) { + ObservedUri uri = uriTrigger.getUris().get(i); + flagsArray[i] = uri.getFlags(); + uriArray[i] = uri.getUri(); + } + data.putIntArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY, flagsArray); + data.putParcelableArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_ARRAY, uriArray); + } + + public Bundle writeToBundle(JobParameters job, Bundle b) { + b.putString(REQUEST_PARAM_TAG, job.getTag()); + b.putBoolean(REQUEST_PARAM_UPDATE_CURRENT, job.shouldReplaceCurrent()); + + boolean persisted = job.getLifetime() == Lifetime.FOREVER; + b.putBoolean(REQUEST_PARAM_PERSISTED, persisted); + b.putString(REQUEST_PARAM_SERVICE, GooglePlayReceiver.class.getName()); + + writeTriggerToBundle(job, b); + writeConstraintsToBundle(job, b); + writeRetryStrategyToBundle(job, b); + + // Embed the job spec (minus extras) into the extras (under a prefix) + Bundle extras = job.getExtras(); + if (extras == null) { + extras = new Bundle(); + } + b.putBundle(REQUEST_PARAM_EXTRAS, jobCoder.encode(job, extras)); + + return b; + } + + private void writeRetryStrategyToBundle(JobParameters job, Bundle b) { + RetryStrategy strategy = job.getRetryStrategy(); + + Bundle rb = new Bundle(); + rb.putInt(REQUEST_PARAM_RETRY_STRATEGY_POLICY, + convertRetryPolicyToLegacyVersion(strategy.getPolicy())); + rb.putInt(REQUEST_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS, + strategy.getInitialBackoff()); + rb.putInt(REQUEST_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS, + strategy.getMaximumBackoff()); + + b.putBundle(REQUEST_PARAM_RETRY_STRATEGY, rb); + } + + private int convertRetryPolicyToLegacyVersion(@RetryPolicy int policy) { + switch (policy) { + case RetryStrategy.RETRY_POLICY_LINEAR: + return LEGACY_RETRY_POLICY_LINEAR; + + case RetryStrategy.RETRY_POLICY_EXPONENTIAL: + // fallthrough + default: + return LEGACY_RETRY_POLICY_EXPONENTIAL; + } + } + + private void writeTriggerToBundle(JobParameters job, Bundle b) { + final JobTrigger trigger = job.getTrigger(); + + if (trigger == Trigger.NOW) { + writeImmediateTriggerToBundle(b); + } else if (trigger instanceof JobTrigger.ExecutionWindowTrigger) { + writeExecutionWindowTriggerToBundle(job, b, (JobTrigger.ExecutionWindowTrigger) trigger); + } else if (trigger instanceof JobTrigger.ContentUriTrigger) { + writeContentUriTriggerToBundle(b, (JobTrigger.ContentUriTrigger) trigger); + } else { + throw new IllegalArgumentException("Unknown trigger: " + trigger.getClass()); + } + } + + private void writeConstraintsToBundle(JobParameters job, Bundle b) { + int c = Constraint.compact(job.getConstraints()); + + b.putBoolean(REQUEST_PARAM_REQUIRES_CHARGING, + (c & Constraint.DEVICE_CHARGING) == Constraint.DEVICE_CHARGING); + b.putBoolean(REQUEST_PARAM_REQUIRES_IDLE, + (c & Constraint.DEVICE_IDLE) == Constraint.DEVICE_IDLE); + b.putInt(REQUEST_PARAM_REQUIRED_NETWORK, convertConstraintsToLegacyNetConstant(c)); + } + + /** + * Converts a bitmap of Constraint values into a LegacyNetworkConstraint constant (int). + */ + @LegacyNetworkConstant + private int convertConstraintsToLegacyNetConstant(int constraintMap) { + int reqNet = LEGACY_NETWORK_ANY; + + reqNet = (constraintMap & Constraint.ON_ANY_NETWORK) == Constraint.ON_ANY_NETWORK + ? LEGACY_NETWORK_CONNECTED + : reqNet; + + reqNet = (constraintMap & Constraint.ON_UNMETERED_NETWORK) == Constraint.ON_UNMETERED_NETWORK + ? LEGACY_NETWORK_UNMETERED + : reqNet; + + return reqNet; + } + + @IntDef({LEGACY_NETWORK_ANY, LEGACY_NETWORK_CONNECTED, LEGACY_NETWORK_UNMETERED}) + @Retention(RetentionPolicy.SOURCE) + private @interface LegacyNetworkConstant {} +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessageHandler.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessageHandler.java new file mode 100644 index 000000000..664805f67 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessageHandler.java @@ -0,0 +1,114 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG; +import static com.firebase.jobdispatcher.GooglePlayReceiver.TAG; + +import android.annotation.TargetApi; +import android.app.AppOpsManager; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.util.Log; +import com.firebase.jobdispatcher.JobInvocation.Builder; + +/** + * A messenger for communication with GCM Network Scheduler. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +class GooglePlayMessageHandler extends Handler { + + static final int MSG_START_EXEC = 1; + static final int MSG_STOP_EXEC = 2; + static final int MSG_RESULT = 3; + private static final int MSG_INIT = 4; + private final GooglePlayReceiver googlePlayReceiver; + + public GooglePlayMessageHandler(Looper mainLooper, GooglePlayReceiver googlePlayReceiver) { + super(mainLooper); + this.googlePlayReceiver = googlePlayReceiver; + } + + @Override + public void handleMessage(Message message) { + if (message == null) { + return; + } + + AppOpsManager appOpsManager = (AppOpsManager) googlePlayReceiver.getApplicationContext() + .getSystemService(Context.APP_OPS_SERVICE); + try { + appOpsManager.checkPackage(message.sendingUid, GooglePlayDriver.BACKEND_PACKAGE); + } catch (SecurityException e) { + Log.e(TAG, "Message was not sent from GCM."); + return; + } + + switch (message.what) { + case MSG_START_EXEC: + handleStartMessage(message); + break; + + case MSG_STOP_EXEC: + handleStopMessage(message); + break; + + case MSG_INIT: + // Not implemented. + break; + + default: + Log.e(TAG, "Unrecognized message received: " + message); + break; + } + } + + private void handleStartMessage(Message message) { + final Bundle data = message.getData(); + + final Messenger replyTo = message.replyTo; + String tag = data.getString(REQUEST_PARAM_TAG); + if (replyTo == null || tag == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Invalid start execution message."); + } + return; + } + + GooglePlayMessengerCallback messengerCallback = + new GooglePlayMessengerCallback(replyTo, tag); + JobInvocation jobInvocation = googlePlayReceiver.prepareJob(messengerCallback, data); + googlePlayReceiver.getExecutionDelegator().executeJob(jobInvocation); + } + + private void handleStopMessage(Message message) { + Builder builder = GooglePlayReceiver.getJobCoder().decode(message.getData()); + if (builder == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Invalid stop execution message."); + } + return; + } + JobInvocation job = builder.build(); + googlePlayReceiver.getExecutionDelegator().stopJob(job); + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessengerCallback.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessengerCallback.java new file mode 100644 index 000000000..06fb153f3 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayMessengerCallback.java @@ -0,0 +1,61 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG; + +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.JobService.JobResult; + +/** + * Wraps the GooglePlay messenger in a JobCallback-compatible interface. + */ +class GooglePlayMessengerCallback implements JobCallback { + + private final Messenger messenger; + private final String tag; + + GooglePlayMessengerCallback(Messenger messenger, String tag) { + this.messenger = messenger; + this.tag = tag; + } + + @Override + public void jobFinished(@JobResult int status) { + try { + messenger.send(createResultMessage(status)); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + @NonNull + private Message createResultMessage(int result) { + final Message msg = Message.obtain(); + msg.what = GooglePlayMessageHandler.MSG_RESULT; + msg.arg1 = result; + + Bundle b = new Bundle(); + b.putString(REQUEST_PARAM_TAG, tag); + msg.setData(b); + return msg; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayReceiver.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayReceiver.java new file mode 100644 index 000000000..e8dc60609 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayReceiver.java @@ -0,0 +1,274 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Looper; +import android.os.Messenger; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.util.SimpleArrayMap; +import android.util.Log; +import android.util.Pair; +import com.firebase.jobdispatcher.Job.Builder; +import com.firebase.jobdispatcher.JobService.JobResult; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; + +/** + * Handles incoming execute requests from the GooglePlay driver and forwards them to your Service. + */ +public class GooglePlayReceiver extends Service implements ExecutionDelegator.JobFinishedCallback { + /** + * Logging tag. + */ + /* package */ static final String TAG = "FJD.GooglePlayReceiver"; + /** + * The action sent by Google Play services that triggers job execution. + */ + @VisibleForTesting + static final String ACTION_EXECUTE = "com.google.android.gms.gcm.ACTION_TASK_READY"; + + /** Action sent by Google Play services when your app has been updated. */ + @VisibleForTesting + static final String ACTION_INITIALIZE = "com.google.android.gms.gcm.SERVICE_ACTION_INITIALIZE"; + + private static final String ERROR_NULL_INTENT = "Null Intent passed, terminating"; + private static final String ERROR_UNKNOWN_ACTION = "Unknown action received, terminating"; + private static final String ERROR_NO_DATA = "No data provided, terminating"; + + private static final JobCoder prefixedCoder = + new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, true); + + private final GooglePlayCallbackExtractor callbackExtractor = new GooglePlayCallbackExtractor(); + + /** + * The single Messenger that's returned from valid onBind requests. Guarded by intrinsic lock. + */ + @VisibleForTesting + Messenger serviceMessenger; + + /** + * Driver for rescheduling jobs. Guarded by intrinsic lock. + */ + @VisibleForTesting + Driver driver; + + /** + * Guarded by intrinsic lock. + */ + @VisibleForTesting + ValidationEnforcer validationEnforcer; + + /** + * The ExecutionDelegator used to communicate with client JobServices. + * Guarded by intrinsic lock. + */ + private ExecutionDelegator executionDelegator; + + /** + * The most recent startId passed to onStartCommand. + * Guarded by intrinsic lock. + */ + private int latestStartId; + + /** + * Endpoint (String) -> Tag (String) -> JobCallback + */ + private SimpleArrayMap> callbacks = + new SimpleArrayMap<>(1); + + private static void sendResultSafely(JobCallback callback, int result) { + try { + callback.jobFinished(result); + } catch (Throwable e) { + Log.e(TAG, "Encountered error running callback", e.getCause()); + } + } + + @Override + public final int onStartCommand(Intent intent, int flags, int startId) { + try { + super.onStartCommand(intent, flags, startId); + + if (intent == null) { + Log.w(TAG, ERROR_NULL_INTENT); + return START_NOT_STICKY; + } + + String action = intent.getAction(); + if (ACTION_EXECUTE.equals(action)) { + getExecutionDelegator().executeJob(prepareJob(intent)); + return START_NOT_STICKY; + } else if (ACTION_INITIALIZE.equals(action)) { + return START_NOT_STICKY; + } + + Log.e(TAG, ERROR_UNKNOWN_ACTION); + return START_NOT_STICKY; + } finally { + synchronized (this) { + latestStartId = startId; + if (callbacks.isEmpty()) { + stopSelf(latestStartId); + } + } + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + // Only Lollipop+ supports UID checking messages, so we can't trust this system on older + // platforms. + if (intent == null + || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP + || !ACTION_EXECUTE.equals(intent.getAction())) { + return null; + } + return getServiceMessenger().getBinder(); + } + + private synchronized Messenger getServiceMessenger() { + if (serviceMessenger == null) { + serviceMessenger = + new Messenger(new GooglePlayMessageHandler(Looper.getMainLooper(), this)); + } + return serviceMessenger; + } + + /* package */ synchronized ExecutionDelegator getExecutionDelegator() { + if (executionDelegator == null) { + executionDelegator = new ExecutionDelegator(this, this); + } + return executionDelegator; + } + + @NonNull + private synchronized Driver getGooglePlayDriver() { + if (driver == null) { + driver = new GooglePlayDriver(getApplicationContext()); + } + return driver; + } + + @NonNull + private synchronized ValidationEnforcer getValidationEnforcer() { + if (validationEnforcer == null) { + validationEnforcer = new ValidationEnforcer(getGooglePlayDriver().getValidator()); + } + return validationEnforcer; + } + + @Nullable + @VisibleForTesting + JobInvocation prepareJob(Intent intent) { + Bundle intentExtras = intent.getExtras(); + if (intentExtras == null) { + Log.e(TAG, ERROR_NO_DATA); + return null; + } + + // get the callback first. If we don't have this we can't talk back to the backend. + Pair extraction = callbackExtractor.extractCallback(intentExtras); + if (extraction == null) { + Log.i(TAG, "no callback found"); + return null; + } + return prepareJob(extraction.first, extraction.second); + } + + @Nullable + synchronized JobInvocation prepareJob(JobCallback callback, Bundle bundle) { + JobInvocation job = prefixedCoder.decodeIntentBundle(bundle); + if (job == null) { + Log.e(TAG, "unable to decode job"); + sendResultSafely(callback, JobService.RESULT_FAIL_NORETRY); + return null; + } + SimpleArrayMap map = callbacks.get(job.getService()); + if (map == null) { + map = new SimpleArrayMap<>(1); + callbacks.put(job.getService(), map); + } + + map.put(job.getTag(), callback); + + return job; + } + + @Override + public synchronized void onJobFinished(@NonNull JobInvocation js, @JobResult int result) { + try { + SimpleArrayMap map = callbacks.get(js.getService()); + if (map == null) { + return; + } + JobCallback callback = map.remove(js.getTag()); + if (callback == null) { + return; + } + if (map.isEmpty()) { + callbacks.remove(js.getService()); + } + + if (needsToBeRescheduled(js, result)) { + reschedule(js); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "sending jobFinished for " + js.getTag() + " = " + result); + } + sendResultSafely(callback, result); + } + } finally { + if (callbacks.isEmpty()) { + // Safe to call stopSelf, even if we're being bound to + stopSelf(latestStartId); + } + } + } + + private void reschedule(JobInvocation jobInvocation) { + Job job = new Builder(getValidationEnforcer(), jobInvocation) + .setReplaceCurrent(true) + .build(); + + getGooglePlayDriver().schedule(job); + } + + /** + * Recurring content URI triggered jobs need to be rescheduled when execution is finished. + * + *

GooglePlay does not support recurring content URI triggered jobs. + * + *

{@link JobService#RESULT_FAIL_RETRY} needs to be sent or current triggered URIs will be + * lost. + */ + private static boolean needsToBeRescheduled(JobParameters job, int result) { + return job.isRecurring() + && job.getTrigger() instanceof ContentUriTrigger + && result != JobService.RESULT_FAIL_RETRY; + } + + static JobCoder getJobCoder() { + return prefixedCoder; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Job.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Job.java new file mode 100644 index 000000000..692404fe9 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Job.java @@ -0,0 +1,378 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.firebase.jobdispatcher.Constraint.JobConstraint; + +/** + * Job is the embodiment of a unit of work and an associated set of triggers, settings, and runtime + * constraints. + */ +public final class Job implements JobParameters { + private final String mService; + private final String mTag; + private final JobTrigger mTrigger; + private final RetryStrategy mRetryStrategy; + private final int mLifetime; + private final boolean mRecurring; + private final int[] mConstraints; + private final boolean mReplaceCurrent; + private Bundle mExtras; + + private Job(Builder builder) { + mService = builder.mServiceClassName; + mExtras = builder.mExtras; + mTag = builder.mTag; + mTrigger = builder.mTrigger; + mRetryStrategy = builder.mRetryStrategy; + mLifetime = builder.mLifetime; + mRecurring = builder.mRecurring; + mConstraints = builder.mConstraints != null ? builder.mConstraints : new int[0]; + mReplaceCurrent = builder.mReplaceCurrent; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public int[] getConstraints() { + return mConstraints; + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + public Bundle getExtras() { + return mExtras; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public RetryStrategy getRetryStrategy() { + return mRetryStrategy; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean shouldReplaceCurrent() { + return mReplaceCurrent; + } + + @Nullable + @Override + public TriggerReason getTriggerReason() { + return null; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getTag() { + return mTag; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public JobTrigger getTrigger() { + return mTrigger; + } + + /** + * {@inheritDoc} + */ + @Override + public int getLifetime() { + return mLifetime; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isRecurring() { + return mRecurring; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getService() { + return mService; + } + + /** + * A class that understands how to build a {@link Job}. Retrieved by calling + * {@link FirebaseJobDispatcher#newJobBuilder()}. + */ + public final static class Builder implements JobParameters { + private final ValidationEnforcer mValidator; + + private String mServiceClassName; + private Bundle mExtras; + private String mTag; + private JobTrigger mTrigger = Trigger.NOW; + private int mLifetime = Lifetime.UNTIL_NEXT_BOOT; + private int[] mConstraints; + + private RetryStrategy mRetryStrategy = RetryStrategy.DEFAULT_EXPONENTIAL; + private boolean mReplaceCurrent = false; + private boolean mRecurring = false; + + Builder(ValidationEnforcer validator) { + mValidator = validator; + } + + Builder(ValidationEnforcer validator, JobParameters job) { + mValidator = validator; + + mTag = job.getTag(); + mServiceClassName = job.getService(); + mTrigger = job.getTrigger(); + mRecurring = job.isRecurring(); + mLifetime = job.getLifetime(); + mConstraints = job.getConstraints(); + mExtras = job.getExtras(); + mRetryStrategy = job.getRetryStrategy(); + } + + /** + * Adds the provided constraint to the current list of runtime constraints. + */ + public Builder addConstraint(@JobConstraint int constraint) { + // Create a new, longer constraints array + int[] newConstraints = new int[mConstraints == null ? 1 : mConstraints.length + 1]; + + if (mConstraints != null && mConstraints.length != 0) { + // Copy all the old values over + System.arraycopy(mConstraints, 0, newConstraints, 0, mConstraints.length); + } + + // add the new value + newConstraints[newConstraints.length - 1] = constraint; + // update the pointer + mConstraints = newConstraints; + + return this; + } + + /** + * Sets whether this Job should replace pre-existing Jobs with the same tag. + */ + public Builder setReplaceCurrent(boolean replaceCurrent) { + mReplaceCurrent = replaceCurrent; + + return this; + } + + /** + * Builds the Job, using the settings provided so far. + * + * @throws ValidationEnforcer.ValidationException + */ + public Job build() { + mValidator.ensureValid(this); + + return new Job(this); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getService() { + return mServiceClassName; + } + + /** + * Sets the backing JobService class for the Job. See {@link #getService()}. + */ + public Builder setService(Class serviceClass) { + mServiceClassName = serviceClass == null ? null : serviceClass.getName(); + + return this; + } + + /** + * Sets the backing JobService class name for the Job. See {@link #getService()}. + * + *

Should not be exposed, for internal use only. + */ + Builder setServiceName(String serviceClassName) { + mServiceClassName = serviceClassName; + + return this; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getTag() { + return mTag; + } + + /** + * Sets the unique String tag used to identify the Job. See {@link #getTag()}. + */ + public Builder setTag(String tag) { + mTag = tag; + + return this; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public JobTrigger getTrigger() { + return mTrigger; + } + + /** + * Sets the Trigger used for the Job. See {@link #getTrigger()}. + */ + public Builder setTrigger(JobTrigger trigger) { + mTrigger = trigger; + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + @Lifetime.LifetimeConstant + public int getLifetime() { + return mLifetime; + } + + /** + * Sets the Job's lifetime, or how long it should persist. See {@link #getLifetime()}. + */ + public Builder setLifetime(@Lifetime.LifetimeConstant int lifetime) { + mLifetime = lifetime; + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isRecurring() { + return mRecurring; + } + + /** + * Sets whether the job should recur. The default is false. + */ + public Builder setRecurring(boolean recurring) { + mRecurring = recurring; + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + @JobConstraint + public int[] getConstraints() { + return mConstraints == null ? new int[]{} : mConstraints; + } + + /** + * Sets the Job's runtime constraints. See {@link #getConstraints()}. + */ + public Builder setConstraints(@JobConstraint int... constraints) { + mConstraints = constraints; + + return this; + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + public Bundle getExtras() { + return mExtras; + } + + /** + * Sets the user-defined extras associated with the Job. See {@link #getExtras()}. + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + + return this; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public RetryStrategy getRetryStrategy() { + return mRetryStrategy; + } + + /** + * Set the RetryStrategy used for the Job. See {@link #getRetryStrategy()}. + */ + public Builder setRetryStrategy(RetryStrategy retryStrategy) { + mRetryStrategy = retryStrategy; + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean shouldReplaceCurrent() { + return mReplaceCurrent; + } + + @Nullable + @Override + public TriggerReason getTriggerReason() { + return null; + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCallback.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCallback.java new file mode 100644 index 000000000..adeb82eaf --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCallback.java @@ -0,0 +1,28 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +/** + * JobCallback describes an object that knows how to send a JobResult back to the underlying + * execution driver. + */ +public interface JobCallback { + /** + * @throws RuntimeException + */ + void jobFinished(@JobService.JobResult int status); +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCoder.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCoder.java new file mode 100644 index 000000000..e5523587e --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCoder.java @@ -0,0 +1,253 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.Constraint.compact; +import static com.firebase.jobdispatcher.Constraint.uncompact; +import static com.firebase.jobdispatcher.ExecutionDelegator.TAG; + +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * JobCoder is a tool to encode and decode JobSpecs from Bundles. + */ +/* package */ final class JobCoder { + private final boolean includeExtras; + private final String prefix; + + private static final String JSON_URI_FLAGS = "uri_flags"; + private static final String JSON_URIS = "uris"; + + JobCoder(String prefix, boolean includeExtras) { + this.includeExtras = includeExtras; + this.prefix = prefix; + } + + @NonNull + Bundle encode(@NonNull JobParameters jobParameters, @NonNull Bundle data) { + if (data == null) { + throw new IllegalArgumentException("Unexpected null Bundle provided"); + } + + data.putInt(prefix + BundleProtocol.PACKED_PARAM_LIFETIME, + jobParameters.getLifetime()); + data.putBoolean(prefix + BundleProtocol.PACKED_PARAM_RECURRING, + jobParameters.isRecurring()); + data.putBoolean(prefix + BundleProtocol.PACKED_PARAM_REPLACE_CURRENT, + jobParameters.shouldReplaceCurrent()); + data.putString(prefix + BundleProtocol.PACKED_PARAM_TAG, + jobParameters.getTag()); + data.putString(prefix + BundleProtocol.PACKED_PARAM_SERVICE, + jobParameters.getService()); + data.putInt(prefix + BundleProtocol.PACKED_PARAM_CONSTRAINTS, + compact(jobParameters.getConstraints())); + + if (includeExtras) { + data.putBundle(prefix + BundleProtocol.PACKED_PARAM_EXTRAS, + jobParameters.getExtras()); + } + + encodeTrigger(jobParameters.getTrigger(), data); + encodeRetryStrategy(jobParameters.getRetryStrategy(), data); + + return data; + } + + JobInvocation decodeIntentBundle(@NonNull Bundle bundle) { + if (bundle == null) { + Log.e(TAG, "Unexpected null Bundle provided"); + return null; + } + + Bundle taskExtras = bundle.getBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS); + if (taskExtras == null) { + return null; + } + + JobInvocation.Builder builder = decode(taskExtras); + + List triggeredContentUris = + bundle.getParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS); + if (triggeredContentUris != null) { + builder.setTriggerReason(new TriggerReason(triggeredContentUris)); + } + return builder.build(); + } + + @Nullable + public JobInvocation.Builder decode(@NonNull Bundle data) { + if (data == null) { + throw new IllegalArgumentException("Unexpected null Bundle provided"); + } + + boolean recur = data.getBoolean(prefix + BundleProtocol.PACKED_PARAM_RECURRING); + boolean replaceCur = data.getBoolean(prefix + BundleProtocol.PACKED_PARAM_REPLACE_CURRENT); + int lifetime = data.getInt(prefix + BundleProtocol.PACKED_PARAM_LIFETIME); + int[] constraints = uncompact(data.getInt(prefix + BundleProtocol.PACKED_PARAM_CONSTRAINTS)); + + JobTrigger trigger = decodeTrigger(data); + RetryStrategy retryStrategy = decodeRetryStrategy(data); + + String tag = data.getString(prefix + BundleProtocol.PACKED_PARAM_TAG); + String service = data.getString(prefix + BundleProtocol.PACKED_PARAM_SERVICE); + + if (tag == null || service == null || trigger == null || retryStrategy == null) { + return null; + } + + JobInvocation.Builder builder = new JobInvocation.Builder(); + builder.setTag(tag); + builder.setService(service); + builder.setTrigger(trigger); + builder.setRetryStrategy(retryStrategy); + builder.setRecurring(recur); + //noinspection WrongConstant + builder.setLifetime(lifetime); + //noinspection WrongConstant + builder.setConstraints(constraints); + builder.setReplaceCurrent(replaceCur); + + // repack the taskExtras + builder.addExtras(data); + return builder; + } + + @NonNull + private JobTrigger decodeTrigger(Bundle data) { + switch (data.getInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE)) { + case BundleProtocol.TRIGGER_TYPE_IMMEDIATE: + return Trigger.NOW; + + case BundleProtocol.TRIGGER_TYPE_EXECUTION_WINDOW: + return Trigger.executionWindow( + data.getInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_START), + data.getInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_END)); + + case BundleProtocol.TRIGGER_TYPE_CONTENT_URI: + String uris = data.getString(prefix + BundleProtocol.PACKED_PARAM_OBSERVED_URI); + List observedUris = convertJsonToObservedUris(uris); + return Trigger.contentUriTrigger(Collections.unmodifiableList(observedUris)); + + default: + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unsupported trigger."); + } + return null; + } + } + + private void encodeTrigger(JobTrigger trigger, Bundle data) { + if (trigger == Trigger.NOW) { + data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE, + BundleProtocol.TRIGGER_TYPE_IMMEDIATE); + } else if (trigger instanceof JobTrigger.ExecutionWindowTrigger) { + JobTrigger.ExecutionWindowTrigger t = (JobTrigger.ExecutionWindowTrigger) trigger; + + data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE, + BundleProtocol.TRIGGER_TYPE_EXECUTION_WINDOW); + data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_START, + t.getWindowStart()); + data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_END, + t.getWindowEnd()); + } else if (trigger instanceof JobTrigger.ContentUriTrigger) { + data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE, + BundleProtocol.TRIGGER_TYPE_CONTENT_URI); + ContentUriTrigger uriTrigger = (ContentUriTrigger) trigger; + String jsonTrigger = convertObservedUrisToJsonString(uriTrigger.getUris()); + data.putString(prefix + BundleProtocol.PACKED_PARAM_OBSERVED_URI, jsonTrigger); + } else { + throw new IllegalArgumentException("Unsupported trigger."); + } + } + + private RetryStrategy decodeRetryStrategy(Bundle data) { + int policy = data.getInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_POLICY); + if (policy != RetryStrategy.RETRY_POLICY_EXPONENTIAL + && policy != RetryStrategy.RETRY_POLICY_LINEAR) { + + return RetryStrategy.DEFAULT_EXPONENTIAL; + } + + //noinspection WrongConstant + return new RetryStrategy( + policy, + data.getInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS), + data.getInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS)); + } + + private void encodeRetryStrategy(RetryStrategy retryStrategy, Bundle data) { + if (retryStrategy == null) { + retryStrategy = RetryStrategy.DEFAULT_EXPONENTIAL; + } + + data.putInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_POLICY, + retryStrategy.getPolicy()); + data.putInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS, + retryStrategy.getInitialBackoff()); + data.putInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS, + retryStrategy.getMaximumBackoff()); + } + + @NonNull + private String convertObservedUrisToJsonString(@NonNull List uris) { + JSONObject contentUris = new JSONObject(); + JSONArray jsonFlags = new JSONArray(); + JSONArray jsonUris = new JSONArray(); + for (ObservedUri uri : uris) { + jsonFlags.put(uri.getFlags()); + jsonUris.put(uri.getUri()); + } + try { + contentUris.put(JSON_URI_FLAGS, jsonFlags); + contentUris.put(JSON_URIS, jsonUris); + } catch (JSONException e) { + throw new RuntimeException(e); + } + return contentUris.toString(); + } + + @NonNull + private List convertJsonToObservedUris(@NonNull String contentUrisJson) { + List uris = new ArrayList<>(); + try { + JSONObject json = new JSONObject(contentUrisJson); + JSONArray jsonFlags = json.getJSONArray(JSON_URI_FLAGS); + JSONArray jsonUris = json.getJSONArray(JSON_URIS); + int length = jsonFlags.length(); + + for (int i = 0; i < length; i++) { + int flags = jsonFlags.getInt(i); + String uri = jsonUris.getString(i); + uris.add(new ObservedUri(Uri.parse(uri), flags)); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return uris; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobInvocation.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobInvocation.java new file mode 100644 index 000000000..08263e845 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobInvocation.java @@ -0,0 +1,236 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.Constraint.JobConstraint; + +/** + * An internal non-Job implementation of JobParameters. Passed to JobService invocations. + */ +/* package */ final class JobInvocation implements JobParameters { + + @NonNull + private final String mTag; + + @NonNull + private final String mService; + + @NonNull + private final JobTrigger mTrigger; + + private final boolean mRecurring; + + private final int mLifetime; + + @NonNull + @JobConstraint + private final int[] mConstraints; + + @NonNull + private final Bundle mExtras; + + private final RetryStrategy mRetryStrategy; + + private final boolean mReplaceCurrent; + + private final TriggerReason mTriggerReason; + + private JobInvocation(Builder builder) { + mTag = builder.mTag; + mService = builder.mService; + mTrigger = builder.mTrigger; + mRetryStrategy = builder.mRetryStrategy; + mRecurring = builder.mRecurring; + mLifetime = builder.mLifetime; + mConstraints = builder.mConstraints; + mExtras = builder.mExtras; + mReplaceCurrent = builder.mReplaceCurrent; + mTriggerReason = builder.mTriggerReason; + } + + @NonNull + @Override + public String getService() { + return mService; + } + + @NonNull + @Override + public String getTag() { + return mTag; + } + + @NonNull + @Override + public JobTrigger getTrigger() { + return mTrigger; + } + + @Override + public int getLifetime() { + return mLifetime; + } + + @Override + public boolean isRecurring() { + return mRecurring; + } + + @NonNull + @Override + public int[] getConstraints() { + return mConstraints; + } + + @NonNull + @Override + public Bundle getExtras() { + return mExtras; + } + + @NonNull + @Override + public RetryStrategy getRetryStrategy() { + return mRetryStrategy; + } + + @Override + public boolean shouldReplaceCurrent() { + return mReplaceCurrent; + } + + @Override + public TriggerReason getTriggerReason() { + return mTriggerReason; + } + + static final class Builder { + + @NonNull + private String mTag; + + @NonNull + private String mService; + + @NonNull + private JobTrigger mTrigger; + + private boolean mRecurring; + + private int mLifetime; + + @NonNull + @JobConstraint + private int[] mConstraints; + + @NonNull + private final Bundle mExtras = new Bundle(); + + private RetryStrategy mRetryStrategy; + + private boolean mReplaceCurrent; + + private TriggerReason mTriggerReason; + + JobInvocation build() { + if (mTag == null || mService == null || mTrigger == null) { + throw new IllegalArgumentException("Required fields were not populated."); + } + return new JobInvocation(this); + } + + public Builder setTag(@NonNull String mTag) { + this.mTag = mTag; + return this; + } + + public Builder setService(@NonNull String mService) { + this.mService = mService; + return this; + } + + public Builder setTrigger(@NonNull JobTrigger mTrigger) { + this.mTrigger = mTrigger; + return this; + } + + public Builder setRecurring(boolean mRecurring) { + this.mRecurring = mRecurring; + return this; + } + + public Builder setLifetime(@Lifetime.LifetimeConstant int mLifetime) { + this.mLifetime = mLifetime; + return this; + } + + public Builder setConstraints(@JobConstraint @NonNull int[] mConstraints) { + this.mConstraints = mConstraints; + return this; + } + + public Builder addExtras(@NonNull Bundle bundle) { + if (bundle != null) { + mExtras.putAll(bundle); + } + return this; + } + + public Builder setRetryStrategy(RetryStrategy mRetryStrategy) { + this.mRetryStrategy = mRetryStrategy; + return this; + } + + public Builder setReplaceCurrent(boolean mReplaceCurrent) { + this.mReplaceCurrent = mReplaceCurrent; + return this; + } + + public Builder setTriggerReason(TriggerReason triggerReason) { + this.mTriggerReason = triggerReason; + return this; + } + } + + /** + * @return true if the tag and the service of provided {@link JobInvocation} have the same + * values. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(o.getClass())) { + return false; + } + + JobInvocation jobInvocation = (JobInvocation) o; + + return mTag.equals(jobInvocation.mTag) + && mService.equals(jobInvocation.mService); + } + + @Override + public int hashCode() { + int result = mTag.hashCode(); + result = 31 * result + mService.hashCode(); + return result; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobParameters.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobParameters.java new file mode 100644 index 000000000..24f0844e1 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobParameters.java @@ -0,0 +1,86 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.firebase.jobdispatcher.Constraint.JobConstraint; + +/** + * JobParameters represents anything that can describe itself in terms of Job components. + */ +public interface JobParameters { + + /** + * Returns the name of the backing JobService class. + */ + @NonNull + String getService(); + + /** + * Returns a string identifier for the Job. Used when cancelling Jobs and displaying debug + * messages. + */ + @NonNull + String getTag(); + + /** + * The Job's Trigger, which decides when the Job is ready to run. + */ + @NonNull + JobTrigger getTrigger(); + + /** + * The Job's lifetime; how long it should persist for. + */ + @Lifetime.LifetimeConstant + int getLifetime(); + + /** + * Whether the Job should repeat. + */ + boolean isRecurring(); + + /** + * The runtime constraints applied to this Job. A Job is not run until the trigger is activated + * and all the runtime constraints are satisfied. + */ + @JobConstraint + int[] getConstraints(); + + /** + * The optional set of user-supplied extras associated with this Job. + */ + @Nullable + Bundle getExtras(); + + /** + * The RetryStrategy for the Job. Used to determine how to handle failures. + */ + @NonNull + RetryStrategy getRetryStrategy(); + + /** + * Whether the Job should replace a pre-existing Job with the same tag. + */ + boolean shouldReplaceCurrent(); + + /** @return A {@link TriggerReason} that - if non null - describes why the job was triggered. */ + @Nullable + TriggerReason getTriggerReason(); +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java new file mode 100644 index 000000000..0c85d5994 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java @@ -0,0 +1,257 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.app.Service; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Binder; +import android.os.IBinder; +import android.os.Message; +import android.support.annotation.IntDef; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.util.SimpleArrayMap; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Locale; + +/** + * JobService is the fundamental unit of work used in the JobDispatcher. + *

+ * Users will need to override {@link #onStartJob(JobParameters)}, which is where any asynchronous + * execution should start. This method, like most lifecycle methods, runs on the main thread; you + * must offload execution to another thread (or {@link android.os.AsyncTask}, or + * {@link android.os.Handler}, or your favorite flavor of concurrency). + *

+ * Once any asynchronous work is complete {@link #jobFinished(JobParameters, boolean)} should be + * called to inform the backing driver of the result. + *

+ * Implementations should also override {@link #onStopJob(JobParameters)}, which will be called if + * the scheduling engine wishes to interrupt your work (most likely because the runtime constraints + * that are associated with the job in question are no longer met). + */ +public abstract class JobService extends Service { + /** + * Returned to indicate the job was executed successfully. If the job is not recurring (i.e. a + * one-off) it will be dequeued and forgotten. If it is recurring the trigger will be reset and + * the job will be requeued. + */ + public static final int RESULT_SUCCESS = 0; + + /** + * Returned to indicate the job encountered an error during execution and should be retried after + * a backoff period. + */ + public static final int RESULT_FAIL_RETRY = 1; + + /** + * Returned to indicate the job encountered an error during execution but should not be retried. + * If the job is not recurring (i.e. a one-off) it will be dequeued and forgotten. If it is + * recurring the trigger will be reset and the job will be requeued. + */ + public static final int RESULT_FAIL_NORETRY = 2; + + static final String TAG = "FJD.JobService"; + + @VisibleForTesting + static final String ACTION_EXECUTE = "com.firebase.jobdispatcher.ACTION_EXECUTE"; + + /** + * Correlates job tags (unique strings) with Messages, which are used to signal the completion + * of a job. + */ + private final SimpleArrayMap runningJobs = new SimpleArrayMap<>(1); + private LocalBinder binder = new LocalBinder(); + + /** + * The entry point to your Job. Implementations should offload work to another thread of + * execution as soon as possible because this runs on the main thread. If work was offloaded, + * call {@link JobService#jobFinished(JobParameters, boolean)} to notify the scheduling service + * that the work is completed. + *

+ * In order to reschedule use {@link JobService#jobFinished(JobParameters, boolean)}. + * + * @return {@code true} if there is more work remaining in the worker thread, {@code false} if the job was completed. + */ + @MainThread + public abstract boolean onStartJob(JobParameters job); + + /** + * Called when the scheduling engine has decided to interrupt the execution of a running job, + * most likely because the runtime constraints associated with the job are no longer satisfied. + * The job must stop execution. + * + * @return true if the job should be retried + * @see com.firebase.jobdispatcher.JobInvocation.Builder#setRetryStrategy(RetryStrategy) + * @see RetryStrategy + */ + @MainThread + public abstract boolean onStopJob(JobParameters job); + + @MainThread + void start(JobParameters job, Message msg) { + synchronized (runningJobs) { + if (runningJobs.containsKey(job.getTag())) { + Log.w(TAG, String + .format(Locale.US, "Job with tag = %s was already running.", job.getTag())); + return; + } + runningJobs.put(job.getTag(), new JobCallback(msg)); + + boolean moreWork = onStartJob(job); + if (!moreWork) { + JobCallback callback = runningJobs.remove(job.getTag()); + if (callback != null) { + callback.sendResult(RESULT_SUCCESS); + } + } + } + } + + @MainThread + void stop(JobInvocation job) { + synchronized (runningJobs) { + JobCallback jobCallback = runningJobs.remove(job.getTag()); + + if (jobCallback == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Provided job has already been executed."); + } + return; + } + boolean shouldRetry = onStopJob(job); + jobCallback.sendResult(shouldRetry ? RESULT_FAIL_RETRY : RESULT_SUCCESS); + } + } + + /** + * Callback to inform the scheduling driver that you've finished executing. Can be called from + * any thread. When the system receives this message, it will release the wakelock being held. + * + * @param job + * @param needsReschedule + * whether the job should be rescheduled + * @see com.firebase.jobdispatcher.JobInvocation.Builder#setRetryStrategy(RetryStrategy) + */ + public final void jobFinished(@NonNull JobParameters job, boolean needsReschedule) { + if (job == null) { + Log.e(TAG, "jobFinished called with a null JobParameters"); + return; + } + + synchronized (runningJobs) { + JobCallback jobCallback = runningJobs.remove(job.getTag()); + + if (jobCallback != null) { + jobCallback.sendResult(needsReschedule ? RESULT_FAIL_RETRY : RESULT_SUCCESS); + } + } + } + + @Override + public final int onStartCommand(Intent intent, int flags, int startId) { + stopSelf(startId); + + return START_NOT_STICKY; + } + + @Nullable + @Override + public final IBinder onBind(Intent intent) { + return binder; + } + + @Override + public final boolean onUnbind(Intent intent) { + synchronized (runningJobs) { + for (int i = runningJobs.size() - 1; i >= 0; i--) { + JobCallback callback = runningJobs.get(runningJobs.keyAt(i)); + if (callback != null && callback.message != null) { + if(callback.message.obj instanceof JobParameters) { + callback.sendResult(onStopJob((JobParameters) callback.message.obj) + // returned true, would like to be rescheduled + ? RESULT_FAIL_RETRY + // returned false, but was interrupted so consider it a fail + : RESULT_FAIL_NORETRY); + } + } + } + } + + return super.onUnbind(intent); + } + + @Override + public final void onRebind(Intent intent) { + super.onRebind(intent); + } + + @Override + public final void onStart(Intent intent, int startId) { + } + + @Override + protected final void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(fd, writer, args); + } + + @Override + public final void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + @Override + public final void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + } + + /** + * The result returned from a job execution. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RESULT_SUCCESS, RESULT_FAIL_RETRY, RESULT_FAIL_NORETRY}) + public @interface JobResult { + } + + private final static class JobCallback { + public final Message message; + + private JobCallback(Message message) { + this.message = message; + } + + void sendResult(@JobResult int result) { + if (message != null) { + message.arg1 = result; + message.sendToTarget(); + } + } + } + + class LocalBinder extends Binder { + JobService getService() { + return JobService.this; + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobServiceConnection.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobServiceConnection.java new file mode 100644 index 000000000..c96e9f687 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobServiceConnection.java @@ -0,0 +1,82 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.ExecutionDelegator.TAG; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.Message; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +/** + * ServiceConnection for job execution. + */ +@VisibleForTesting +class JobServiceConnection implements ServiceConnection { + + private final JobInvocation jobInvocation; + // Should be sent only once. Can't be reused. + private final Message jobFinishedMessage; + private boolean wasMessageUsed = false; + + //Guarded by "this". Can be updated from main and binder threads. + private JobService.LocalBinder binder; + + JobServiceConnection(JobInvocation jobInvocation, Message jobFinishedMessage) { + this.jobFinishedMessage = jobFinishedMessage; + this.jobInvocation = jobInvocation; + this.jobFinishedMessage.obj = this.jobInvocation; + } + + @Override + public synchronized void onServiceConnected(ComponentName name, IBinder service) { + if (!(service instanceof JobService.LocalBinder)) { + Log.w(TAG, "Unknown service connected"); + return; + } + if (wasMessageUsed) { + Log.w(TAG, "onServiceConnected Duplicate calls. Ignored."); + return; + } else { + wasMessageUsed = true; + } + + binder = (JobService.LocalBinder) service; + + JobService jobService = binder.getService(); + + jobService.start(jobInvocation, jobFinishedMessage); + } + + @Override + public synchronized void onServiceDisconnected(ComponentName name) { + binder = null; + } + + synchronized boolean isBound() { + return binder != null; + } + + synchronized void onStop() { + if (isBound()) { + binder.getService().stop(jobInvocation); + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobTrigger.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobTrigger.java new file mode 100644 index 000000000..b1c510c7f --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobTrigger.java @@ -0,0 +1,70 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import java.util.List; + +/** + * Contains all supported triggers. + */ +public class JobTrigger { + + /** + * ImmediateTrigger is a Trigger that's immediately available. The Job will be run as soon as + * the runtime constraints are satisfied. + */ + public static final class ImmediateTrigger extends JobTrigger { + /* package */ ImmediateTrigger() {} + } + + /** + * ExecutionWindow represents a Job trigger that becomes eligible once + * the current elapsed time exceeds the scheduled time + the {@code windowStart} + * value. The scheduler backend is encouraged to use the windowEnd value as a + * signal that the job should be run, but this is not an enforced behavior. + */ + public static final class ExecutionWindowTrigger extends JobTrigger { + private final int mWindowStart; + private final int mWindowEnd; + + /* package */ ExecutionWindowTrigger(int windowStart, int windowEnd) { + this.mWindowStart = windowStart; + this.mWindowEnd = windowEnd; + } + + public int getWindowStart() { + return mWindowStart; + } + + public int getWindowEnd() { + return mWindowEnd; + } + } + + /** A trigger that will be triggered on content update for any of provided uris. */ + public static final class ContentUriTrigger extends JobTrigger { + private final List uris; + + /* package */ ContentUriTrigger(List uris) { + this.uris = uris; + } + + public List getUris() { + return uris; + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobValidator.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobValidator.java new file mode 100644 index 000000000..c97198f49 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobValidator.java @@ -0,0 +1,49 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.Nullable; +import java.util.List; + +/** + * A JobValidator is an object that knows how to validate Jobs and some of their composite + * components. + */ +public interface JobValidator { + /** + * Returns a List of error messages, or null if the JobParameters is + * valid. + */ + @Nullable + List validate(JobParameters job); + + /** + * Returns a List of error messages, or null if the Trigger is + * valid. + * @param trigger + */ + @Nullable + List validate(JobTrigger trigger); + + /** + * Returns a List of error messages, or null if the RetryStrategy + * is valid. + */ + @Nullable + List validate(RetryStrategy retryStrategy); + +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Lifetime.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Lifetime.java new file mode 100644 index 000000000..456bc221c --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Lifetime.java @@ -0,0 +1,40 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Lifetime represents how long a Job should last. + */ +public final class Lifetime { + /** + * The Job should be preserved until the next boot. This is the default. + */ + public final static int UNTIL_NEXT_BOOT = 1; + + /** + * The Job should be preserved "forever." + */ + public final static int FOREVER = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({FOREVER, UNTIL_NEXT_BOOT}) + @interface LifetimeConstant {} +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ObservedUri.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ObservedUri.java new file mode 100644 index 000000000..b22c15e01 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ObservedUri.java @@ -0,0 +1,83 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.net.Uri; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Represents a single observed URI and any associated flags. */ +public final class ObservedUri { + + private final Uri uri; + + private final int flags; + + /** Flag enforcement. */ + @IntDef(flag = true, value = Flags.FLAG_NOTIFY_FOR_DESCENDANTS) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags { + + /** + * Triggers if any descendants of the given URI change. Corresponds to the {@code + * notifyForDescendants} of {@link android.content.ContentResolver#registerContentObserver}. + */ + int FLAG_NOTIFY_FOR_DESCENDANTS = 1 << 0; + } + + /** + * Create a new ObservedUri. + * + * @param uri The URI to observe. + * @param flags Any {@link Flags} associated with the URI. + */ + public ObservedUri(@NonNull Uri uri, @Flags int flags) { + if (uri == null) { + throw new IllegalArgumentException("URI must not be null."); + } + this.uri = uri; + this.flags = flags; + } + + public Uri getUri() { + return uri; + } + + public int getFlags() { + return flags; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ObservedUri)) { + return false; + } + + ObservedUri otherUri = (ObservedUri) o; + return flags == otherUri.flags && uri.equals(otherUri.uri); + } + + @Override + public int hashCode() { + return uri.hashCode() ^ flags; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/RetryStrategy.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/RetryStrategy.java new file mode 100644 index 000000000..7d9b31901 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/RetryStrategy.java @@ -0,0 +1,106 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * RetryStrategy represents an approach to handling job execution failures. Jobs will have a + * time-based backoff enforced, based on the chosen policy (one of {@code RETRY_POLICY_EXPONENTIAL} + * or {@code RETRY_POLICY_LINEAR}. + */ +public final class RetryStrategy { + /** + * Increase the backoff time exponentially. + *

+ * Calculated using {@code initial_backoff * 2 ^ (num_failures - 1)}. + */ + public final static int RETRY_POLICY_EXPONENTIAL = 1; + + /** + * Increase the backoff time linearly. + *

+ * Calculated using {@code initial_backoff * num_failures}. + */ + public final static int RETRY_POLICY_LINEAR = 2; + + /** + * Expected schedule is: [30s, 60s, 120s, 240s, ..., 3600s] + */ + public final static RetryStrategy DEFAULT_EXPONENTIAL = + new RetryStrategy(RETRY_POLICY_EXPONENTIAL, 30, 3600); + + /** + * Expected schedule is: [30s, 60s, 90s, 120s, ..., 3600s] + */ + public final static RetryStrategy DEFAULT_LINEAR = + new RetryStrategy(RETRY_POLICY_LINEAR, 30, 3600); + + @RetryPolicy + private final int mPolicy; + private final int mInitialBackoff; + private final int mMaximumBackoff; + + /* package */ RetryStrategy(@RetryPolicy int policy, int initialBackoff, int maximumBackoff) { + mPolicy = policy; + mInitialBackoff = initialBackoff; + mMaximumBackoff = maximumBackoff; + } + + /** + * Returns the backoff policy in place. + */ + @RetryPolicy + public int getPolicy() { + return mPolicy; + } + + /** + * Returns the initial backoff (i.e. when # of failures == 1), in seconds. + */ + public int getInitialBackoff() { + return mInitialBackoff; + } + + /** + * Returns the maximum backoff duration in seconds. + */ + public int getMaximumBackoff() { + return mMaximumBackoff; + } + + @IntDef({RETRY_POLICY_LINEAR, RETRY_POLICY_EXPONENTIAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface RetryPolicy { + } + + /* package */ final static class Builder { + private final ValidationEnforcer mValidator; + + Builder(ValidationEnforcer validator) { + mValidator = validator; + } + + public RetryStrategy build(@RetryPolicy int policy, int initialBackoff, int maxBackoff) { + RetryStrategy rs = new RetryStrategy(policy, initialBackoff, maxBackoff); + mValidator.ensureValid(rs); + return rs; + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/SimpleJobService.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/SimpleJobService.java new file mode 100644 index 000000000..8cfbe5f4c --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/SimpleJobService.java @@ -0,0 +1,90 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.os.AsyncTask; +import android.support.annotation.CallSuper; +import android.support.v4.util.SimpleArrayMap; + +/** + * SimpleJobService provides a simple way of doing background work in a JobService. + * + * Users should override onRunJob and return one of the {@link JobResult} ints. + */ +public abstract class SimpleJobService extends JobService { + private final SimpleArrayMap runningJobs = + new SimpleArrayMap<>(); + + @CallSuper + @Override + public boolean onStartJob(JobParameters job) { + AsyncJobTask async = new AsyncJobTask(this, job); + + synchronized (runningJobs) { + runningJobs.put(job, async); + } + + async.execute(); + + return true; // more work to do + } + + @CallSuper + @Override + public boolean onStopJob(JobParameters job) { + synchronized (runningJobs) { + AsyncJobTask async = runningJobs.remove(job); + if (async != null) { + async.cancel(true); + return true; + } + } + + return false; + } + + private void onJobFinished(JobParameters jobParameters, boolean b) { + synchronized (runningJobs) { + runningJobs.remove(jobParameters); + } + + jobFinished(jobParameters, b); + } + + @JobResult + public abstract int onRunJob(JobParameters job); + + private static class AsyncJobTask extends AsyncTask { + private final SimpleJobService jobService; + private final JobParameters jobParameters; + + private AsyncJobTask(SimpleJobService jobService, JobParameters jobParameters) { + this.jobService = jobService; + this.jobParameters = jobParameters; + } + + @Override + protected Integer doInBackground(Void... params) { + return jobService.onRunJob(jobParameters); + } + + @Override + protected void onPostExecute(Integer integer) { + jobService.onJobFinished(jobParameters, integer == JobService.RESULT_FAIL_RETRY); + } + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Trigger.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Trigger.java new file mode 100644 index 000000000..3e01ae9c5 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Trigger.java @@ -0,0 +1,73 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.NonNull; +import java.util.List; + +/** + * Generally, a Trigger is an object that can answer the question, "is this job ready to run?" + *

+ * More specifically, a Trigger is an opaque, abstract class used to root the type hierarchy. + */ +public final class Trigger { + + /** + * Immediate is a Trigger that's immediately available. The Job will be run as soon as the + * runtime constraints are satisfied. + *

+ * It is invalid to schedule an Immediate with a recurring Job. + */ + public final static JobTrigger.ImmediateTrigger NOW = new JobTrigger.ImmediateTrigger(); + + /** + * Creates a new ExecutionWindow based on the provided time interval. + * + * @param windowStart The earliest time (in seconds) the job should be + * considered eligible to run. Calculated from when the + * job was scheduled (for new jobs) or last run (for + * recurring jobs). + * @param windowEnd The latest time (in seconds) the job should be run in + * an ideal world. Calculated in the same way as + * {@code windowStart}. + * @throws IllegalArgumentException if the provided parameters are too + * restrictive. + */ + public static JobTrigger.ExecutionWindowTrigger executionWindow(int windowStart, int windowEnd) { + if (windowStart < 0) { + throw new IllegalArgumentException("Window start can't be less than 0"); + } else if (windowEnd < windowStart) { + throw new IllegalArgumentException("Window end can't be less than window start"); + } + + return new JobTrigger.ExecutionWindowTrigger(windowStart, windowEnd); + } + + /** + * Creates a new ContentUriTrigger based on the provided list of {@link ObservedUri}. + * + * @param uris The list of URIs to observe. The trigger will be available if a piece of content, + * corresponding to any of provided URIs, is updated. + * @throws IllegalArgumentException if provided list of URIs is null or empty. + */ + public static JobTrigger.ContentUriTrigger contentUriTrigger(@NonNull List uris) { + if (uris == null || uris.isEmpty()) { + throw new IllegalArgumentException("Uris must not be null or empty."); + } + return new JobTrigger.ContentUriTrigger(uris); + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/TriggerReason.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/TriggerReason.java new file mode 100644 index 000000000..00fc053b8 --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/TriggerReason.java @@ -0,0 +1,33 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.net.Uri; +import java.util.List; + +/** The class contains a summary of the events which caused the job to be executed. */ +public class TriggerReason { + private final List mTriggeredContentUris; + + TriggerReason(List mTriggeredContentUris) { + this.mTriggeredContentUris = mTriggeredContentUris; + } + + public List getTriggeredContentUris() { + return mTriggeredContentUris; + } +} diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ValidationEnforcer.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ValidationEnforcer.java new file mode 100644 index 000000000..d9cfbe02f --- /dev/null +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ValidationEnforcer.java @@ -0,0 +1,132 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import java.util.List; + +/** + * Wraps a JobValidator and provides helpful validation utilities. + */ +public class ValidationEnforcer implements JobValidator { + private final JobValidator mValidator; + + public ValidationEnforcer(JobValidator validator) { + mValidator = validator; + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + public List validate(JobParameters job) { + return mValidator.validate(job); + } + + /** + * {@inheritDoc} + * @param trigger + */ + @Nullable + @Override + public List validate(JobTrigger trigger) { + return mValidator.validate(trigger); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + public List validate(RetryStrategy retryStrategy) { + return mValidator.validate(retryStrategy); + } + + /** + * Indicates whether the provided JobParameters is valid. + */ + public final boolean isValid(JobParameters job) { + return validate(job) == null; + } + + /** + * Indicates whether the provided JobTrigger is valid. + */ + public final boolean isValid(JobTrigger trigger) { + return validate(trigger) == null; + } + + /** + * Indicates whether the provided RetryStrategy is valid. + */ + public final boolean isValid(RetryStrategy retryStrategy) { + return validate(retryStrategy) == null; + } + + /** + * Throws a RuntimeException if the provided JobParameters is invalid. + * + * @throws ValidationException + */ + public final void ensureValid(JobParameters job) { + ensureNoErrors(validate(job)); + } + + /** + * Throws a RuntimeException if the provided JobTrigger is invalid. + * + * @throws ValidationException + */ + public final void ensureValid(JobTrigger trigger) { + ensureNoErrors(validate(trigger)); + } + + /** + * Throws a RuntimeException if the provided RetryStrategy is + * invalid. + * + * @throws ValidationException + */ + public final void ensureValid(RetryStrategy retryStrategy) { + ensureNoErrors(validate(retryStrategy)); + } + + private void ensureNoErrors(List errors) { + if (errors != null) { + throw new ValidationException("JobParameters is invalid", errors); + } + } + + /** + * An Exception thrown when a validation error is encountered. + */ + public final static class ValidationException extends RuntimeException { + private final List mErrors; + + public ValidationException(String msg, @NonNull List errors) { + super(msg + ": " + TextUtils.join("\n - ", errors)); + mErrors = errors; + } + + public List getErrors() { + return mErrors; + } + } +} diff --git a/jobdispatcher/src/test/java/android/net/http/AndroidHttpClient.java b/jobdispatcher/src/test/java/android/net/http/AndroidHttpClient.java new file mode 100644 index 000000000..0720a56f5 --- /dev/null +++ b/jobdispatcher/src/test/java/android/net/http/AndroidHttpClient.java @@ -0,0 +1,10 @@ +package android.net.http; + +/** + * Robolectric requires this class be available in the classpath, otherwise {@link + * org.robolectric.Shadows#shadowOf(android.os.Looper)} fails. We don't use AndroidHttpClient, so + * include a stub to make Robolectric happy. + * + * @see https://github.com/robolectric/robolectric/issues/1862 + */ +public class AndroidHttpClient {} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ConstraintTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ConstraintTest.java new file mode 100644 index 000000000..808d7a3c6 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ConstraintTest.java @@ -0,0 +1,66 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; + +import android.text.TextUtils; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class ConstraintTest { + + /** + * Just to get 100% coverage. + */ + @Test + public void testPrivateConstructor() throws Exception { + TestUtil.assertHasSinglePrivateConstructor(Constraint.class); + } + + @Test + public void testCompactAndUnCompact() { + for (List combo : TestUtil.getAllConstraintCombinations()) { + int[] input = TestUtil.toIntArray(combo); + Arrays.sort(input); + + int[] output = Constraint.uncompact(Constraint.compact(input)); + Arrays.sort(output); + + for (int i = 0; i < combo.size(); i++) { + assertEquals("Combination = " + TextUtils.join(", ", combo), + input[i], + output[i]); + } + + assertEquals("Expected length of two arrays to be the same", + input.length, + output.length); + } + } + + @Test + public void compactNull() { + assertEquals(0, Constraint.compact(null)); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ContentUriTriggerTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ContentUriTriggerTest.java new file mode 100644 index 000000000..5ea200af0 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ContentUriTriggerTest.java @@ -0,0 +1,52 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static junit.framework.Assert.assertEquals; + +import android.provider.ContactsContract; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Test for {@link ContentUriTrigger}. */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21) +public class ContentUriTriggerTest { + + @Test(expected = IllegalArgumentException.class) + public void constrains_null() throws Exception { + Trigger.contentUriTrigger(null); + } + + @Test(expected = IllegalArgumentException.class) + public void constrains_emptyList() throws Exception { + Trigger.contentUriTrigger(Collections.emptyList()); + } + + @Test + public void constrains_valid() throws Exception { + List uris = Arrays.asList(new ObservedUri(ContactsContract.AUTHORITY_URI, 0)); + ContentUriTrigger uriTrigger = Trigger.contentUriTrigger(uris); + assertEquals(uris, uriTrigger.getUris()); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/DefaultJobValidatorTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/DefaultJobValidatorTest.java new file mode 100644 index 000000000..2d5264175 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/DefaultJobValidatorTest.java @@ -0,0 +1,119 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.provider.ContactsContract; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; +import com.firebase.jobdispatcher.ObservedUri.Flags; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class DefaultJobValidatorTest { + + @Mock + private Context mMockContext; + + private DefaultJobValidator mValidator; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mValidator = new DefaultJobValidator(mMockContext); + } + + @SuppressWarnings("WrongConstant") + @Test + public void testValidate_retryStrategy() throws Exception { + Map> testCases = new HashMap<>(); + testCases.put( + new RetryStrategy(0 /* bad policy */, 30, 3600), + singletonList("Unknown retry policy provided")); + testCases.put( + new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 15, 3600), + singletonList("Initial backoff must be at least 30s")); + testCases.put( + new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 15, 3600), + singletonList("Initial backoff must be at least 30s")); + testCases.put( + new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 30, 60), + singletonList("Maximum backoff must be greater than 300s (5 minutes)")); + testCases.put( + new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 30, 60), + singletonList("Maximum backoff must be greater than 300s (5 minutes)")); + testCases.put( + new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 301, 300), + singletonList("Maximum backoff must be greater than or equal to initial backoff")); + testCases.put( + new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 301, 300), + singletonList("Maximum backoff must be greater than or equal to initial backoff")); + + for (Entry> testCase : testCases.entrySet()) { + List validationErrors = mValidator.validate(testCase.getKey()); + assertNotNull("Expected validation errors, but got null", validationErrors); + + for (String expected : testCase.getValue()) { + assertTrue( + "Expected validation errors to contain \"" + expected + "\"", + validationErrors.contains(expected)); + } + } + } + + @Test + public void testValidate_trigger() throws Exception { + Map testCases = new HashMap<>(); + + testCases.put(Trigger.NOW, null); + testCases.put(Trigger.executionWindow(0, 100), null); + ContentUriTrigger contentUriTrigger = + Trigger.contentUriTrigger( + Arrays.asList( + new ObservedUri( + ContactsContract.AUTHORITY_URI, Flags.FLAG_NOTIFY_FOR_DESCENDANTS))); + testCases.put(contentUriTrigger, null); + + for (Entry testCase : testCases.entrySet()) { + List validationErrors = mValidator.validate(testCase.getKey()); + if (testCase.getValue() == null) { + assertNull("Expected no validation errors for trigger", validationErrors); + } else { + assertTrue( + "Expected validation errors to contain \"" + testCase.getValue() + "\"", + validationErrors.contains(testCase.getValue())); + } + } + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionDelegatorTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionDelegatorTest.java new file mode 100644 index 000000000..a523c4654 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionDelegatorTest.java @@ -0,0 +1,224 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.TestUtil.getContentUriTrigger; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.JobService.JobResult; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@SuppressWarnings("WrongConstant") +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21) +public class ExecutionDelegatorTest { + + private Context mMockContext; + private TestJobReceiver mReceiver; + private ExecutionDelegator mExecutionDelegator; + + @Before + public void setUp() { + mMockContext = spy(RuntimeEnvironment.application); + doReturn("com.example.foo").when(mMockContext).getPackageName(); + + mReceiver = new TestJobReceiver(); + mExecutionDelegator = new ExecutionDelegator(mMockContext, mReceiver); + } + + @Test + public void testExecuteJob_sendsBroadcastWithJobAndMessage() throws Exception { + for (JobInvocation input : TestUtil.getJobInvocationCombinations()) { + verifyExecuteJob(input); + } + } + + private void verifyExecuteJob(JobInvocation input) throws Exception { + reset(mMockContext); + mReceiver.lastResult = -1; + + mReceiver.setLatch(new CountDownLatch(1)); + + mExecutionDelegator.executeJob(input); + + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + final ArgumentCaptor connCaptor = + ArgumentCaptor.forClass(ServiceConnection.class); + verify(mMockContext).bindService(intentCaptor.capture(), connCaptor.capture(), anyInt()); + + final Intent result = intentCaptor.getValue(); + // verify the intent was sent to the right place + assertEquals(input.getService(), result.getComponent().getClassName()); + assertEquals(JobService.ACTION_EXECUTE, result.getAction()); + + final ServiceConnection connection = connCaptor.getValue(); + + ComponentName cname = mock(ComponentName.class); + JobService.LocalBinder mockLocalBinder = mock(JobService.LocalBinder.class); + final JobParameters[] out = new JobParameters[1]; + + JobService mockJobService = new JobService() { + @Override + public boolean onStartJob(JobParameters job) { + out[0] = job; + return false; + } + + @Override + public boolean onStopJob(JobParameters job) { + return false; + } + }; + + when(mockLocalBinder.getService()).thenReturn(mockJobService); + + connection.onServiceConnected(cname, mockLocalBinder); + + TestUtil.assertJobsEqual(input, out[0]); + + // make sure the countdownlatch was decremented + assertTrue(mReceiver.mLatch.await(1, TimeUnit.SECONDS)); + + // verify the lastResult was set correctly + assertEquals(JobService.RESULT_SUCCESS, mReceiver.lastResult); + } + + @Test + public void testExecuteJob_handlesNull() { + assertFalse("Expected calling triggerExecution on null to fail and return false", + mExecutionDelegator.executeJob(null)); + } + + @Test + public void testHandleMessage_doesntCrashOnBadJobData() { + JobInvocation j = new JobInvocation.Builder() + .setService(TestJobService.class.getName()) + .setTag("tag") + .setTrigger(Trigger.NOW) + .build(); + + mExecutionDelegator.executeJob(j); + + ArgumentCaptor intentCaptor = + ArgumentCaptor.forClass(Intent.class); + ArgumentCaptor connCaptor = + ArgumentCaptor.forClass(ServiceConnection.class); + + //noinspection WrongConstant + verify(mMockContext).bindService(intentCaptor.capture(), connCaptor.capture(), anyInt()); + + Intent executeReq = intentCaptor.getValue(); + assertEquals(JobService.ACTION_EXECUTE, executeReq.getAction()); + } + + @Test + public void onStop_mock() throws InterruptedException { + JobInvocation job = new JobInvocation.Builder() + .setTag("TAG") + .setTrigger(getContentUriTrigger()) + .setService(TestJobService.class.getName()) + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .build(); + + reset(mMockContext); + mReceiver.lastResult = -1; + + mExecutionDelegator.executeJob(job); + + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + final ArgumentCaptor connCaptor = + ArgumentCaptor.forClass(ServiceConnection.class); + verify(mMockContext).bindService(intentCaptor.capture(), connCaptor.capture(), anyInt()); + + final Intent result = intentCaptor.getValue(); + // verify the intent was sent to the right place + assertEquals(job.getService(), result.getComponent().getClassName()); + assertEquals(JobService.ACTION_EXECUTE, result.getAction()); + + final JobParameters[] out = new JobParameters[2]; + + JobService mockJobService = new JobService() { + @Override + public boolean onStartJob(JobParameters job) { + out[0] = job; + return true; + } + + @Override + public boolean onStopJob(JobParameters job) { + out[1] = job; + return false; + } + }; + + JobService.LocalBinder mockLocalBinder = mock(JobService.LocalBinder.class); + when(mockLocalBinder.getService()).thenReturn(mockJobService); + + ComponentName componentName = mock(ComponentName.class); + final ServiceConnection connection = connCaptor.getValue(); + connection.onServiceConnected(componentName, mockLocalBinder); + + mExecutionDelegator.stopJob(job); + + TestUtil.assertJobsEqual(job, out[0]); + TestUtil.assertJobsEqual(job, out[1]); + } + + private final static class TestJobReceiver implements ExecutionDelegator.JobFinishedCallback { + int lastResult; + + private CountDownLatch mLatch; + + @Override + public void onJobFinished(@NonNull JobInvocation js, @JobResult int result) { + lastResult = result; + + if (mLatch != null) { + mLatch.countDown(); + } + } + + /** + * Convenience method for tests. + */ + public void setLatch(CountDownLatch latch) { + mLatch = latch; + } + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionWindowTriggerTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionWindowTriggerTest.java new file mode 100644 index 000000000..6dfe85774 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExecutionWindowTriggerTest.java @@ -0,0 +1,76 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class ExecutionWindowTriggerTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testNewInstance_withValidWindow() throws Exception { + JobTrigger.ExecutionWindowTrigger trigger = Trigger.executionWindow(0, 60); + + assertEquals(0, trigger.getWindowStart()); + assertEquals(60, trigger.getWindowEnd()); + } + + @Test + public void testNewInstance_withNegativeStart() throws Exception { + expectedException.expect(IllegalArgumentException.class); + + Trigger.executionWindow(-10, 60); + } + + @Test + public void testNewInstance_withNegativeEnd() throws Exception { + expectedException.expect(IllegalArgumentException.class); + + Trigger.executionWindow(0, -1); + } + + @Test + public void testNewInstance_withReversedValues() throws Exception { + expectedException.expect(IllegalArgumentException.class); + + Trigger.executionWindow(60, 0); + } + + @Test + public void testNewInstance_withTooSmallWindow_now() throws Exception { + expectedException.expect(IllegalArgumentException.class); + + Trigger.executionWindow(60, 59); + } + + @Test + public void testNewInstance_withTooSmallWindow_inFuture() throws Exception { + expectedException.expect(IllegalArgumentException.class); + + Trigger.executionWindow(200, 100); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExtendedShadowParcel.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExtendedShadowParcel.java new file mode 100644 index 000000000..e5ecf1042 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ExtendedShadowParcel.java @@ -0,0 +1,40 @@ +package com.firebase.jobdispatcher; + +import android.os.IBinder; +import android.os.Parcel; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.shadows.ShadowParcel; + +/** + * ShadowParcel doesn't correctly handle {@link Parcel#writeStrongBinder(IBinder)} or {@link + * Parcel#readStrongBinder()}, so we shim a simple implementation that uses an in-memory map to read + * and write Binder objects. + */ +@Implements(Parcel.class) +public class ExtendedShadowParcel extends ShadowParcel { + @RealObject private Parcel realObject; + + // Map each IBinder to an integer, and use the super's int-writing capability to fake Binder + // read/writes. + private final AtomicInteger nextBinderId = new AtomicInteger(1); + private final Map binderMap = + Collections.synchronizedMap(new HashMap()); + + @Implementation + public void writeStrongBinder(IBinder binder) { + int id = nextBinderId.getAndIncrement(); + binderMap.put(id, binder); + realObject.writeInt(id); + } + + @Implementation + public IBinder readStrongBinder() { + return binderMap.get(realObject.readInt()); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/FirebaseJobDispatcherTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/FirebaseJobDispatcherTest.java new file mode 100644 index 000000000..dd0b22ee6 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/FirebaseJobDispatcherTest.java @@ -0,0 +1,205 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleFailedException; +import java.util.Arrays; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class FirebaseJobDispatcherTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Mock + private Driver mDriver; + + @Mock + private JobValidator mValidator; + + private FirebaseJobDispatcher mDispatcher; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mDriver.getValidator()).thenReturn(mValidator); + + mDispatcher = new FirebaseJobDispatcher(mDriver); + setDriverAvailability(true); + } + + @Test + public void testSchedule_passThrough() throws Exception { + final int[] possibleResults = { + FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS, + FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE, + FirebaseJobDispatcher.SCHEDULE_RESULT_BAD_SERVICE, + FirebaseJobDispatcher.SCHEDULE_RESULT_UNKNOWN_ERROR, + FirebaseJobDispatcher.SCHEDULE_RESULT_UNSUPPORTED_TRIGGER}; + + for (int result : possibleResults) { + when(mDriver.schedule(null)).thenReturn(result); + assertEquals(result, mDispatcher.schedule(null)); + } + + verify(mDriver, times(possibleResults.length)).schedule(null); + } + + @Test + public void testSchedule_unavailable() throws Exception { + setDriverAvailability(false); + assertEquals( + FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE, + mDispatcher.schedule(null)); + verify(mDriver, never()).schedule(null); + } + + @Test + public void testCancelJob() throws Exception { + final String tag = "foo"; + + // simulate success + when(mDriver.cancel(tag)) + .thenReturn(FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS); + + assertEquals( + "Expected dispatcher to pass the result of Driver#cancel(String, Class) through", + FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS, + mDispatcher.cancel(tag)); + + // verify the driver was indeed called + verify(mDriver).cancel(tag); + } + + @Test + public void testCancelJob_unavailable() throws Exception { + setDriverAvailability(false); // driver is unavailable + + assertEquals( + FirebaseJobDispatcher.CANCEL_RESULT_NO_DRIVER_AVAILABLE, + mDispatcher.cancel("foo")); + + // verify the driver was never even consulted + verify(mDriver, never()).cancel("foo"); + } + + @Test + public void testCancelAllJobs() throws Exception { + when(mDriver.cancelAll()).thenReturn(FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS); + + assertEquals(FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS, mDispatcher.cancelAll()); + verify(mDriver).cancelAll(); + } + + @Test + public void testCancelAllJobs_unavailable() throws Exception { + setDriverAvailability(false); // driver is unavailable + + assertEquals( + FirebaseJobDispatcher.CANCEL_RESULT_NO_DRIVER_AVAILABLE, + mDispatcher.cancelAll()); + + verify(mDriver, never()).cancelAll(); + } + + @Test + public void testMustSchedule_success() throws Exception { + when(mDriver.schedule(null)).thenReturn(FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS); + + /* assert no exception is thrown */ + mDispatcher.mustSchedule(null); + } + + @Test + public void testMustSchedule_unavailable() throws Exception { + setDriverAvailability(false); // driver is unavailable + expectedException.expect(FirebaseJobDispatcher.ScheduleFailedException.class); + + mDispatcher.mustSchedule(null); + } + + @Test + public void testMustSchedule_failure() throws Exception { + final int[] possibleErrors = { + FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE, + FirebaseJobDispatcher.SCHEDULE_RESULT_BAD_SERVICE, + FirebaseJobDispatcher.SCHEDULE_RESULT_UNKNOWN_ERROR, + FirebaseJobDispatcher.SCHEDULE_RESULT_UNSUPPORTED_TRIGGER}; + + for (int scheduleError : possibleErrors) { + when(mDriver.schedule(null)).thenReturn(scheduleError); + + try { + mDispatcher.mustSchedule(null); + + fail("Expected mustSchedule() with error code " + scheduleError + " to fail"); + } catch (ScheduleFailedException expected) { /* expected */ } + } + + verify(mDriver, times(possibleErrors.length)).schedule(null); + } + + @Test + public void testNewRetryStrategyBuilder() { + // custom validator that only approves strategies where initialbackoff == 30s + when(mValidator.validate(any(RetryStrategy.class))).thenAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) throws Throwable { + RetryStrategy rs = (RetryStrategy) invocation.getArguments()[0]; + // only succeed if initialBackoff == 30s + return rs.getInitialBackoff() == 30 ? null : Arrays.asList("foo", "bar"); + } + }); + + try { + mDispatcher.newRetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 0, 30); + fail("Expected initial backoff != 30s to fail using custom validator"); + } catch (Exception unused) { /* unused */ } + + try { + mDispatcher.newRetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 30, 30); + } catch (Exception unused) { + fail("Expected initial backoff == 30s not to fail using custom validator"); + } + } + + public void setDriverAvailability(boolean driverAvailability) { + when(mDriver.isAvailable()).thenReturn(driverAvailability); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractorTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractorTest.java new file mode 100644 index 000000000..f65918501 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayCallbackExtractorTest.java @@ -0,0 +1,153 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; +import com.firebase.jobdispatcher.TestUtil.InspectableBinder; +import com.firebase.jobdispatcher.TestUtil.TransactionArguments; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config( + constants = BuildConfig.class, + manifest = Config.NONE, + sdk = 23, + shadows = {ExtendedShadowParcel.class} +) +public final class GooglePlayCallbackExtractorTest { + @Mock + private IBinder mBinder; + + private GooglePlayCallbackExtractor mExtractor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mExtractor = new GooglePlayCallbackExtractor(); + } + + @Test + public void testExtractCallback_nullBundle() { + assertNull(mExtractor.extractCallback(null)); + } + + @Test + public void testExtractCallback_nullParcelable() { + Bundle emptyBundle = new Bundle(); + assertNull(extractCallback(emptyBundle)); + } + + @Test + public void testExtractCallback_badParcelable() { + Bundle misconfiguredBundle = new Bundle(); + misconfiguredBundle.putParcelable("callback", new BadParcelable(1)); + + assertNull(extractCallback(misconfiguredBundle)); + } + + @Test + public void testExtractCallback_goodParcelable() { + InspectableBinder binder = new InspectableBinder(); + Bundle validBundle = new Bundle(); + validBundle.putParcelable("callback", binder.toPendingCallback()); + + Pair extraction = extractCallback(validBundle); + assertNotNull(extraction); + assertEquals("should have stripped the 'callback' entry from the extracted bundle", + 0, extraction.second.keySet().size()); + extraction.first.jobFinished(JobService.RESULT_SUCCESS); + + // Check our homemade Binder is doing the right things: + TransactionArguments args = binder.getArguments().get(0); + // Should have set the transaction code: + assertEquals("transaction code", IBinder.FIRST_CALL_TRANSACTION + 1, args.code); + + // strong mode bit + args.data.readInt(); + // interface token + assertEquals("com.google.android.gms.gcm.INetworkTaskCallback", args.data.readString()); + // result + assertEquals("result", JobService.RESULT_SUCCESS, args.data.readInt()); + } + + @Test + public void testExtractCallback_extraMapValues() { + Bundle validBundle = new Bundle(); + validBundle.putString("foo", "bar"); + validBundle.putInt("bar", 3); + validBundle.putParcelable("parcelable", new Bundle()); + validBundle.putParcelable("callback", new InspectableBinder().toPendingCallback()); + + Pair extraction = extractCallback(validBundle); + assertNotNull(extraction); + assertEquals("should have stripped the 'callback' entry from the extracted bundle", + 3, extraction.second.keySet().size()); + } + + private Pair extractCallback(Bundle bundle) { + return mExtractor.extractCallback(bundle); + } + + private static final class BadParcelable implements Parcelable { + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public BadParcelable createFromParcel(Parcel in) { + return new BadParcelable(in); + } + + @Override + public BadParcelable[] newArray(int size) { + return new BadParcelable[size]; + } + }; + private final int mNum; + + public BadParcelable(int i) { + mNum = i; + } + + private BadParcelable(Parcel in) { + mNum = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dst, int flags) { + dst.writeInt(mNum); + } + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayDriverTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayDriverTest.java new file mode 100644 index 000000000..822a8f1d6 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayDriverTest.java @@ -0,0 +1,203 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class GooglePlayDriverTest { + @Mock + public Context mMockContext; + + private TestJobDriver mDriver; + private FirebaseJobDispatcher mDispatcher; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mDriver = new TestJobDriver(new GooglePlayDriver(mMockContext)); + mDispatcher = new FirebaseJobDispatcher(mDriver); + + when(mMockContext.getPackageName()).thenReturn("foo.bar.whatever"); + } + + @Test + public void testSchedule_failsWhenPlayServicesIsUnavailable() throws Exception { + markBackendUnavailable(); + mockPackageManagerInfo(); + + Job job = null; + try { + job = mDispatcher.newJobBuilder() + .setService(TestJobService.class) + .setTag("foobar") + .setConstraints(Constraint.DEVICE_CHARGING) + .setTrigger(Trigger.executionWindow(0, 60)) + .build(); + } catch (ValidationEnforcer.ValidationException ve) { + fail(TextUtils.join("\n", ve.getErrors())); + } + + assertEquals("Expected schedule() request to fail when backend is unavailable", + FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE, + mDispatcher.schedule(job)); + } + + @Test + public void testCancelJobs_backendUnavailable() throws Exception { + markBackendUnavailable(); + + assertEquals("Expected cancelAll() request to fail when backend is unavailable", + FirebaseJobDispatcher.CANCEL_RESULT_NO_DRIVER_AVAILABLE, + mDispatcher.cancelAll()); + } + + @Test + public void testSchedule_sendsAppropriateBroadcast() { + ArgumentCaptor pmQueryIntentCaptor = mockPackageManagerInfo(); + + Job job = mDispatcher.newJobBuilder() + .setConstraints(Constraint.DEVICE_CHARGING) + .setService(TestJobService.class) + .setTrigger(Trigger.executionWindow(0, 60)) + .setRecurring(false) + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .setTag("foobar") + .build(); + + Intent pmQueryIntent = pmQueryIntentCaptor.getValue(); + assertEquals(JobService.ACTION_EXECUTE, pmQueryIntent.getAction()); + assertEquals(TestJobService.class.getName(), pmQueryIntent.getComponent().getClassName()); + + assertEquals("Expected schedule() request to succeed", + FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS, + mDispatcher.schedule(job)); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockContext).sendBroadcast(captor.capture()); + + Intent broadcast = captor.getValue(); + + assertNotNull(broadcast); + assertEquals("com.google.android.gms.gcm.ACTION_SCHEDULE", broadcast.getAction()); + assertEquals("SCHEDULE_TASK", broadcast.getStringExtra("scheduler_action")); + assertEquals("com.google.android.gms", broadcast.getPackage()); + assertEquals(8, broadcast.getIntExtra("source", -1)); + assertEquals(1, broadcast.getIntExtra("source_version", -1)); + + final Parcelable parcelablePendingIntent = broadcast.getParcelableExtra("app"); + assertTrue("Expected 'app' value to be a PendingIntent", + parcelablePendingIntent instanceof PendingIntent); + } + + private ArgumentCaptor mockPackageManagerInfo() { + PackageManager packageManager = mock(PackageManager.class); + when(mMockContext.getPackageManager()).thenReturn(packageManager); + ArgumentCaptor intentArgCaptor = ArgumentCaptor.forClass(Intent.class); + + ResolveInfo info = new ResolveInfo(); + info.serviceInfo = new ServiceInfo(); + info.serviceInfo.enabled = true; + + //noinspection WrongConstant + when(packageManager.queryIntentServices(intentArgCaptor.capture(), eq(0))) + .thenReturn(Arrays.asList(info)); + + return intentArgCaptor; + } + + @Test + public void testCancel_sendsAppropriateBroadcast() { + mDispatcher.cancel("foobar"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockContext).sendBroadcast(captor.capture()); + + Intent broadcast = captor.getValue(); + + assertNotNull(broadcast); + assertEquals("foobar", broadcast.getStringExtra("tag")); + } + + private void markBackendUnavailable() { + mDriver.available = false; + } + + public final static class TestJobDriver implements Driver { + public boolean available = true; + + private final Driver wrappedDriver; + + public TestJobDriver(Driver wrappedDriver) { + this.wrappedDriver = wrappedDriver; + } + + @Override + public int schedule(@NonNull Job job) { + return this.wrappedDriver.schedule(job); + } + + @Override + public int cancel(@NonNull String tag) { + return this.wrappedDriver.cancel(tag); + } + + @Override + public int cancelAll() { + return this.wrappedDriver.cancelAll(); + } + + @NonNull + @Override + public JobValidator getValidator() { + return this.wrappedDriver.getValidator(); + } + + @Override + public boolean isAvailable() { + return available; + } + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayJobWriterTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayJobWriterTest.java new file mode 100644 index 000000000..c56ab0d08 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayJobWriterTest.java @@ -0,0 +1,268 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import com.firebase.jobdispatcher.Job.Builder; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; +import com.firebase.jobdispatcher.ObservedUri.Flags; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class GooglePlayJobWriterTest { + + private static final boolean[] ALL_BOOLEANS = {true, false}; + + private GooglePlayJobWriter mWriter; + + private static Builder initializeDefaultBuilder() { + return TestUtil.getBuilderWithNoopValidator() + .setConstraints(Constraint.DEVICE_CHARGING) + .setExtras(null) + .setLifetime(Lifetime.FOREVER) + .setRecurring(false) + .setReplaceCurrent(false) + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .setService(TestJobService.class) + .setTag("tag") + .setTrigger(Trigger.NOW); + } + + @Before + public void setUp() throws Exception { + mWriter = new GooglePlayJobWriter(); + } + + @Test + public void testWriteToBundle_tags() { + for (String tag : Arrays.asList("foo", "bar", "foobar", "this is a tag")) { + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder().setTag(tag).build(), + new Bundle()); + + assertEquals("tag", tag, b.getString("tag")); + } + } + + @Test + public void testWriteToBundle_updateCurrent() { + for (boolean replaceCurrent : ALL_BOOLEANS) { + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder().setReplaceCurrent(replaceCurrent).build(), + new Bundle()); + + assertEquals("update_current", replaceCurrent, b.getBoolean("update_current")); + } + } + + @Test + public void testWriteToBundle_persisted() { + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder().setLifetime(Lifetime.FOREVER).build(), + new Bundle()); + + assertTrue("persisted", b.getBoolean("persisted")); + + for (int lifetime : new int[]{Lifetime.UNTIL_NEXT_BOOT}) { + b = mWriter.writeToBundle( + initializeDefaultBuilder().setLifetime(lifetime).build(), + new Bundle()); + + assertFalse("persisted", b.getBoolean("persisted")); + } + } + + @Test + public void testWriteToBundle_service() { + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder().setService(TestJobService.class).build(), + new Bundle()); + + assertEquals("service", GooglePlayReceiver.class.getName(), b.getString("service")); + } + + @Test + public void testWriteToBundle_requiredNetwork() { + Map mapping = new HashMap<>(); + mapping.put(Constraint.ON_ANY_NETWORK, GooglePlayJobWriter.LEGACY_NETWORK_CONNECTED); + mapping.put(Constraint.ON_UNMETERED_NETWORK, GooglePlayJobWriter.LEGACY_NETWORK_UNMETERED); + mapping.put(0, GooglePlayJobWriter.LEGACY_NETWORK_ANY); + + for (Entry testCase : mapping.entrySet()) { + @SuppressWarnings("WrongConstant") + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder().setConstraints(testCase.getKey()).build(), + new Bundle()); + + assertEquals("requiredNetwork", (int) testCase.getValue(), b.getInt("requiredNetwork")); + } + } + + @Test + public void testWriteToBundle_unmeteredConstraintShouldTakePrecendence() { + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder() + .setConstraints(Constraint.ON_ANY_NETWORK, Constraint.ON_UNMETERED_NETWORK) + .build(), + new Bundle()); + + assertEquals("expected ON_UNMETERED_NETWORK to take precendence over ON_ANY_NETWORK", + GooglePlayJobWriter.LEGACY_NETWORK_UNMETERED, b.getInt("requiredNetwork")); + } + + @Test + public void testWriteToBundle_requiresCharging() { + assertTrue("requiresCharging", mWriter.writeToBundle( + initializeDefaultBuilder().setConstraints(Constraint.DEVICE_CHARGING).build(), + new Bundle()).getBoolean("requiresCharging")); + + for (Integer constraint : Arrays.asList( + Constraint.ON_ANY_NETWORK, + Constraint.ON_UNMETERED_NETWORK)) { + + assertFalse("requiresCharging", mWriter.writeToBundle( + initializeDefaultBuilder().setConstraints(constraint).build(), + new Bundle()).getBoolean("requiresCharging")); + } + } + + @Test + public void testWriteToBundle_requiresIdle() { + assertTrue("requiresIdle", mWriter.writeToBundle( + initializeDefaultBuilder().setConstraints(Constraint.DEVICE_IDLE).build(), + new Bundle()).getBoolean("requiresIdle")); + + for (Integer constraint : Arrays.asList( + Constraint.ON_ANY_NETWORK, + Constraint.ON_UNMETERED_NETWORK)) { + + assertFalse("requiresIdle", mWriter.writeToBundle( + initializeDefaultBuilder().setConstraints(constraint).build(), + new Bundle()).getBoolean("requiresIdle")); + } + } + + @Test + public void testWriteToBundle_retryPolicy() { + assertEquals("retry_policy", + GooglePlayJobWriter.LEGACY_RETRY_POLICY_EXPONENTIAL, + mWriter.writeToBundle( + initializeDefaultBuilder() + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .build(), + new Bundle()).getBundle("retryStrategy").getInt("retry_policy")); + + assertEquals("retry_policy", + GooglePlayJobWriter.LEGACY_RETRY_POLICY_LINEAR, + mWriter.writeToBundle( + initializeDefaultBuilder() + .setRetryStrategy(RetryStrategy.DEFAULT_LINEAR) + .build(), + new Bundle()).getBundle("retryStrategy").getInt("retry_policy")); + } + + @Test + public void testWriteToBundle_backoffSeconds() { + for (RetryStrategy retryStrategy : Arrays + .asList(RetryStrategy.DEFAULT_EXPONENTIAL, RetryStrategy.DEFAULT_LINEAR)) { + + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder().setRetryStrategy(retryStrategy).build(), + new Bundle()).getBundle("retryStrategy"); + + assertEquals("initial_backoff_seconds", + retryStrategy.getInitialBackoff(), + b.getInt("initial_backoff_seconds")); + + assertEquals("maximum_backoff_seconds", + retryStrategy.getMaximumBackoff(), + b.getInt("maximum_backoff_seconds")); + + } + } + + @Test + public void testWriteToBundle_triggers() { + // immediate + Bundle b = mWriter.writeToBundle( + initializeDefaultBuilder().setTrigger(Trigger.NOW).build(), + new Bundle()); + + assertEquals("window_start", 0, b.getLong("window_start")); + assertEquals("window_end", 30, b.getLong("window_end")); + + // execution window (oneoff) + JobTrigger.ExecutionWindowTrigger t = Trigger.executionWindow(631, 978); + b = mWriter.writeToBundle( + initializeDefaultBuilder().setTrigger(t).build(), + new Bundle()); + + assertEquals("window_start", t.getWindowStart(), b.getLong("window_start")); + assertEquals("window_end", t.getWindowEnd(), b.getLong("window_end")); + + // execution window (periodic) + b = mWriter.writeToBundle( + initializeDefaultBuilder().setRecurring(true).setTrigger(t).build(), + new Bundle()); + + assertEquals("period", t.getWindowEnd(), b.getLong("period")); + assertEquals("period_flex", t.getWindowEnd() - t.getWindowStart(), b.getLong("period_flex")); + } + + @Test + public void testWriteToBundle_contentUriTrigger() { + ObservedUri observedUri = new ObservedUri(ContactsContract.AUTHORITY_URI, + Flags.FLAG_NOTIFY_FOR_DESCENDANTS); + ContentUriTrigger contentUriTrigger = Trigger.contentUriTrigger(Arrays.asList(observedUri)); + Bundle bundle = mWriter.writeToBundle( + initializeDefaultBuilder().setTrigger(contentUriTrigger).build(), new Bundle()); + Uri[] uris = + (Uri[]) bundle.getParcelableArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_ARRAY); + int[] flags = bundle.getIntArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY); + assertTrue("Array size", uris.length == flags.length && flags.length == 1); + assertEquals(BundleProtocol.PACKED_PARAM_CONTENT_URI_ARRAY, + ContactsContract.AUTHORITY_URI, uris[0]); + assertEquals(BundleProtocol.PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY, + Flags.FLAG_NOTIFY_FOR_DESCENDANTS, flags[0]); + } + + @Test + public void testWriteToBundle_extras() { + Bundle extras = new Bundle(); + + Bundle result = mWriter.writeToBundle( + initializeDefaultBuilder().setExtras(extras).build(), + new Bundle()); + + assertEquals("extras", extras, result.getBundle("extras")); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessageHandlerTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessageHandlerTest.java new file mode 100644 index 000000000..31969a16e --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessageHandlerTest.java @@ -0,0 +1,153 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.content.Context; +import android.os.Bundle; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import com.firebase.jobdispatcher.JobInvocation.Builder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests {@link GooglePlayMessageHandler}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21) +public class GooglePlayMessageHandlerTest { + + @Mock + Looper looper; + @Mock + GooglePlayReceiver receiverMock; + @Mock + Context context; + @Mock + AppOpsManager appOpsManager; + @Mock + Messenger messengerMock; + @Mock + ExecutionDelegator executionDelegatorMock; + + GooglePlayMessageHandler handler; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + handler = new GooglePlayMessageHandler(looper, receiverMock); + when(receiverMock.getExecutionDelegator()).thenReturn(executionDelegatorMock); + when(receiverMock.getApplicationContext()).thenReturn(context); + when(context.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(appOpsManager); + } + + @Test + public void handleMessage_nullNoException() throws Exception { + handler.handleMessage(null); + } + + @Test + public void handleMessage_ignoreIfSenderIsNotGcm() throws Exception { + Message message = Message.obtain(); + message.what = GooglePlayMessageHandler.MSG_START_EXEC; + Bundle data = new Bundle(); + data.putString(REQUEST_PARAM_TAG, "TAG"); + message.setData(data); + message.replyTo = messengerMock; + doThrow(new SecurityException()).when(appOpsManager) + .checkPackage(message.sendingUid, GooglePlayDriver.BACKEND_PACKAGE); + handler.handleMessage(message); + verify(receiverMock, never()).prepareJob(any(GooglePlayMessengerCallback.class), eq(data)); + } + + @Test + public void handleMessage_startExecution_noData() throws Exception { + Message message = Message.obtain(); + message.what = GooglePlayMessageHandler.MSG_START_EXEC; + message.replyTo = messengerMock; + + handler.handleMessage(message); + verify(receiverMock, never()) + .prepareJob(any(GooglePlayMessengerCallback.class), any(Bundle.class)); + } + + @Test + public void handleMessage_startExecution() throws Exception { + Message message = Message.obtain(); + message.what = GooglePlayMessageHandler.MSG_START_EXEC; + Bundle data = new Bundle(); + data.putString(REQUEST_PARAM_TAG, "TAG"); + message.setData(data); + message.replyTo = messengerMock; + JobInvocation jobInvocation = new Builder() + .setTag("tag") + .setService(TestJobService.class.getName()) + .setTrigger(Trigger.NOW).build(); + when(receiverMock.prepareJob(any(GooglePlayMessengerCallback.class), eq(data))) + .thenReturn(jobInvocation); + + handler.handleMessage(message); + + verify(executionDelegatorMock).executeJob(jobInvocation); + } + + @Test + public void handleMessage_stopExecution() throws Exception { + Message message = Message.obtain(); + message.what = GooglePlayMessageHandler.MSG_STOP_EXEC; + JobCoder jobCoder = GooglePlayReceiver.getJobCoder(); + Bundle data = TestUtil.encodeContentUriJob(TestUtil.getContentUriTrigger(), jobCoder); + JobInvocation jobInvocation = jobCoder.decode(data).build(); + message.setData(data); + message.replyTo = messengerMock; + + handler.handleMessage(message); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(JobInvocation.class); + + verify(executionDelegatorMock).stopJob(captor.capture()); + + TestUtil.assertJobsEqual(jobInvocation, captor.getValue()); + } + + @Test + public void handleMessage_stopExecution_invalidNoCrash() throws Exception { + Message message = Message.obtain(); + message.what = GooglePlayMessageHandler.MSG_STOP_EXEC; + message.replyTo = messengerMock; + + handler.handleMessage(message); + + verify(executionDelegatorMock, never()).stopJob(any(JobInvocation.class)); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessengerCallbackTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessengerCallbackTest.java new file mode 100644 index 000000000..b71cf003a --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayMessengerCallbackTest.java @@ -0,0 +1,63 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG; +import static org.junit.Assert.assertEquals; + +import android.os.Message; +import android.os.Messenger; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests {@link GooglePlayMessengerCallback}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21) +public class GooglePlayMessengerCallbackTest { + + @Mock + Messenger messengerMock; + GooglePlayMessengerCallback callback; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + callback = new GooglePlayMessengerCallback(messengerMock, "tag"); + } + + @Test + public void jobFinished() throws Exception { + final ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + + callback.jobFinished(JobService.RESULT_SUCCESS); + + Mockito.verify(messengerMock).send(messageCaptor.capture()); + Message message = messageCaptor.getValue(); + assertEquals(message.what, GooglePlayMessageHandler.MSG_RESULT); + assertEquals(message.arg1, JobService.RESULT_SUCCESS); + assertEquals(message.getData().getString(REQUEST_PARAM_TAG), "tag"); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayReceiverTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayReceiverTest.java new file mode 100644 index 000000000..47d7da573 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/GooglePlayReceiverTest.java @@ -0,0 +1,327 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.TestUtil.encodeContentUriJob; +import static com.firebase.jobdispatcher.TestUtil.encodeRecurringContentUriJob; +import static com.firebase.jobdispatcher.TestUtil.getContentUriTrigger; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.app.Service; +import android.content.Intent; +import android.net.Uri; +import android.os.Binder; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Messenger; +import android.os.Parcel; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.MediaStore.Images.Media; +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.GooglePlayReceiverTest.ShadowMessenger; +import com.firebase.jobdispatcher.JobInvocation.Builder; +import com.firebase.jobdispatcher.TestUtil.InspectableBinder; +import com.google.android.gms.gcm.PendingCallback; +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implements; + +@RunWith(RobolectricTestRunner.class) +@Config( + constants = BuildConfig.class, + manifest = Config.NONE, + sdk = 21, + shadows = {ShadowMessenger.class} +) +public class GooglePlayReceiverTest { + + /** + * The default ShadowMessenger implementation causes NPEs when using the + * {@link Messenger#Messenger(Handler)} constructor. We create our own empty Shadow so we can + * just use the standard Android implementation, which is totally fine. + * + * @see Robolectric issue + * + */ + @Implements(Messenger.class) + public static class ShadowMessenger {} + + GooglePlayReceiver receiver; + + JobCoder jobCoder = new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, true); + + @Mock + Messenger messengerMock; + @Mock + IBinder binderMock; + @Mock + JobCallback callbackMock; + @Mock + ExecutionDelegator executionDelegatorMock; + @Mock + Driver driverMock; + @Captor + ArgumentCaptor jobArgumentCaptor; + + ArrayList triggeredUris = new ArrayList<>(); + + { + triggeredUris.add(ContactsContract.AUTHORITY_URI); + triggeredUris.add(Media.EXTERNAL_CONTENT_URI); + } + + Builder jobInvocationBuilder = new Builder() + .setTag("tag") + .setService(TestJobService.class.getName()) + .setTrigger(Trigger.NOW); + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + receiver = spy(new GooglePlayReceiver()); + when(receiver.getExecutionDelegator()).thenReturn(executionDelegatorMock); + receiver.driver = driverMock; + receiver.validationEnforcer = new ValidationEnforcer(new NoopJobValidator()); + } + + @Test + public void onJobFinished_unknownJobCallbackIsNotPresent_ignoreNoException() { + receiver.onJobFinished(jobInvocationBuilder.build(), JobService.RESULT_SUCCESS); + + verifyZeroInteractions(driverMock); + } + + @Test + public void onJobFinished_notRecurringContentJob_sendResult() { + jobInvocationBuilder.setTrigger( + Trigger.contentUriTrigger(Arrays.asList(new ObservedUri(Contacts.CONTENT_URI, 0)))); + + JobInvocation jobInvocation = receiver + .prepareJob(callbackMock, getBundleForContentJobExecution()); + + receiver.onJobFinished(jobInvocation, JobService.RESULT_SUCCESS); + verify(callbackMock).jobFinished(JobService.RESULT_SUCCESS); + verifyZeroInteractions(driverMock); + } + + @Test + public void onJobFinished_successRecurringContentJob_reschedule() { + JobInvocation jobInvocation = receiver + .prepareJob(callbackMock, getBundleForContentJobExecutionRecurring()); + + receiver.onJobFinished(jobInvocation, JobService.RESULT_SUCCESS); + + verify(driverMock).schedule(jobArgumentCaptor.capture()); + + // No need to callback when job finished. + // Reschedule request is treated as two events: completion of old job and scheduling of new + // job with the same parameters. + verifyZeroInteractions(callbackMock); + + Job rescheduledJob = jobArgumentCaptor.getValue(); + TestUtil.assertJobsEqual(jobInvocation, rescheduledJob); + } + + @Test + public void onJobFinished_failWithRetryRecurringContentJob_sendResult() { + JobInvocation jobInvocation = receiver + .prepareJob(callbackMock, getBundleForContentJobExecutionRecurring()); + + receiver.onJobFinished(jobInvocation, JobService.RESULT_FAIL_RETRY); + + // If a job finishes with RESULT_FAIL_RETRY we don't need to send a reschedule request. + // Rescheduling will erase previously triggered URIs. + verify(callbackMock).jobFinished(JobService.RESULT_FAIL_RETRY); + + verifyZeroInteractions(driverMock); + } + + @Test + public void prepareJob() { + Intent intent = new Intent(); + + Bundle encode = encodeContentUriJob(getContentUriTrigger(), jobCoder); + intent.putExtra(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode); + + Parcel container = Parcel.obtain(); + container.writeStrongBinder(new Binder()); + PendingCallback pcb = new PendingCallback(container); + intent.putExtra("callback", pcb); + + ArrayList uris = new ArrayList<>(); + uris.add(ContactsContract.AUTHORITY_URI); + uris.add(Media.EXTERNAL_CONTENT_URI); + intent.putParcelableArrayListExtra(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, uris); + + JobInvocation jobInvocation = receiver.prepareJob(intent); + assertEquals(jobInvocation.getTriggerReason().getTriggeredContentUris(), uris); + } + + @Test + public void prepareJob_messenger() { + JobInvocation jobInvocation = receiver.prepareJob(callbackMock, new Bundle()); + assertNull(jobInvocation); + verify(callbackMock).jobFinished(JobService.RESULT_FAIL_NORETRY); + } + + @Test + public void prepareJob_messenger_noExtras() { + Bundle bundle = getBundleForContentJobExecution(); + + JobInvocation jobInvocation = receiver.prepareJob(callbackMock, bundle); + assertEquals(jobInvocation.getTriggerReason().getTriggeredContentUris(), triggeredUris); + } + + @NonNull + private Bundle getBundleForContentJobExecution() { + Bundle bundle = new Bundle(); + + Bundle encode = encodeContentUriJob(getContentUriTrigger(), jobCoder); + bundle.putBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode); + + bundle.putParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, triggeredUris); + return bundle; + } + + @NonNull + private Bundle getBundleForContentJobExecutionRecurring() { + Bundle bundle = new Bundle(); + + Bundle encode = encodeRecurringContentUriJob(getContentUriTrigger(), jobCoder); + bundle.putBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode); + + bundle.putParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, triggeredUris); + return bundle; + } + + @Test + public void onBind() { + Intent intent = new Intent(GooglePlayReceiver.ACTION_EXECUTE); + IBinder binderA = receiver.onBind(intent); + IBinder binderB = receiver.onBind(intent); + + assertEquals(binderA, binderB); + } + + @Test + public void onBind_nullIntent() { + IBinder binder = receiver.onBind(null); + assertNull(binder); + } + + @Test + public void onBind_wrongAction() { + Intent intent = new Intent("test"); + IBinder binder = receiver.onBind(intent); + assertNull(binder); + } + + @Test + @Config(sdk = VERSION_CODES.KITKAT) + public void onBind_wrongBuild() { + Intent intent = new Intent(GooglePlayReceiver.ACTION_EXECUTE); + IBinder binder = receiver.onBind(intent); + assertNull(binder); + } + + @Test + public void onStartCommand_nullIntent() { + assertResultWasStartNotSticky(receiver.onStartCommand(null, 0, 101)); + verify(receiver).stopSelf(101); + } + + @Test + public void onStartCommand_initAction() { + Intent initIntent = new Intent("com.google.android.gms.gcm.SERVICE_ACTION_INITIALIZE"); + assertResultWasStartNotSticky(receiver.onStartCommand(initIntent, 0, 101)); + verify(receiver).stopSelf(101); + } + + @Test + public void onStartCommand_unknownAction() { + Intent unknownIntent = new Intent("com.example.foo.bar"); + assertResultWasStartNotSticky(receiver.onStartCommand(unknownIntent, 0, 101)); + assertResultWasStartNotSticky(receiver.onStartCommand(unknownIntent, 0, 102)); + assertResultWasStartNotSticky(receiver.onStartCommand(unknownIntent, 0, 103)); + + InOrder inOrder = inOrder(receiver); + inOrder.verify(receiver).stopSelf(101); + inOrder.verify(receiver).stopSelf(102); + inOrder.verify(receiver).stopSelf(103); + } + + @Test + public void onStartCommand_executeActionWithEmptyExtras() { + Intent execIntent = new Intent("com.google.android.gms.gcm.ACTION_TASK_READY"); + assertResultWasStartNotSticky(receiver.onStartCommand(execIntent, 0, 101)); + verify(receiver).stopSelf(101); + } + + @Test + public void onStartCommand_executeAction() { + JobInvocation job = new JobInvocation.Builder() + .setTag("tag") + .setService("com.example.foo.FooService") + .setTrigger(Trigger.NOW) + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .setLifetime(Lifetime.UNTIL_NEXT_BOOT) + .setConstraints(new int[]{Constraint.DEVICE_IDLE}) + .build(); + + Intent execIntent = new Intent("com.google.android.gms.gcm.ACTION_TASK_READY") + .putExtra("extras", new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, true) + .encode(job, new Bundle())) + .putExtra("callback", new InspectableBinder().toPendingCallback()); + + when(executionDelegatorMock.executeJob(any(JobInvocation.class))).thenReturn(true); + + assertResultWasStartNotSticky(receiver.onStartCommand(execIntent, 0, 101)); + + verify(receiver, never()).stopSelf(anyInt()); + verify(executionDelegatorMock).executeJob(any(JobInvocation.class)); + + receiver.onJobFinished(job, JobService.RESULT_SUCCESS); + + verify(receiver).stopSelf(101); + } + + private void assertResultWasStartNotSticky(int result) { + assertEquals( + "Result for onStartCommand wasn't START_NOT_STICKY", Service.START_NOT_STICKY, result); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ImmediateTriggerTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ImmediateTriggerTest.java new file mode 100644 index 000000000..1cf57ee68 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ImmediateTriggerTest.java @@ -0,0 +1,34 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class ImmediateTriggerTest { + /** + * Code coverage. + */ + @Test + public void testPrivateConstructor() throws Exception { + TestUtil.assertHasSinglePrivateConstructor(JobTrigger.ImmediateTrigger.class); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobBuilderTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobBuilderTest.java new file mode 100644 index 000000000..ffb4026e8 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobBuilderTest.java @@ -0,0 +1,65 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class JobBuilderTest { + private static final int[] ALL_LIFETIMES = {Lifetime.UNTIL_NEXT_BOOT, Lifetime.FOREVER}; + + private Job.Builder mBuilder; + + @Before + public void setUp() throws Exception { + mBuilder = TestUtil.getBuilderWithNoopValidator(); + } + + @Test + public void testAddConstraints() { + mBuilder.setConstraints() + .addConstraint(Constraint.DEVICE_CHARGING) + .addConstraint(Constraint.ON_UNMETERED_NETWORK); + + int[] expected = {Constraint.DEVICE_CHARGING, Constraint.ON_UNMETERED_NETWORK}; + + assertEquals(Constraint.compact(expected), Constraint.compact(mBuilder.getConstraints())); + } + + @Test + public void testSetLifetime() { + for (int lifetime : ALL_LIFETIMES) { + mBuilder.setLifetime(lifetime); + assertEquals(lifetime, mBuilder.getLifetime()); + } + } + + @Test + public void testSetShouldReplaceCurrent() { + for (boolean replace : new boolean[]{true, false}) { + mBuilder.setReplaceCurrent(replace); + assertEquals(replace, mBuilder.shouldReplaceCurrent()); + } + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobCoderTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobCoderTest.java new file mode 100644 index 000000000..bd3ebfb94 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobCoderTest.java @@ -0,0 +1,158 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static com.firebase.jobdispatcher.TestUtil.encodeContentUriJob; +import static com.firebase.jobdispatcher.TestUtil.getContentUriTrigger; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.MediaStore.Images.Media; +import com.firebase.jobdispatcher.Job.Builder; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class JobCoderTest { + private final JobCoder mCoder = new JobCoder(PREFIX, true); + private static final String PREFIX = "prefix"; + private Builder mBuilder; + + private static Builder setValidBuilderDefaults(Builder mBuilder) { + return mBuilder + .setTag("tag") + .setTrigger(Trigger.NOW) + .setService(TestJobService.class) + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL); + } + + @Before + public void setUp() throws Exception { + mBuilder = TestUtil.getBuilderWithNoopValidator(); + } + + @Test + public void testCodingIsLossless() { + for (JobParameters input : TestUtil.getJobCombinations(mBuilder)) { + + JobParameters output = mCoder.decode(mCoder.encode(input, input.getExtras())).build(); + + TestUtil.assertJobsEqual(input, output); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testEncode_throwsOnNullBundle() { + mCoder.encode(mBuilder.build(), null); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecode_throwsOnNullBundle() { + mCoder.decode(null); + } + + @Test + public void testDecode_failsWhenMissingFields() { + assertNull("Expected null tag to cause decoding to fail", + mCoder.decode(mCoder.encode( + setValidBuilderDefaults(mBuilder).setTag(null).build(), + new Bundle()))); + + assertNull("Expected null service to cause decoding to fail", + mCoder.decode(mCoder.encode( + setValidBuilderDefaults(mBuilder).setService(null).build(), + new Bundle()))); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecode_failsUnsupportedTrigger() { + mCoder.decode(mCoder.encode(setValidBuilderDefaults(mBuilder).setTrigger(null).build(), + new Bundle())); + } + + @Test + public void testDecode_ignoresMissingRetryStrategy() { + assertNotNull("Expected null retry strategy to cause decode to use a default", + mCoder.decode(mCoder.encode( + setValidBuilderDefaults(mBuilder).setRetryStrategy(null).build(), + new Bundle()))); + } + + @Test + public void encode_contentUriTrigger() { + Bundle encode = TestUtil.encodeContentUriJob(TestUtil.getContentUriTrigger(), mCoder); + int triggerType = encode.getInt(PREFIX + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE); + assertEquals("Trigger type", BundleProtocol.TRIGGER_TYPE_CONTENT_URI, triggerType); + + String json = encode.getString(PREFIX + BundleProtocol.PACKED_PARAM_OBSERVED_URI); + String expectedJson = "{\"uri_flags\":[1,0],\"uris\":[\"content:\\/\\/com.android.contacts" + + "\",\"content:\\/\\/media\\/external\\/images\\/media\"]}"; + assertEquals("Json trigger", expectedJson, json); + } + + @Test + public void decode_contentUriTrigger() { + ContentUriTrigger contentUriTrigger = TestUtil.getContentUriTrigger(); + Bundle bundle = TestUtil.encodeContentUriJob(contentUriTrigger, mCoder); + JobInvocation decode = mCoder.decode(bundle).build(); + ContentUriTrigger trigger = (ContentUriTrigger) decode.getTrigger(); + assertEquals(contentUriTrigger.getUris(), trigger.getUris()); + } + + @Test + public void decode_addBundleAsExtras() { + ContentUriTrigger contentUriTrigger = TestUtil.getContentUriTrigger(); + Bundle bundle = TestUtil.encodeContentUriJob(contentUriTrigger, mCoder); + bundle.putString("test_key", "test_value"); + JobInvocation decode = mCoder.decode(bundle).build(); + assertEquals("test_value", decode.getExtras().getString("test_key")); + } + + @Test + public void decodeIntentBundle() { + Bundle bundle = new Bundle(); + + ContentUriTrigger uriTrigger = getContentUriTrigger(); + Bundle encode = encodeContentUriJob(uriTrigger, mCoder); + bundle.putBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode); + + ArrayList uris = new ArrayList<>(); + uris.add(ContactsContract.AUTHORITY_URI); + uris.add(Media.EXTERNAL_CONTENT_URI); + bundle.putParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, uris); + + JobInvocation jobInvocation = mCoder.decodeIntentBundle(bundle); + + assertEquals(uris, jobInvocation.getTriggerReason().getTriggeredContentUris()); + assertEquals("TAG", jobInvocation.getTag()); + assertEquals(uriTrigger.getUris(), ((ContentUriTrigger) jobInvocation.getTrigger()) + .getUris()); + assertEquals(TestJobService.class.getName(), jobInvocation.getService()); + assertEquals(RetryStrategy.DEFAULT_EXPONENTIAL.getPolicy(), + jobInvocation.getRetryStrategy().getPolicy()); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobInvocationTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobInvocationTest.java new file mode 100644 index 000000000..769dd19d6 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobInvocationTest.java @@ -0,0 +1,83 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.os.Bundle; +import com.firebase.jobdispatcher.JobInvocation.Builder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class JobInvocationTest { + private Builder builder; + + @Before + public void setUp() { + builder = new Builder() + .setTag("tag") + .setService(TestJobService.class.getName()) + .setTrigger(Trigger.NOW); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testShouldReplaceCurrent() throws Exception { + assertTrue("Expected shouldReplaceCurrent() to return value passed in constructor", + builder.setReplaceCurrent(true).build().shouldReplaceCurrent()); + assertFalse("Expected shouldReplaceCurrent() to return value passed in constructor", + builder.setReplaceCurrent(false).build().shouldReplaceCurrent()); + } + + @Test + public void extras() throws Exception { + assertNotNull(builder.build().getExtras()); + + Bundle bundle = new Bundle(); + bundle.putLong("test", 1L); + Bundle extras = builder.addExtras(bundle).build().getExtras(); + assertEquals(1, extras.size()); + assertEquals(1L, extras.getLong("test")); + } + + @Test + public void contract_hashCode_equals() { + JobInvocation jobInvocation = builder.build(); + assertEquals(jobInvocation, builder.build()); + assertEquals(jobInvocation.hashCode(), builder.build().hashCode()); + JobInvocation jobInvocationNew = builder.setTag("new").build(); + assertNotEquals(jobInvocation, jobInvocationNew); + assertNotEquals(jobInvocation.hashCode(), jobInvocationNew.hashCode()); + } + + @Test + public void contract_hashCode_equals_triggerShouldBeIgnored() { + JobInvocation jobInvocation = builder.build(); + JobInvocation periodic = builder.setTrigger(Trigger.executionWindow(0, 1)).build(); + assertEquals(jobInvocation, periodic); + assertEquals(jobInvocation.hashCode(), periodic.hashCode()); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceConnectionTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceConnectionTest.java new file mode 100644 index 000000000..8a8be89f3 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceConnectionTest.java @@ -0,0 +1,116 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.IBinder; +import android.os.Message; +import com.firebase.jobdispatcher.JobInvocation.Builder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Test for {@link JobServiceConnection}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class JobServiceConnectionTest { + + JobInvocation job = new Builder() + .setTag("tag") + .setService(TestJobService.class.getName()) + .setTrigger(Trigger.NOW) + .build(); + + @Mock + Message messageMock; + @Mock + JobService.LocalBinder binderMock; + @Mock + JobService jobServiceMock; + + JobServiceConnection connection; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(binderMock.getService()).thenReturn(jobServiceMock); + connection = new JobServiceConnection(job, messageMock); + } + + @Test + public void fullConnectionCycle() { + assertFalse(connection.isBound()); + + connection.onServiceConnected(null, binderMock); + verify(jobServiceMock).start(job, messageMock); + assertTrue(connection.isBound()); + + connection.onStop(); + verify(jobServiceMock).stop(job); + assertTrue(connection.isBound()); + + connection.onServiceDisconnected(null); + assertFalse(connection.isBound()); + } + + @Test + public void onServiceConnected_shouldNotSendExecutionRequestTwice() { + assertFalse(connection.isBound()); + + connection.onServiceConnected(null, binderMock); + verify(jobServiceMock).start(job, messageMock); + assertTrue(connection.isBound()); + reset(jobServiceMock); + + connection.onServiceConnected(null, binderMock); + verify(jobServiceMock, never()).start(job, messageMock); // start should not be called again + + connection.onStop(); + verify(jobServiceMock).stop(job); + assertTrue(connection.isBound()); + + connection.onServiceDisconnected(null); + assertFalse(connection.isBound()); + } + + @Test + public void stopOnUnboundConnection() { + assertFalse(connection.isBound()); + connection.onStop(); + verify(jobServiceMock, never()).onStopJob(job); + } + + @Test + public void onServiceConnectedWrongBinder() { + IBinder binder = mock(IBinder.class); + connection.onServiceConnected(null, binder); + assertFalse(connection.isBound()); + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceTest.java new file mode 100644 index 000000000..83c7134f4 --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/JobServiceTest.java @@ -0,0 +1,346 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Message; +import android.os.Parcel; +import com.firebase.jobdispatcher.JobInvocation.Builder; +import com.google.android.gms.gcm.PendingCallback; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class JobServiceTest { + private static CountDownLatch countDownLatch; + + @Before + public void setUp() throws Exception {} + + @After + public void tearDown() throws Exception { + countDownLatch = null; + } + + @Test + public void testOnStartCommand_handlesNullIntent() throws Exception { + JobService service = spy(new ExampleJobService()); + int startId = 7; + + try { + service.onStartCommand(null, 0, startId); + + verify(service).stopSelf(startId); + } catch (NullPointerException npe) { + fail("Unexpected NullPointerException after calling onStartCommand with a null Intent."); + } + } + + @Test + public void testOnStartCommand_handlesNullAction() throws Exception { + JobService service = spy(new ExampleJobService()); + int startId = 7; + + Intent nullActionIntent = new Intent(); + service.onStartCommand(nullActionIntent, 0, startId); + + verify(service).stopSelf(startId); + } + + @Test + public void testOnStartCommand_handlesEmptyAction() throws Exception { + JobService service = spy(new ExampleJobService()); + int startId = 7; + + Intent emptyActionIntent = new Intent(""); + service.onStartCommand(emptyActionIntent, 0, startId); + + verify(service).stopSelf(startId); + } + + @Test + public void testOnStartCommand_handlesUnknownAction() throws Exception { + JobService service = spy(new ExampleJobService()); + int startId = 7; + + Intent emptyActionIntent = new Intent("foo.bar.baz"); + service.onStartCommand(emptyActionIntent, 0, startId); + + verify(service).stopSelf(startId); + } + + @Test + public void testOnStartCommand_handlesStartJob_nullData() { + JobService service = spy(new ExampleJobService()); + int startId = 7; + + Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE); + service.onStartCommand(executeJobIntent, 0, startId); + + verify(service).stopSelf(startId); + } + + @Test + public void testOnStartCommand_handlesStartJob_noTag() { + JobService service = spy(new ExampleJobService()); + int startId = 7; + + Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE); + Parcel p = Parcel.obtain(); + p.writeStrongBinder(mock(IBinder.class)); + executeJobIntent.putExtra("callback", new PendingCallback(p)); + + service.onStartCommand(executeJobIntent, 0, startId); + + verify(service).stopSelf(startId); + + p.recycle(); + } + + @Test + public void testOnStartCommand_handlesStartJob_noCallback() { + JobService service = spy(new ExampleJobService()); + int startId = 7; + + Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE); + executeJobIntent.putExtra("tag", "foobar"); + + service.onStartCommand(executeJobIntent, 0, startId); + + verify(service).stopSelf(startId); + } + + @Test + public void testOnStartCommand_handlesStartJob_validRequest() throws InterruptedException { + JobService service = spy(new ExampleJobService()); + + HandlerThread ht = new HandlerThread("handler"); + ht.start(); + Handler h = new Handler(ht.getLooper()); + + Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE); + + Job jobSpec = TestUtil.getBuilderWithNoopValidator() + .setTag("tag") + .setService(ExampleJobService.class) + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .setTrigger(Trigger.NOW) + .setLifetime(Lifetime.FOREVER) + .build(); + + countDownLatch = new CountDownLatch(1); + + ((JobService.LocalBinder) service.onBind(executeJobIntent)) + .getService() + .start(jobSpec, h.obtainMessage(ExecutionDelegator.JOB_FINISHED, jobSpec)); + + assertTrue("Expected job to run to completion", countDownLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testOnStartCommand_handlesStartJob_doNotStartRunningJobAgain() { + StoppableJobService service = new StoppableJobService(false); + + Job jobSpec = TestUtil.getBuilderWithNoopValidator() + .setTag("tag") + .setService(StoppableJobService.class) + .setTrigger(Trigger.NOW) + .build(); + + ((JobService.LocalBinder) service.onBind(null)).getService().start(jobSpec, null); + ((JobService.LocalBinder) service.onBind(null)).getService().start(jobSpec, null); + + assertEquals(1, service.getNumberOfExecutionRequestsReceived()); + } + + @Test + public void stop_noCallback_finished() { + JobService service = spy(new StoppableJobService(false)); + JobInvocation job = new Builder() + .setTag("Tag") + .setTrigger(Trigger.NOW) + .setService(StoppableJobService.class.getName()) + .build(); + service.stop(job); + verify(service, never()).onStopJob(job); + } + + @Test + public void stop_withCallback_retry() { + JobService service = spy(new StoppableJobService(false)); + + JobInvocation job = new Builder() + .setTag("Tag") + .setTrigger(Trigger.NOW) + .setService(StoppableJobService.class.getName()) + .build(); + + Handler handlerMock = mock(Handler.class); + Message message = Message.obtain(handlerMock); + service.start(job, message); + + service.stop(job); + verify(service).onStopJob(job); + verify(handlerMock).sendMessage(message); + assertEquals(message.arg1, JobService.RESULT_SUCCESS); + } + + @Test + public void stop_withCallback_done() { + JobService service = spy(new StoppableJobService(true)); + + JobInvocation job = new Builder() + .setTag("Tag") + .setTrigger(Trigger.NOW) + .setService(StoppableJobService.class.getName()) + .build(); + + Handler handlerMock = mock(Handler.class); + Message message = Message.obtain(handlerMock); + service.start(job, message); + + service.stop(job); + verify(service).onStopJob(job); + verify(handlerMock).sendMessage(message); + assertEquals(message.arg1, JobService.RESULT_FAIL_RETRY); + } + + @Test + public void onStartJob_jobFinishedReschedule() { + // Verify that a retry request from within onStartJob will cause the retry result to be sent + // to the bouncer service's handler, regardless of what value is ultimately returned from + // onStartJob. + JobService reschedulingService = new JobService() { + @Override + public boolean onStartJob(JobParameters job) { + // Reschedules job. + jobFinished(job, true /* retry this job */); + return false; + } + + @Override + public boolean onStopJob(JobParameters job) { + return false; + } + }; + + Job jobSpec = TestUtil.getBuilderWithNoopValidator() + .setTag("tag") + .setService(reschedulingService.getClass()) + .setTrigger(Trigger.NOW) + .build(); + Handler mock = mock(Handler.class); + Message message = new Message(); + message.setTarget(mock); + reschedulingService.start(jobSpec, message); + + verify(mock).sendMessage(message); + assertEquals(message.arg1, JobService.RESULT_FAIL_RETRY); + } + + @Test + public void onStartJob_jobFinishedNotReschedule() { + // Verify that a termination request from within onStartJob will cause the result to be sent + // to the bouncer service's handler, regardless of what value is ultimately returned from + // onStartJob. + JobService reschedulingService = new JobService() { + @Override + public boolean onStartJob(JobParameters job) { + jobFinished(job, false /* don't retry this job */); + return false; + } + + @Override + public boolean onStopJob(JobParameters job) { + return false; + } + }; + + Job jobSpec = TestUtil.getBuilderWithNoopValidator() + .setTag("tag") + .setService(reschedulingService.getClass()) + .setTrigger(Trigger.NOW) + .build(); + Handler mock = mock(Handler.class); + Message message = new Message(); + message.setTarget(mock); + reschedulingService.start(jobSpec, message); + + verify(mock).sendMessage(message); + assertEquals(message.arg1, JobService.RESULT_SUCCESS); + } + + public static class ExampleJobService extends JobService { + @Override + public boolean onStartJob(JobParameters job) { + countDownLatch.countDown(); + return false; + } + + @Override + public boolean onStopJob(JobParameters job) { + return false; + } + } + + public static class StoppableJobService extends JobService { + + private final boolean shouldReschedule; + + public int getNumberOfExecutionRequestsReceived() { + return amountOfExecutionRequestReceived.get(); + } + + private final AtomicInteger amountOfExecutionRequestReceived = new AtomicInteger(); + + public StoppableJobService(boolean shouldReschedule) { + this.shouldReschedule = shouldReschedule; + } + + @Override + public boolean onStartJob(JobParameters job) { + amountOfExecutionRequestReceived.incrementAndGet(); + return true; + } + + @Override + public boolean onStopJob(JobParameters job) { + return shouldReschedule; + } + + + } +} diff --git a/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ValidationEnforcerTest.java b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ValidationEnforcerTest.java new file mode 100644 index 000000000..2e4e939ac --- /dev/null +++ b/jobdispatcher/src/test/java/com/firebase/jobdispatcher/ValidationEnforcerTest.java @@ -0,0 +1,187 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23) +public class ValidationEnforcerTest { + private static final List ERROR_LIST = Collections.singletonList("error: foo"); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Mock + private JobValidator mValidator; + + @Mock + private JobParameters mMockJobParameters; + + @Mock + private JobTrigger mMockTrigger; + + private ValidationEnforcer mEnforcer; + private RetryStrategy mRetryStrategy = RetryStrategy.DEFAULT_EXPONENTIAL; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mEnforcer = new ValidationEnforcer(mValidator); + } + + @Test + public void testValidate_retryStrategy() throws Exception { + mEnforcer.validate(mRetryStrategy); + verify(mValidator).validate(mRetryStrategy); + } + + @Test + public void testValidate_jobSpec() throws Exception { + mEnforcer.validate(mMockJobParameters); + verify(mValidator).validate(mMockJobParameters); + } + + @Test + public void testValidate_trigger() throws Exception { + mEnforcer.validate(mMockTrigger); + verify(mValidator).validate(mMockTrigger); + } + + @Test + public void testIsValid_retryStrategy_invalid() throws Exception { + when(mValidator.validate(mRetryStrategy)) + .thenReturn(Collections.singletonList("error: foo")); + + assertFalse("isValid", mEnforcer.isValid(mRetryStrategy)); + } + + @Test + public void testIsValid_retryStrategy_valid() throws Exception { + when(mValidator.validate(mRetryStrategy)).thenReturn(null); + + assertTrue("isValid", mEnforcer.isValid(mRetryStrategy)); + + } + + @Test + public void testIsValid_trigger_invalid() throws Exception { + when(mValidator.validate(mMockTrigger)) + .thenReturn(Collections.singletonList("error: foo")); + + assertFalse("isValid", mEnforcer.isValid(mMockTrigger)); + } + + @Test + public void testIsValid_trigger_valid() throws Exception { + when(mValidator.validate(mMockTrigger)).thenReturn(null); + + assertTrue("isValid", mEnforcer.isValid(mMockTrigger)); + } + + @Test + public void testIsValid_jobSpec_invalid() throws Exception { + when(mValidator.validate(mMockJobParameters)).thenReturn(ERROR_LIST); + + assertFalse("isValid", mEnforcer.isValid(mMockJobParameters)); + } + + @Test + public void testIsValid_jobSpec_valid() throws Exception { + when(mValidator.validate(mMockJobParameters)).thenReturn(null); + + assertTrue("isValid", mEnforcer.isValid(mMockJobParameters)); + } + + @Test + public void testEnsureValid_retryStrategy_valid() throws Exception { + when(mValidator.validate(mRetryStrategy)).thenReturn(null); + + mEnforcer.ensureValid(mRetryStrategy); + } + + @Test + public void testEnsureValid_trigger_valid() throws Exception { + when(mValidator.validate(mMockTrigger)).thenReturn(null); + + mEnforcer.ensureValid(mMockTrigger); + } + + @Test + public void testEnsureValid_jobSpec_valid() throws Exception { + when(mValidator.validate(mMockJobParameters)).thenReturn(null); + + mEnforcer.ensureValid(mMockJobParameters); + } + + @Test + public void testEnsureValid_retryStrategy_invalid() throws Exception { + expectedException.expect(ValidationEnforcer.ValidationException.class); + + when(mValidator.validate(mRetryStrategy)).thenReturn(ERROR_LIST); + mEnforcer.ensureValid(mRetryStrategy); + } + + @Test + public void testEnsureValid_trigger_invalid() throws Exception { + expectedException.expect(ValidationEnforcer.ValidationException.class); + + when(mValidator.validate(mMockTrigger)).thenReturn(ERROR_LIST); + mEnforcer.ensureValid(mMockTrigger); + } + + @Test + public void testEnsureValid_jobSpec_invalid() throws Exception { + expectedException.expect(ValidationEnforcer.ValidationException.class); + + when(mValidator.validate(mMockJobParameters)).thenReturn(ERROR_LIST); + mEnforcer.ensureValid(mMockJobParameters); + } + + @Test + public void testValidationMessages() throws Exception { + when(mValidator.validate(mMockJobParameters)).thenReturn(ERROR_LIST); + + try { + mEnforcer.ensureValid(mMockJobParameters); + + fail("Expected ensureValid to fail"); + } catch (ValidationEnforcer.ValidationException ve) { + assertEquals("Expected ValidationException to have 1 error message", + 1, + ve.getErrors().size()); + } + } +} diff --git a/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/NoopJobValidator.java b/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/NoopJobValidator.java new file mode 100644 index 000000000..079a6eed2 --- /dev/null +++ b/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/NoopJobValidator.java @@ -0,0 +1,44 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import android.support.annotation.Nullable; +import java.util.List; + +/** + * A very simple Validator that thinks that everything is ok. Used for testing. + */ +class NoopJobValidator implements JobValidator { + + @Nullable + @Override + public List validate(JobParameters job) { + return null; + } + + @Nullable + @Override + public List validate(JobTrigger trigger) { + return null; + } + + @Nullable + @Override + public List validate(RetryStrategy retryStrategy) { + return null; + } +} diff --git a/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestJobService.java b/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestJobService.java new file mode 100644 index 000000000..77b3604ab --- /dev/null +++ b/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestJobService.java @@ -0,0 +1,71 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +/** A very simple JobService that can be configured for individual tests. */ +public class TestJobService extends JobService { + + public interface JobServiceProxy { + boolean onStartJob(JobParameters job); + + boolean onStopJob(JobParameters job); + } + + public static final JobServiceProxy NOOP_PROXY = + new JobServiceProxy() { + @Override + public boolean onStartJob(JobParameters job) { + return false; + } + + @Override + public boolean onStopJob(JobParameters job) { + return false; + } + }; + + private static final Object lock = new Object(); + + // GuardedBy("lock") + private static JobServiceProxy currentProxy = NOOP_PROXY; + + public static void setProxy(JobServiceProxy proxy) { + synchronized (lock) { + currentProxy = proxy; + } + } + + public static void reset() { + synchronized (lock) { + currentProxy = NOOP_PROXY; + } + } + + @Override + public boolean onStartJob(JobParameters job) { + synchronized (lock) { + return currentProxy.onStartJob(job); + } + } + + @Override + public boolean onStopJob(JobParameters job) { + synchronized (lock) { + return currentProxy.onStopJob(job); + } + } +} diff --git a/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestUtil.java b/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestUtil.java new file mode 100644 index 000000000..85f9fadcd --- /dev/null +++ b/jobdispatcher/src/testLib/java/com/firebase/jobdispatcher/TestUtil.java @@ -0,0 +1,375 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.firebase.jobdispatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Binder; +import android.os.Bundle; +import android.os.Parcel; +import android.provider.ContactsContract; +import android.provider.MediaStore.Images.Media; +import android.support.annotation.NonNull; +import com.firebase.jobdispatcher.Job.Builder; +import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger; +import com.firebase.jobdispatcher.ObservedUri.Flags; +import com.google.android.gms.gcm.PendingCallback; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * Provides common utilities helpful for testing. + */ +public class TestUtil { + + private static final String TAG = "TAG"; + private static final String[] TAG_COMBINATIONS = {"tag", "foobar", "fooooooo", "bz", "100"}; + + private static final int[] LIFETIME_COMBINATIONS = { + Lifetime.UNTIL_NEXT_BOOT, + Lifetime.FOREVER}; + + private static final JobTrigger[] TRIGGER_COMBINATIONS = { + Trigger.executionWindow(0, 30), + Trigger.executionWindow(300, 600), + Trigger.executionWindow(86400, 86400 * 2), + Trigger.NOW, + Trigger.contentUriTrigger( + Arrays.asList(new ObservedUri(ContactsContract.AUTHORITY_URI, 0))), + Trigger.contentUriTrigger(Arrays.asList(new ObservedUri(ContactsContract.AUTHORITY_URI, 0), + new ObservedUri(ContactsContract.AUTHORITY_URI, Flags.FLAG_NOTIFY_FOR_DESCENDANTS))) + }; + + @SuppressWarnings("unchecked") + private static final List> SERVICE_COMBINATIONS = + Arrays.asList(TestJobService.class); + + private static final RetryStrategy[] RETRY_STRATEGY_COMBINATIONS = { + RetryStrategy.DEFAULT_LINEAR, + new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 60, 300), + RetryStrategy.DEFAULT_EXPONENTIAL, + new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 300, 600), + }; + + public static void assertHasSinglePrivateConstructor(Class cls) throws Exception { + Constructor[] constructors = cls.getDeclaredConstructors(); + assertEquals("expected number of constructors to be == 1", 1, constructors.length); + + Constructor constructor = constructors[0]; + assertFalse("expected constructor to be inaccessible", constructor.isAccessible()); + + constructor.setAccessible(true); + constructor.newInstance(); + } + + static List> getAllConstraintCombinations() { + List> combos = new LinkedList<>(); + + combos.add(Collections.emptyList()); + for (Integer cur : Constraint.ALL_CONSTRAINTS) { + for (int l = combos.size() - 1; l >= 0; l--) { + List oldCombo = combos.get(l); + List newCombo = Arrays.asList(new Integer[oldCombo.size() + 1]); + + Collections.copy(newCombo, oldCombo); + newCombo.set(oldCombo.size(), cur); + combos.add(newCombo); + } + combos.add(Collections.singletonList(cur)); + } + + return combos; + } + + static int[] toIntArray(List list) { + int[] input = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + input[i] = list.get(i); + } + return input; + } + + static List getJobCombinations(Builder builder) { + return getCombination(new JobBuilder(builder)); + } + + static List getJobInvocationCombinations() { + return getCombination(new JobInvocationBuilder()); + } + + private static List getCombination( + JobParameterBuilder buildJobParam) { + + List result = new ArrayList<>(); + for (String tag : TAG_COMBINATIONS) { + for (List constraintList : getAllConstraintCombinations()) { + for (boolean recurring : new boolean[]{true, false}) { + for (boolean replaceCurrent : new boolean[]{true, false}) { + for (int lifetime : LIFETIME_COMBINATIONS) { + for (JobTrigger trigger : TRIGGER_COMBINATIONS) { + for (Class service : SERVICE_COMBINATIONS) { + for (Bundle extras : getBundleCombinations()) { + for (RetryStrategy rs : RETRY_STRATEGY_COMBINATIONS) { + result.add(buildJobParam.build( + tag, + replaceCurrent, + constraintList, + recurring, + lifetime, + trigger, + service, + extras, + rs)); + } + } + } + } + } + } + } + } + } + return result; + } + + private static Bundle[] getBundleCombinations() { + List bundles = new LinkedList<>(); + bundles.add(new Bundle()); + + Bundle b = new Bundle(); + b.putString("foo", "bar"); + b.putInt("bar", 1); + b.putLong("baz", 3L); + bundles.add(b); + + return bundles.toArray(new Bundle[bundles.size()]); + } + + static void assertJobsEqual(JobParameters input, JobParameters output) { + assertNotNull("input", input); + assertNotNull("output", output); + + assertEquals("isRecurring()", input.isRecurring(), output.isRecurring()); + assertEquals("shouldReplaceCurrent()", + input.shouldReplaceCurrent(), + output.shouldReplaceCurrent()); + assertEquals("getLifetime()", input.getLifetime(), output.getLifetime()); + assertEquals("getTag()", input.getTag(), output.getTag()); + assertEquals("getService()", input.getService(), output.getService()); + assertEquals("getConstraints()", + Constraint.compact(input.getConstraints()), + Constraint.compact(output.getConstraints())); + + assertTriggersEqual(input.getTrigger(), output.getTrigger()); + assertBundlesEqual(input.getExtras(), output.getExtras()); + assertRetryStrategiesEqual(input.getRetryStrategy(), output.getRetryStrategy()); + } + + static void assertRetryStrategiesEqual(RetryStrategy in, RetryStrategy out) { + String prefix = "getRetryStrategy()."; + + assertEquals(prefix + "getPolicy()", + in.getPolicy(), out.getPolicy()); + assertEquals(prefix + "getInitialBackoff()", + in.getInitialBackoff(), out.getInitialBackoff()); + assertEquals(prefix + "getMaximumBackoff()", + in.getMaximumBackoff(), out.getMaximumBackoff()); + } + + static void assertBundlesEqual(Bundle inExtras, Bundle outExtras) { + if (inExtras == null || outExtras == null) { + assertNull(inExtras); + assertNull(outExtras); + return; + } + + assertEquals("getExtras().size()", inExtras.size(), outExtras.size()); + final Set inKeys = inExtras.keySet(); + for (String key : inKeys) { + assertTrue("getExtras().containsKey(\"" + key + "\")", outExtras.containsKey(key)); + assertEquals("getExtras().get(\"" + key + "\")", inExtras.get(key), outExtras.get(key)); + } + } + + static void assertTriggersEqual(JobTrigger inTrigger, JobTrigger outTrigger) { + assertEquals("", inTrigger.getClass(), outTrigger.getClass()); + + if (inTrigger instanceof JobTrigger.ExecutionWindowTrigger) { + assertEquals("getTrigger().getWindowStart()", + ((JobTrigger.ExecutionWindowTrigger) inTrigger).getWindowStart(), + ((JobTrigger.ExecutionWindowTrigger) outTrigger).getWindowStart()); + assertEquals("getTrigger().getWindowEnd()", + ((JobTrigger.ExecutionWindowTrigger) inTrigger).getWindowEnd(), + ((JobTrigger.ExecutionWindowTrigger) outTrigger).getWindowEnd()); + } else if (inTrigger == Trigger.NOW) { + assertEquals(inTrigger, outTrigger); + } else if (inTrigger instanceof JobTrigger.ContentUriTrigger) { + assertEquals("Collection of URIs", + ((ContentUriTrigger) inTrigger).getUris(), + ((ContentUriTrigger) outTrigger).getUris()); + } else { + fail("Unknown Trigger class: " + inTrigger.getClass()); + } + } + + @NonNull + public static Builder getBuilderWithNoopValidator() { + return new Builder(new ValidationEnforcer(new NoopJobValidator())); + } + + @NonNull + static Bundle encodeContentUriJob(ContentUriTrigger trigger, JobCoder coder) { + Job job = getBuilderWithNoopValidator() + .setTag(TAG) + .setTrigger(trigger) + .setService(TestJobService.class) + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .build(); + return coder.encode(job, new Bundle()); + } + + @NonNull + static Bundle encodeRecurringContentUriJob(ContentUriTrigger trigger, JobCoder coder) { + Job job = getBuilderWithNoopValidator() + .setTag(TAG) + .setTrigger(trigger) + .setService(TestJobService.class) + .setReplaceCurrent(true) + .setRecurring(true) + .build(); + return coder.encode(job, new Bundle()); + } + + static ContentUriTrigger getContentUriTrigger() { + ObservedUri contactUri = new ObservedUri( + ContactsContract.AUTHORITY_URI, Flags.FLAG_NOTIFY_FOR_DESCENDANTS); + ObservedUri imageUri = new ObservedUri(Media.EXTERNAL_CONTENT_URI, 0); + return Trigger.contentUriTrigger(Arrays.asList(contactUri, imageUri)); + } + + public static class TransactionArguments { + public final int code; + public final Parcel data; + public final int flags; + + public TransactionArguments(int code, Parcel data, int flags) { + this.code = code; + this.data = data; + this.flags = flags; + } + } + + public static class InspectableBinder extends Binder { + private final List transactionArguments = new LinkedList<>(); + + public InspectableBinder() {} + + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) { + transactionArguments.add(new TransactionArguments(code, copyParcel(data), flags)); + return true; + } + + public PendingCallback toPendingCallback() { + Parcel container = Parcel.obtain(); + try { + container.writeStrongBinder(this); + container.setDataPosition(0); + return new PendingCallback(container); + } finally { + container.recycle(); + } + } + + private Parcel copyParcel(Parcel data) { + Parcel clone = Parcel.obtain(); + clone.appendFrom(data, 0, data.dataSize()); + clone.setDataPosition(0); + return clone; + } + + public List getArguments() { + return Collections.unmodifiableList(transactionArguments); + } + } + + private static class JobInvocationBuilder implements + JobParameterBuilder { + + @Override + public JobInvocation build(String tag, boolean replaceCurrent, List constraintList, + boolean recurring, int lifetime, JobTrigger trigger, Class service, + Bundle extras, RetryStrategy rs) { + //noinspection WrongConstant + return new JobInvocation.Builder() + .setTag(tag) + .setReplaceCurrent(replaceCurrent) + .setRecurring(recurring) + .setConstraints(toIntArray(constraintList)) + .setLifetime(lifetime) + .setTrigger(trigger) + .setService(service.getName()) + .addExtras(extras) + .setRetryStrategy(rs) + .build(); + } + } + + private static class JobBuilder implements JobParameterBuilder { + + private final Builder builder; + + public JobBuilder(Builder builder){ + this.builder = builder; + } + + @Override + public Job build(String tag, boolean replaceCurrent, List constraintList, + boolean recurring, int lifetime, JobTrigger trigger, Class service, + Bundle extras, RetryStrategy rs) { + //noinspection WrongConstant + return builder + .setTag(tag) + .setReplaceCurrent(replaceCurrent) + .setRecurring(recurring) + .setConstraints(toIntArray(constraintList)) + .setLifetime(lifetime) + .setTrigger(trigger) + .setService(service) + .setExtras(extras) + .setRetryStrategy(rs) + .build(); + } + } + + private interface JobParameterBuilder { + + T build(String tag, boolean replaceCurrent, List constraintList, boolean recurring, + int lifetime, JobTrigger trigger, Class service, Bundle extras, + RetryStrategy rs); + } +} diff --git a/jobdispatcher/src/testLib/java/com/google/android/gms/gcm/PendingCallback.java b/jobdispatcher/src/testLib/java/com/google/android/gms/gcm/PendingCallback.java new file mode 100644 index 000000000..a12317c8a --- /dev/null +++ b/jobdispatcher/src/testLib/java/com/google/android/gms/gcm/PendingCallback.java @@ -0,0 +1,61 @@ +// Copyright 2016 Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.android.gms.gcm; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Keep; + +/** + * Parcelable class to wrap the binder we send to the client over IPC. Only included for the benefit + * of tests. + */ +@Keep +public final class PendingCallback implements Parcelable { + public static final Creator CREATOR = + new Creator() { + @Override + public PendingCallback createFromParcel(Parcel parcel) { + return new PendingCallback(parcel); + } + + @Override + public PendingCallback[] newArray(int i) { + return new PendingCallback[i]; + } + }; + private final IBinder mBinder; + + public PendingCallback(Parcel in) { + mBinder = in.readStrongBinder(); + } + + public IBinder getIBinder() { + return mBinder; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeStrongBinder(mBinder); + } +} diff --git a/settings.gradle b/settings.gradle index e7b4def49..570872419 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':jobdispatcher' From 2403ec3d6359332523e1e6394ddf3a8c22fa8e5f Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sat, 26 Aug 2017 22:19:24 +0200 Subject: [PATCH 03/49] this commit fixes #891 & fixes #890 --- .../data/dao/model/AbstractNotification.java | 4 ++ .../com/fastaccess/helper/PrefGetter.java | 3 +- .../provider/scheme/LinkParserHelper.java | 51 ++++++++++--------- .../provider/scheme/SchemeParser.java | 10 +++- .../handler/drawable/GlideDrawableTarget.java | 3 +- .../viewholder/IssueTimelineViewHolder.java | 17 ++++--- .../ui/modules/main/MainActivity.java | 4 +- .../ui/modules/main/MainPresenter.java | 4 +- .../repos/code/prettifier/ViewerFragment.java | 4 +- .../repos/code/prettifier/ViewerMvp.java | 2 +- .../code/prettifier/ViewerPresenter.java | 10 +++- .../PullRequestTimelinePresenter.java | 3 ++ .../reviews/AddReviewDialogFragment.kt | 2 +- .../reviews/changes/ReviewChangesActivity.kt | 15 ++++-- .../prettifier/pretty/PrettifyWebView.java | 13 +++-- .../layout/add_review_dialog_layout.xml | 29 ++++------- .../layout/comment_box_layout.xml | 1 + .../layout/issue_pager_activity.xml | 10 ++-- .../layout/issue_timeline_row_item.xml | 2 +- app/src/main/res/values/strings.xml | 1 - app/src/main/res/values/theme_bluish.xml | 6 ++- app/src/main/res/values/theme_light.xml | 6 ++- app/src/main/res/xml/behaviour_settings.xml | 2 +- debug_gradle.properties | 1 - 24 files changed, 123 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/com/fastaccess/data/dao/model/AbstractNotification.java b/app/src/main/java/com/fastaccess/data/dao/model/AbstractNotification.java index 616579463..573f44dd0 100644 --- a/app/src/main/java/com/fastaccess/data/dao/model/AbstractNotification.java +++ b/app/src/main/java/com/fastaccess/data/dao/model/AbstractNotification.java @@ -143,6 +143,10 @@ public static boolean hasUnreadNotifications() { .value() > 0; } + public static void deleteAll() { + App.getInstance().getDataStore().toBlocking().delete(Notification.class).get().value(); + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; diff --git a/app/src/main/java/com/fastaccess/helper/PrefGetter.java b/app/src/main/java/com/fastaccess/helper/PrefGetter.java index b5fba5976..73cbcb1fb 100644 --- a/app/src/main/java/com/fastaccess/helper/PrefGetter.java +++ b/app/src/main/java/com/fastaccess/helper/PrefGetter.java @@ -14,6 +14,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.net.URLDecoder; /** * Created by Kosh on 10 Nov 2016, 3:43 PM @@ -91,7 +92,7 @@ public class PrefGetter { private static final String OTP_CODE = "otp_code"; private static final String ENTERPRISE_OTP_CODE = "enterprise_otp_code"; private static final String APP_LANGUAGE = "app_language"; - private static final String SENT_VIA = "sent_via"; + private static final String SENT_VIA = "fasthub_signature"; private static final String SENT_VIA_BOX = "sent_via_enabled"; private static final String PROFILE_BACKGROUND_URL = "profile_background_url"; private static final String AMLOD_THEME_ENABLED = "amlod_theme_enabled"; diff --git a/app/src/main/java/com/fastaccess/provider/scheme/LinkParserHelper.java b/app/src/main/java/com/fastaccess/provider/scheme/LinkParserHelper.java index 2c0e6310a..dedc2b6b2 100644 --- a/app/src/main/java/com/fastaccess/provider/scheme/LinkParserHelper.java +++ b/app/src/main/java/com/fastaccess/provider/scheme/LinkParserHelper.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; +import android.webkit.MimeTypeMap; import com.annimon.stream.Optional; import com.annimon.stream.Stream; @@ -34,33 +35,35 @@ public class LinkParserHelper { } @NonNull static Uri getBlobBuilder(@NonNull Uri uri) { + boolean isSvg = "svg".equalsIgnoreCase(MimeTypeMap.getFileExtensionFromUrl(uri.toString())); List segments = uri.getPathSegments(); - Uri.Builder urlBuilder = null; - if (uri.getAuthority().equalsIgnoreCase(HOST_DEFAULT)) { - String owner = segments.get(0); - String repo = segments.get(1); - String branch = segments.get(3); - urlBuilder = new Uri.Builder(); - urlBuilder.scheme("https") - .authority(API_AUTHORITY) - .appendPath("repos") - .appendPath(owner) - .appendPath(repo) - .appendPath("contents"); - for (int i = 4; i < segments.size(); i++) { - urlBuilder.appendPath(segments.get(i)); - } - if (uri.getQueryParameterNames() != null) { - for (String query : uri.getQueryParameterNames()) { - urlBuilder.appendQueryParameter(query, uri.getQueryParameter(query)); - } - } - if (uri.getEncodedFragment() != null) { - urlBuilder.encodedFragment(uri.getEncodedFragment()); + if (isSvg) { + Uri svgBlob = Uri.parse(uri.toString().replace("blob/", "")); + return svgBlob.buildUpon().authority(RAW_AUTHORITY).build(); + } + Uri.Builder urlBuilder = new Uri.Builder(); + String owner = segments.get(0); + String repo = segments.get(1); + String branch = segments.get(3); + urlBuilder.scheme("https") + .authority(API_AUTHORITY) + .appendPath("repos") + .appendPath(owner) + .appendPath(repo) + .appendPath("contents"); + for (int i = 4; i < segments.size(); i++) { + urlBuilder.appendPath(segments.get(i)); + } + if (uri.getQueryParameterNames() != null) { + for (String query : uri.getQueryParameterNames()) { + urlBuilder.appendQueryParameter(query, uri.getQueryParameter(query)); } - urlBuilder.appendQueryParameter("ref", branch); } - return urlBuilder != null ? urlBuilder.build() : uri; + if (uri.getEncodedFragment() != null) { + urlBuilder.encodedFragment(uri.getEncodedFragment()); + } + urlBuilder.appendQueryParameter("ref", branch); + return urlBuilder.build(); } public static boolean isEnterprise(@Nullable String url) { diff --git a/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java b/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java index 5e60502f4..30c5c68de 100644 --- a/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java +++ b/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java @@ -131,7 +131,6 @@ public static void launchUri(@NonNull Context context, @NonNull Uri data, boolea if (MarkDownProvider.isArchive(data.toString())) return null; if (TextUtils.equals(authority, HOST_DEFAULT) || TextUtils.equals(authority, RAW_AUTHORITY) || TextUtils.equals(authority, API_AUTHORITY) || isEnterprise) { - Logger.e(data); Intent trending = getTrending(context, data); Intent userIntent = getUser(context, data); Intent repoIssues = getRepoIssueIntent(context, data); @@ -178,6 +177,11 @@ public static void launchUri(@NonNull Context context, @NonNull Uri data, boolea return null; } + private static boolean getInvitationIntent(@NonNull Uri uri) { + List segments = uri.getPathSegments(); + return (segments != null && segments.size() == 3) && "invitations".equalsIgnoreCase(uri.getLastPathSegment()); + } + @Nullable private static Intent getPullRequestIntent(@NonNull Context context, @NonNull Uri uri, boolean showRepoBtn) { List segments = uri.getPathSegments(); if (segments == null || segments.size() < 3) return null; @@ -264,6 +268,9 @@ public static void launchUri(@NonNull Context context, @NonNull Uri data, boolea */ @Nullable private static Intent getGeneralRepo(@NonNull Context context, @NonNull Uri uri) { //TODO parse deeper links to their associate views. meantime fallback to repoPage + if (getInvitationIntent(uri)) { + return null; + } boolean isEnterprise = PrefGetter.isEnterprise() && Uri.parse(LinkParserHelper.getEndpoint(PrefGetter.getEnterpriseUrl())).getAuthority() .equalsIgnoreCase(uri.getAuthority()); if (uri.getAuthority().equals(HOST_DEFAULT) || uri.getAuthority().equals(API_AUTHORITY) || isEnterprise) { @@ -359,6 +366,7 @@ public static void launchUri(@NonNull Context context, @NonNull Uri data, boolea } if (segmentTwo.equals("blob") || segmentTwo.equals("tree")) { Uri urlBuilder = getBlobBuilder(uri); + Logger.e(urlBuilder); return CodeViewerActivity.createIntent(context, urlBuilder.toString(), uri.toString()); } else { String authority = uri.getAuthority(); diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java index 428bab2d8..6dc63a6c2 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java @@ -8,6 +8,7 @@ import com.bumptech.glide.request.animation.GlideAnimation; import com.bumptech.glide.request.target.SimpleTarget; import com.fastaccess.R; +import com.fastaccess.helper.PrefGetter; import java.lang.ref.WeakReference; @@ -38,7 +39,7 @@ class GlideDrawableTarget extends SimpleTarget { resource.setBounds(rect); urlDrawable.setBounds(rect); urlDrawable.setDrawable(resource); - if (resource.isAnimated()) { + if (resource.isAnimated() && !PrefGetter.isGistDisabled()) { urlDrawable.setCallback((Drawable.Callback) textView.getTag(R.id.drawable_callback)); resource.setLoopCount(GlideDrawable.LOOP_FOREVER); resource.start(); diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/IssueTimelineViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/IssueTimelineViewHolder.java index bb7ca244c..01e94d5ea 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/IssueTimelineViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/IssueTimelineViewHolder.java @@ -47,12 +47,17 @@ public static IssueTimelineViewHolder newInstance(ViewGroup viewGroup, BaseRecyc avatarLayout.setUrl(issueEventModel.getAssigner().getAvatarUrl(), issueEventModel.getAssigner().getLogin(), false, LinkParserHelper.isEnterprise(issueEventModel.getUrl())); } else { - if (issueEventModel.getActor() != null) { - avatarLayout.setUrl(issueEventModel.getActor().getAvatarUrl(), issueEventModel.getActor().getLogin(), - false, LinkParserHelper.isEnterprise(issueEventModel.getUrl())); - } else if (issueEventModel.getAuthor() != null) { - avatarLayout.setUrl(issueEventModel.getAuthor().getAvatarUrl(), issueEventModel.getAuthor().getLogin(), - false, LinkParserHelper.isEnterprise(issueEventModel.getUrl())); + if (event != IssueEventType.committed) { + avatarLayout.setVisibility(View.VISIBLE); + if (issueEventModel.getActor() != null) { + avatarLayout.setUrl(issueEventModel.getActor().getAvatarUrl(), issueEventModel.getActor().getLogin(), + false, LinkParserHelper.isEnterprise(issueEventModel.getUrl())); + } else if (issueEventModel.getAuthor() != null) { + avatarLayout.setUrl(issueEventModel.getAuthor().getAvatarUrl(), issueEventModel.getAuthor().getLogin(), + false, LinkParserHelper.isEnterprise(issueEventModel.getUrl())); + } + } else { + avatarLayout.setVisibility(View.GONE); } } if (event != null) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java b/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java index db12f7c2e..e6da5aec2 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java @@ -109,8 +109,8 @@ public class MainActivity extends BaseActivity impl if (isLoggedIn() && Notification.hasUnreadNotifications()) { ViewHelper.tintDrawable(menu.findItem(R.id.notifications).setIcon(R.drawable.ic_ring).getIcon(), ViewHelper.getAccentColor(this)); } else { - ViewHelper.tintDrawable(menu.findItem(R.id.notifications).setIcon(R.drawable.ic_notifications_none).getIcon(), ViewHelper.getIconColor - (this)); + ViewHelper.tintDrawable(menu.findItem(R.id.notifications) + .setIcon(R.drawable.ic_notifications_none).getIcon(), ViewHelper.getIconColor(this)); } return super.onPrepareOptionsMenu(menu); } diff --git a/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java index 31a863749..7f7d7b0c4 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java @@ -47,8 +47,10 @@ public class MainPresenter extends BasePresenter implements MainMv .flatMap(login -> RxHelper.getObservable(RestProvider.getNotificationService(isEnterprise()) .getNotifications(ParseDateFormat.getLastWeekDate()))) .flatMapSingle(notificationPageable -> { - if (notificationPageable != null) { + if (notificationPageable != null && (notificationPageable.getItems() != null && !notificationPageable.getItems().isEmpty())) { return Notification.saveAsSingle(notificationPageable.getItems()); + } else { + Notification.deleteAll(); } return Single.just(true); }) diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java index b99954848..121aea971 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java @@ -66,8 +66,8 @@ private static ViewerFragment newInstance(@NonNull Bundle bundle) { return fragmentView; } - @Override public void onSetImageUrl(@NonNull String url) { - webView.loadImage(url); + @Override public void onSetImageUrl(@NonNull String url, boolean isSvg) { + webView.loadImage(url, isSvg); webView.setOnContentChangedListener(this); webView.setVisibility(View.VISIBLE); getActivity().invalidateOptionsMenu(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java index ac950eb49..7f264b717 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java @@ -16,7 +16,7 @@ interface ViewerMvp { interface View extends BaseMvp.FAView, PrettifyWebView.OnContentChangedListener { - void onSetImageUrl(@NonNull String url); + void onSetImageUrl(@NonNull String url, boolean isSvg); void onSetMdText(@NonNull String text, String baseUrl, boolean replace); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java index ee35725da..ec1437e05 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.webkit.MimeTypeMap; import com.fastaccess.R; import com.fastaccess.data.dao.MarkdownModel; @@ -83,7 +84,7 @@ class ViewerPresenter extends BasePresenter implements ViewerMvp if (fileModel != null) { isImage = MarkDownProvider.isImage(fileModel.getFullUrl()); if (isImage) { - sendToView(view -> view.onSetImageUrl(fileModel.getFullUrl())); + sendToView(view -> view.onSetImageUrl(fileModel.getFullUrl(), false)); } else { downloadedStream = fileModel.getContent(); isRepo = fileModel.isRepo(); @@ -104,7 +105,12 @@ class ViewerPresenter extends BasePresenter implements ViewerMvp @Override public void onWorkOnline() { isImage = MarkDownProvider.isImage(url); if (isImage) { - sendToView(view -> view.onSetImageUrl(url)); + if ("svg".equalsIgnoreCase(MimeTypeMap.getFileExtensionFromUrl(url))) { + makeRestCall(RestProvider.getRepoService(isEnterprise()).getFileAsStream(url), + s -> sendToView(view -> view.onSetImageUrl(s, true))); + return; + } + sendToView(view -> view.onSetImageUrl(url, false)); return; } Observable streamObservable = MarkDownProvider.isMarkdown(url) diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java index bb79ebaf7..940498944 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java @@ -21,6 +21,7 @@ import com.fastaccess.data.dao.model.PullRequest; import com.fastaccess.data.dao.timeline.GenericEvent; import com.fastaccess.data.dao.timeline.SourceModel; +import com.fastaccess.data.dao.types.IssueEventType; import com.fastaccess.data.dao.types.ReactionTypes; import com.fastaccess.helper.ActivityHelper; import com.fastaccess.helper.BundleConstant; @@ -95,6 +96,8 @@ public class PullRequestTimelinePresenter extends BasePresenter loadDataWithBaseURL("file:///android_asset/md/", page, "text/html", "utf-8", null)); } - public void loadImage(@NonNull String url) { + public void loadImage(@NonNull String url, boolean isSvg) { WebSettings settings = getSettings(); settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN); setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); settings.setSupportZoom(true); settings.setBuiltInZoomControls(true); settings.setDisplayZoomControls(false); - String html = ""; + String html; + if (isSvg) { + html = url; + } else { + html = "" + + ""; + } + Logger.e(html); loadData(html, "text/html", null); } diff --git a/app/src/main/res/layouts/main_layouts/layout/add_review_dialog_layout.xml b/app/src/main/res/layouts/main_layouts/layout/add_review_dialog_layout.xml index a37bd8eb0..62cabc425 100644 --- a/app/src/main/res/layouts/main_layouts/layout/add_review_dialog_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/add_review_dialog_layout.xml @@ -1,34 +1,23 @@ - - - - + android:layout_margin="@dimen/spacing_xs_large" + android:entries="@array/review_methods"/> - - - + \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml b/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml index c8c131f20..c982486ce 100644 --- a/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml @@ -17,6 +17,7 @@ android:id="@+id/markdownBtnHolder" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_gravity="bottom" android:background="@drawable/bottom_border" android:orientation="horizontal" android:visibility="gone"> diff --git a/app/src/main/res/layouts/main_layouts/layout/issue_pager_activity.xml b/app/src/main/res/layouts/main_layouts/layout/issue_pager_activity.xml index 24b0fd932..dc553a765 100644 --- a/app/src/main/res/layouts/main_layouts/layout/issue_pager_activity.xml +++ b/app/src/main/res/layouts/main_layouts/layout/issue_pager_activity.xml @@ -8,7 +8,7 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> - + android:layout_height="match_parent" + android:layout_above="@+id/commentFragment"> - - + diff --git a/app/src/main/res/layouts/row_layouts/layout/issue_timeline_row_item.xml b/app/src/main/res/layouts/row_layouts/layout/issue_timeline_row_item.xml index 4d0773a96..75cb6b0d0 100644 --- a/app/src/main/res/layouts/row_layouts/layout/issue_timeline_row_item.xml +++ b/app/src/main/res/layouts/row_layouts/layout/issue_timeline_row_item.xml @@ -64,7 +64,6 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="top" - android:layout_marginEnd="@dimen/avatar_margin_end" android:layout_marginStart="@dimen/avatar_margin"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3425700c..777e1fb0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,7 +122,6 @@ Unlock Unlock Everything Feeds - Loading, please wait… diff --git a/app/src/main/res/values/theme_bluish.xml b/app/src/main/res/values/theme_bluish.xml index 77dcb5d00..4d37c0a3d 100644 --- a/app/src/main/res/values/theme_bluish.xml +++ b/app/src/main/res/values/theme_bluish.xml @@ -154,10 +154,14 @@ @style/CommentBoxDarkBluish #eee @style/TimeLineBackgroundBluish + @color/dark_patch_addition_color + @color/dark_patch_deletion_color + @color/dark_patch_ref_color + @color/bluishDivider + @color/bluishWindowBackground @color/bluish_primary @color/bluish_primary_dark @color/bluish_accent - @color/bluishWindowBackground + + +

FastHub changelog +

+

Version 4.1.0 (Gist Editing, Gif images) +

+

Bugs , Enhancements & new Features (4.1.0) +

+
    +
  • (New) Pinned Repos from GitHub Profile.
  • +
  • (New) Gist Editing.
  • +
  • (New) Add multiple files to gist.
  • +
  • (New) GIF Images in comments.
  • +
  • (New) Comment on issues, Prs, Commits & Gists more faster.
  • +
  • (New) Notification sound picker.
  • +
  • (New) Bulgarian language support thanks to (@petarov)
  • +
  • (New) Franch language support thansk to (@ptt-homme)
  • +
  • (New) Add/Search & Render GitHub emojis.
  • +
  • (New) Clicking on Labels, assignees, milestones will open up search with that specific event.
  • +
  • (New) Rendering SVG images in webview.
  • +
  • (New) Handling search links.
  • +
  • (New) Handling Invitations links.
  • +
  • (Enhancment) FastScroller everywhere
  • +
  • (Enhancment) Clicking on topics will search topics.
  • +
  • (Enhancment) More Markdown support, strikethrough, hr & others.
  • +
  • (Enhancment) Table Rendering in comments.
  • +
  • (Enhancment) Rewrite of the PR & Issue timeline to reuse less API calls and to render more faster.
  • +
  • (Enhancement) PR Reviews sort order.
  • +
  • (Enhancment) Markdown editor everywhere.
  • +
  • (Fix) Added search icon in search places where clicking on search on the keyboard doesn’t work for some devices.
  • +
  • (Fix) Removed Signature + Checkbox & made signature to be disabled by default. thanks to @jakeWharton +
  • +
  • (Fix) Readme Appbar flicker on scroll thanks to (@TheAndroidMaster)
  • +
  • (Fix) Private repos for organizations.
  • +
  • (Fix) Labels pagination.
  • +
  • (Fix) Embading a fork of Firebase job-dispatcher with fixes to avoid crashes happen on some devices.
  • +
+ + + \ No newline at end of file diff --git a/app/src/release/java/com/fastaccess/provider/fabric/FabricProvider.java b/app/src/release/java/com/fastaccess/provider/fabric/FabricProvider.java deleted file mode 100644 index b77fdf120..000000000 --- a/app/src/release/java/com/fastaccess/provider/fabric/FabricProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.fastaccess.provider.fabric; - -import android.content.Context; -import android.support.annotation.NonNull; - -import com.fastaccess.BuildConfig; - -/** - * Created by kosh on 14/08/2017. - */ - -public class FabricProvider { - - public static void initFabric(@NonNull Context context) { - Fabric fabric = new Fabric.Builder(context) - .kits(new Crashlytics.Builder() - .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) - .build()) - .debuggable(BuildConfig.DEBUG) - .build(); - Fabric.with(fabric); - } - - public static void logPurchase(@NonNull String productKey) { - Answers.getInstance().logPurchase(PurchaseEvent().putItemName(productKey).putSuccess(true)); - } -} diff --git a/build.gradle b/build.gradle index 8dee36c27..e03a897db 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext { - taskRequests = getGradle().getStartParameter().getTaskRequests().toString() - isProduction = taskRequests.toLowerCase().contains("release") butterKnifeVersion = '8.5.1' state_version = '1.1.0' lombokVersion = '1.12.6' @@ -28,7 +26,7 @@ buildscript { classpath 'com.google.gms:google-services:3.0.0' classpath 'com.novoda:gradle-build-properties-plugin:0.3' classpath 'com.dicedmelon.gradle:jacoco-android:0.1.2' - if (isProduction) classpath 'io.fabric.tools:gradle:1.22.2' + classpath 'io.fabric.tools:gradle:1.22.2' classpath 'com.apollographql.apollo:gradle-plugin:0.4.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.github.viswaramamoorthy:gradle-util-plugins:0.1.0-RELEASE" diff --git a/debug_gradle.properties b/debug_gradle.properties index 879673feb..1303a8bd5 100644 --- a/debug_gradle.properties +++ b/debug_gradle.properties @@ -1,5 +1,5 @@ # Below API Keys are meant for debugging purpose & they aren't being used in production. -org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx2536M android_store_password=kosh2010 android_key_password=kosh2010 android_key_alias=FastAccess From 9256a924aefbea8cb9da346577f4f5e0f794bd8f Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sun, 27 Aug 2017 15:44:53 +0200 Subject: [PATCH 05/49] fixed cross reference link & build --- app/build.gradle | 2 +- .../provider/timeline/TimelineProvider.java | 10 +++--- .../adapter/viewholder/ReviewsViewHolder.kt | 6 ++-- .../ui/modules/main/MainActivity.java | 7 ++++ .../fastaccess/ui/modules/main/MainMvp.java | 2 ++ .../ui/modules/main/MainPresenter.java | 32 +++++++++++++++++++ .../timeline/IssueTimelinePresenter.java | 10 +++--- .../PullRequestTimelinePresenter.java | 10 +++--- 8 files changed, 62 insertions(+), 17 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 84ccc46fb..a70280ad7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,7 +19,7 @@ android { signing { keyAlias((buildProperties.secrets['android_key_alias'] | buildProperties.notThere['android_key_alias']).string) keyPassword((buildProperties.secrets['android_store_password'] | buildProperties.notThere['android_store_password']).string) - storeFile file('fastaccess-key') + storeFile file('fastaccess-public') storePassword((buildProperties.secrets['android_store_password'] | buildProperties.notThere['android_store_password']).string) } } diff --git a/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java b/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java index 9b93c6ccc..b3e5cae53 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java @@ -133,11 +133,13 @@ public class TimelineProvider { } else if (event == IssueEventType.cross_referenced) { SourceModel sourceModel = issueEventModel.getSource(); if (sourceModel != null) { + String type = sourceModel.getType(); SpannableBuilder title = SpannableBuilder.builder(); - if (sourceModel.getIssue() != null) { + if (sourceModel.getPullRequest() != null) { + if (sourceModel.getIssue() != null) title.url("#" + sourceModel.getIssue().getNumber()); + type = "pull request"; + } else if (sourceModel.getIssue() != null) { title.url("#" + sourceModel.getIssue().getNumber()); - } else if (sourceModel.getPullRequest() != null) { - title.url("#" + sourceModel.getPullRequest().getNumber()); } else if (sourceModel.getCommit() != null) { title.url(substring(sourceModel.getCommit().getSha())); } else if (sourceModel.getRepository() != null) { @@ -147,7 +149,7 @@ public class TimelineProvider { spannableBuilder.append(" ") .append(thisString) .append(" in ") - .append(sourceModel.getType()) + .append(type) .append(" ") .append(title); } diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewsViewHolder.kt b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewsViewHolder.kt index 79d18243e..8afc043ba 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewsViewHolder.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewsViewHolder.kt @@ -21,7 +21,8 @@ import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder class ReviewsViewHolder private constructor(itemView: View, adapter: BaseRecyclerAdapter<*, *, *>?, - val viewGroup: ViewGroup) : BaseViewHolder(itemView, adapter) { + val viewGroup: ViewGroup) + : BaseViewHolder(itemView, adapter) { @BindView(R.id.stateImage) lateinit var stateImage: ForegroundImageView @BindView(R.id.avatarLayout) lateinit var avatarLayout: AvatarLayout @@ -42,7 +43,8 @@ class ReviewsViewHolder private constructor(itemView: View, it.user.login } else { "" - }).append(" ${if (model.event == IssueEventType.reviewed) "reviewed" else "requested changes"} ").append(ParseDateFormat.getTimeAgo(it.submittedAt)) + }).append(" ${if (model.event == IssueEventType.reviewed) "reviewed" else "requested changes"} ") + .append(ParseDateFormat.getTimeAgo(it.submittedAt)) if (!it.bodyHtml.isNullOrBlank()) { HtmlHelper.htmlIntoTextView(body, it.bodyHtml, viewGroup.width) body.visibility = View.VISIBLE diff --git a/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java b/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java index e6da5aec2..e3b7f20eb 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/main/MainActivity.java @@ -9,8 +9,10 @@ import android.support.v4.view.GravityCompat; import android.view.Menu; import android.view.MenuItem; +import android.widget.Toast; import com.evernote.android.state.State; +import com.fastaccess.App; import com.fastaccess.R; import com.fastaccess.data.dao.model.Login; import com.fastaccess.data.dao.model.Notification; @@ -140,6 +142,11 @@ public class MainActivity extends BaseActivity impl invalidateOptionsMenu(); } + @Override public void onUserIsBlackListed() { + Toast.makeText(App.getInstance(), "You are blacklisted, please contact the dev", Toast.LENGTH_LONG).show(); + finish(); + } + @Shortcut(id = "myIssues", icon = R.drawable.ic_app_shortcut_issues, shortLabelRes = R.string.issues, rank = 2, action = "myIssues") public void myIssues() {}//do nothing diff --git a/app/src/main/java/com/fastaccess/ui/modules/main/MainMvp.java b/app/src/main/java/com/fastaccess/ui/modules/main/MainMvp.java index f9be72747..50b04eea6 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/main/MainMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/main/MainMvp.java @@ -41,6 +41,8 @@ interface View extends BaseMvp.FAView { void onOpenProfile(); void onInvalidateNotification(); + + void onUserIsBlackListed(); } interface Presenter extends BaseMvp.FAPresenter, diff --git a/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java index 7f7d7b0c4..5dd36dc45 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/main/MainPresenter.java @@ -10,6 +10,7 @@ import com.fastaccess.R; import com.fastaccess.data.dao.model.Login; import com.fastaccess.data.dao.model.Notification; +import com.fastaccess.helper.Logger; import com.fastaccess.helper.ParseDateFormat; import com.fastaccess.helper.PrefGetter; import com.fastaccess.helper.RxHelper; @@ -18,6 +19,12 @@ import com.fastaccess.ui.modules.feeds.FeedsFragment; import com.fastaccess.ui.modules.main.issues.pager.MyIssuesPagerFragment; import com.fastaccess.ui.modules.main.pullrequests.pager.MyPullsPagerFragment; +import com.github.b3er.rxfirebase.database.RxFirebaseDatabase; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.GenericTypeIndicator; + +import java.util.ArrayList; +import java.util.List; import io.reactivex.Single; @@ -31,6 +38,7 @@ public class MainPresenter extends BasePresenter implements MainMvp.Presenter { MainPresenter() { + checkBlackListed(); setEnterprise(PrefGetter.isEnterprise()); manageDisposable(RxHelper.getObservable(RestProvider.getUserService(isEnterprise()).getUser()) .flatMap(login -> { @@ -133,4 +141,28 @@ public class MainPresenter extends BasePresenter implements MainMv } @Override public void onMenuItemReselect(@IdRes int id, int position, boolean fromUser) {} + + private void checkBlackListed() { + manageDisposable(RxHelper.getSingle(RxFirebaseDatabase + .data(FirebaseDatabase.getInstance().getReference().child("black_listed"))) + .map(dataSnapshot -> { + boolean exists = false; + Login login = Login.getUser(); + Logger.e(dataSnapshot); + if (login != null) { + if (dataSnapshot != null && dataSnapshot.exists()) { + List values = dataSnapshot.getValue(new GenericTypeIndicator>() {}); + if (values != null && !values.isEmpty()) { + exists = values.contains(Login.getUser().getLogin()); + } + } + } + return exists; + }) + .subscribe(exists -> { + if (exists) { + sendToView(MainMvp.View::onUserIsBlackListed); + } + }, Throwable::printStackTrace)); + } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java index f907237b9..c2f38c8df 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java @@ -97,13 +97,13 @@ SourceModel sourceModel = issueEventModel.getSource(); if (sourceModel != null) { if (sourceModel.getCommit() != null) { - SchemeParser.launchUri(v.getContext(), Uri.parse(sourceModel.getCommit().getUrl())); - } else if (sourceModel.getIssue() != null) { - SchemeParser.launchUri(v.getContext(), Uri.parse(sourceModel.getIssue().getUrl())); + SchemeParser.launchUri(v.getContext(), sourceModel.getCommit().getUrl()); } else if (sourceModel.getPullRequest() != null) { - SchemeParser.launchUri(v.getContext(), Uri.parse(sourceModel.getPullRequest().getUrl())); + SchemeParser.launchUri(v.getContext(), sourceModel.getPullRequest().getUrl()); + } else if (sourceModel.getIssue() != null) { + SchemeParser.launchUri(v.getContext(), sourceModel.getIssue().getHtmlUrl()); } else if (sourceModel.getRepository() != null) { - SchemeParser.launchUri(v.getContext(), Uri.parse(sourceModel.getRepository().getUrl())); + SchemeParser.launchUri(v.getContext(), sourceModel.getRepository().getUrl()); } } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java index 940498944..4cee0cf78 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java @@ -102,13 +102,13 @@ public class PullRequestTimelinePresenter extends BasePresenter Date: Sun, 27 Aug 2017 18:44:02 +0200 Subject: [PATCH 06/49] releasing 4.1.0 --- .../overview/ProfileOverviewFragment.java | 15 +++-- .../layout/profile_overview_layout.xml | 63 +++++++++---------- app/src/main/res/raw/changelog.html | 40 +++++++----- 3 files changed, 63 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java index ccd9e7606..6b84a01df 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java @@ -40,6 +40,7 @@ import com.fastaccess.ui.base.BaseFragment; import com.fastaccess.ui.modules.profile.ProfilePagerMvp; import com.fastaccess.ui.widgets.AvatarLayout; +import com.fastaccess.ui.widgets.FontButton; import com.fastaccess.ui.widgets.FontTextView; import com.fastaccess.ui.widgets.SpannableBuilder; import com.fastaccess.ui.widgets.contributions.GitHubContributionsView; @@ -78,8 +79,8 @@ public class ProfileOverviewFragment extends BaseFragment - + android:orientation="horizontal"> - + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:textAppearance="@style/TextAppearance.AppCompat.Caption" + android:textColor="@color/white" + tools:text="Following (40)"/> + + - - - + diff --git a/app/src/main/res/raw/changelog.html b/app/src/main/res/raw/changelog.html index 5f8779ae3..5ccaf8194 100644 --- a/app/src/main/res/raw/changelog.html +++ b/app/src/main/res/raw/changelog.html @@ -13,34 +13,40 @@

Version 4.1.0 (Gist Editin

Bugs , Enhancements & new Features (4.1.0)

    -
  • (New) Pinned Repos from GitHub Profile.
  • +
  • (New) Added Pinned Repos from GitHub Profile.
  • (New) Gist Editing.
  • -
  • (New) Add multiple files to gist.
  • +
  • (New) Allow uploading multiple files to a gist.
  • (New) GIF Images in comments.
  • -
  • (New) Comment on issues, Prs, Commits & Gists more faster.
  • -
  • (New) Notification sound picker.
  • +
  • (New) Added quick commenting in Issues, Prs, Commits & Gists.
  • +
  • (New) Added a custom ringtone for notifications.
  • (New) Bulgarian language support thanks to (@petarov)
  • -
  • (New) Franch language support thansk to (@ptt-homme)
  • +
  • (New) French language support thanks to (@ptt-homme)
  • (New) Add/Search & Render GitHub emojis.
  • (New) Clicking on Labels, assignees, milestones will open up search with that specific event.
  • (New) Rendering SVG images in webview.
  • -
  • (New) Handling search links.
  • -
  • (New) Handling Invitations links.
  • -
  • (Enhancment) FastScroller everywhere
  • -
  • (Enhancment) Clicking on topics will search topics.
  • -
  • (Enhancment) More Markdown support, strikethrough, hr & others.
  • -
  • (Enhancment) Table Rendering in comments.
  • -
  • (Enhancment) Rewrite of the PR & Issue timeline to reuse less API calls and to render more faster.
  • +
  • (Enhancement) Source files are now shown.
  • +
  • (Enhancement) Confirm to mark all Notifications as read
  • +
  • (Enhancement) Handling search and Invitations links.
  • +
  • (Enhancement) Comments on Pr commits are now shown
  • +
  • (Enhancement) FastScroller everywhere
  • +
  • (Enhancement) Clicking on topics will search topics.
  • +
  • (Enhancement) More Markdown support, strikethrough, hr & others.
  • +
  • (Enhancement) Table Rendering in comments.
  • +
  • (Enhancement) Rewrite of the PR & Issue timeline to use less API calls (will load much faster).
  • (Enhancement) PR Reviews sort order.
  • -
  • (Enhancment) Markdown editor everywhere.
  • +
  • (Enhancement) Markdown editor everywhere.
  • +
  • (Enhancement) Correct wording to match github.
  • +
  • (Enhancement) Better handling of cross-ref events
  • (Fix) Added search icon in search places where clicking on search on the keyboard doesn’t work for some devices.
  • +
  • (Fix) Readme Appbar flicker on scroll thanks to (@TheAndroidMaster)
  • +
  • (Fix) Reload after closing an issue
  • +
  • (Fix) Private repos for organizations.
  • +
  • (Fix) Labels being removed it some cases.
  • +
  • (Fix) Embedding a fork of Firebase job-dispatcher with fixes to avoid crashes happen on some devices.
  • (Fix) Removed Signature Checkbox & made signature to be disabled by default. thanks to @jakeWharton
  • -
  • (Fix) Readme Appbar flicker on scroll thanks to (@TheAndroidMaster)
  • -
  • (Fix) Private repos for organizations.
  • -
  • (Fix) Labels pagination.
  • -
  • (Fix) Embading a fork of Firebase job-dispatcher with fixes to avoid crashes happen on some devices.
  • +
  • A lot more features, enhancements & bug fixes.
From 206092076d89328a43882569822a2970fd0d6a27 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sun, 27 Aug 2017 19:01:57 +0200 Subject: [PATCH 07/49] reverted to slack link and released 4.1.0 --- .github/CONTRIBUTING.md | 2 +- .github/ISSUE_TEMPLATE.md | 3 +-- README.md | 2 +- .../com/fastaccess/ui/modules/about/FastHubAboutActivity.java | 2 +- .../fastaccess/ui/modules/settings/SlackBottomSheetDialog.java | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 11afebe6c..2b827acfd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -8,7 +8,7 @@ # How to contribute & build *FastHub* -If you have a question in mind, feel free to come our public [Slack](http://rebrand.ly/fasthub-slack) channel. +If you have a question in mind, feel free to come our public [Slack](http://rebrand.ly/fasthub) channel. ### Optional diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 661d44b30..15ff17a89 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,8 +4,7 @@ - Make sure that you are always on the latest version. - Search issue before submitting a new one. - Public Slack channel: https://rebrand.ly/fasthub-slack - Discord: https://discord.gg/V6afZWf + Public Slack channel: https://rebrand.ly/fasthub #### How to submit Issue/Feature Request to *FastHub* - Make sure the included template is filled ( using FastHub will fill them up automatically ). diff --git a/README.md b/README.md index 0465ccc87..8b8f51fa6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Build Status](https://travis-ci.org/k0shk0sh/FastHub.svg?branch=master)](https://travis-ci.org/k0shk0sh/FastHub) [![Build status](https://ci.appveyor.com/api/projects/status/2yhxx7hu6hju24bk?svg=true)](https://ci.appveyor.com/project/k0shk0sh/fasthub) -[![Releases](https://img.shields.io/github/release/k0shk0sh/FastHub.svg)](https://github.com/k0shk0sh/FastHub/releases/latest) [![Discord](https://img.shields.io/badge/chat-discord-7289DA.svg)](https://discord.gg/V6afZWf) +[![Releases](https://img.shields.io/github/release/k0shk0sh/FastHub.svg)](https://github.com/k0shk0sh/FastHub/releases/latest) [![Slack](https://img.shields.io/badge/slack-join-e01563.svg)](http://rebrand.ly/fasthub) ![Logo](/.github/assets/feature_graphic.png?raw=true "Logo") diff --git a/app/src/main/java/com/fastaccess/ui/modules/about/FastHubAboutActivity.java b/app/src/main/java/com/fastaccess/ui/modules/about/FastHubAboutActivity.java index 1de10fdee..9b4f7fdf7 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/about/FastHubAboutActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/about/FastHubAboutActivity.java @@ -146,7 +146,7 @@ private void buildMisc(Context context, MaterialAboutCard.Builder miscCardBuilde .addItem(new MaterialAboutActionItem.Builder() .text(R.string.join_slack) .icon(ContextCompat.getDrawable(context, R.drawable.ic_slack)) - .setOnClickAction(() -> ActivityHelper.startCustomTab(this, "http://rebrand.ly/fasthub-slack")) + .setOnClickAction(() -> ActivityHelper.startCustomTab(this, "http://rebrand.ly/fasthub")) .build()) .addItem(new MaterialAboutActionItem.Builder() .text(R.string.open_source_libs) diff --git a/app/src/main/java/com/fastaccess/ui/modules/settings/SlackBottomSheetDialog.java b/app/src/main/java/com/fastaccess/ui/modules/settings/SlackBottomSheetDialog.java index 91cf9aed5..e999d0ca4 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/settings/SlackBottomSheetDialog.java +++ b/app/src/main/java/com/fastaccess/ui/modules/settings/SlackBottomSheetDialog.java @@ -51,7 +51,7 @@ public interface SlackDialogListener { @OnClick({R.id.cancel, R.id.ok}) public void onViewClicked(View view) { switch (view.getId()) { case R.id.ok: - ActivityHelper.startCustomTab(getActivity(), "http://rebrand.ly/fasthub-slack"); + ActivityHelper.startCustomTab(getActivity(), "http://rebrand.ly/fasthub"); break; } if (listener != null) listener.onDismissed(); From ef07304f716c31a7870d1c6996db651dab7c15a3 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Mon, 28 Aug 2017 17:58:51 +0200 Subject: [PATCH 08/49] this commit fixes #895 and some crashes from fabirc --- .../provider/fcm/PushNotificationService.java | 34 ++++++++++--------- .../NotificationSchedulerJobTask.java | 24 ++----------- .../tasks/version/CheckVersionService.kt | 3 +- .../com/fastaccess/ui/adapter/EmojiAdapter.kt | 4 ++- .../popup/EditorLinkImagePresenter.java | 2 +- .../issues/fragment/FilterIssueFragment.java | 18 ++-------- .../layout/review_changes_bottom_layout.xml | 4 +-- 7 files changed, 30 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/fastaccess/provider/fcm/PushNotificationService.java b/app/src/main/java/com/fastaccess/provider/fcm/PushNotificationService.java index 70aa6b0b3..3e13f97e5 100644 --- a/app/src/main/java/com/fastaccess/provider/fcm/PushNotificationService.java +++ b/app/src/main/java/com/fastaccess/provider/fcm/PushNotificationService.java @@ -19,22 +19,24 @@ public class PushNotificationService extends FirebaseMessagingService { @Override public void onMessageReceived(RemoteMessage remoteMessage) { super.onMessageReceived(remoteMessage); - String title = remoteMessage.getNotification().getTitle(); - String body = remoteMessage.getNotification().getBody(); - if (remoteMessage.getData() != null && !remoteMessage.getData().isEmpty()) { - title = title == null ? remoteMessage.getData().get("title") : title; - body = body == null ? remoteMessage.getData().get("message") : body; + if (remoteMessage != null && remoteMessage.getNotification() != null) { + String title = remoteMessage.getNotification().getTitle(); + String body = remoteMessage.getNotification().getBody(); + if (remoteMessage.getData() != null && !remoteMessage.getData().isEmpty()) { + title = title == null ? remoteMessage.getData().get("title") : title; + body = body == null ? remoteMessage.getData().get("message") : body; + } + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .setContentIntent(pendingIntent); + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(1, notificationBuilder.build()); } - Intent intent = new Intent(this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT); - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(title) - .setContentText(body) - .setAutoCancel(true) - .setContentIntent(pendingIntent); - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(1, notificationBuilder.build()); } } diff --git a/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java b/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java index aea0a71cf..006afb244 100644 --- a/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java +++ b/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java @@ -13,9 +13,6 @@ import android.support.v4.content.ContextCompat; import com.annimon.stream.Stream; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.animation.GlideAnimation; -import com.bumptech.glide.request.target.SimpleTarget; import com.fastaccess.R; import com.fastaccess.data.dao.model.Comment; import com.fastaccess.data.dao.model.Login; @@ -191,16 +188,7 @@ private void finishJob(JobParameters job) { } private void showNotificationWithoutComment(Context context, int accentColor, Notification thread, String iconUrl) { - if (!InputHelper.isEmpty(iconUrl)) { - withoutComments(null, thread, context, accentColor); - } else { - Glide.with(context).load(iconUrl).asBitmap() - .into(new SimpleTarget() { - @Override public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { - withoutComments(resource, thread, context, accentColor); - } - }); - } + withoutComments(null, thread, context, accentColor); } private void withoutComments(Bitmap bitmap, Notification thread, Context context, int accentColor) { @@ -220,15 +208,7 @@ private void withoutComments(Bitmap bitmap, Notification thread, Context context } private void getNotificationWithComment(Context context, int accentColor, Notification thread, Comment comment, String url) { - if (!InputHelper.isEmpty(url)) { - Glide.with(context).load(url).asBitmap().into(new SimpleTarget() { - @Override public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { - withComments(resource, comment, context, thread, accentColor); - } - }); - } else { - withComments(null, comment, context, thread, accentColor); - } + withComments(null, comment, context, thread, accentColor); } private void withComments(Bitmap bitmap, Comment comment, Context context, Notification thread, int accentColor) { diff --git a/app/src/main/java/com/fastaccess/provider/tasks/version/CheckVersionService.kt b/app/src/main/java/com/fastaccess/provider/tasks/version/CheckVersionService.kt index 4f28bd14b..a40a753bc 100644 --- a/app/src/main/java/com/fastaccess/provider/tasks/version/CheckVersionService.kt +++ b/app/src/main/java/com/fastaccess/provider/tasks/version/CheckVersionService.kt @@ -3,6 +3,7 @@ package com.fastaccess.provider.tasks.version import android.app.IntentService import android.content.Intent import android.widget.Toast +import com.fastaccess.App import com.fastaccess.BuildConfig import com.fastaccess.R import com.fastaccess.data.dao.model.Release @@ -19,7 +20,7 @@ class CheckVersionService : IntentService("CheckVersionService") { .getLatestRelease("k0shk0sh", "FastHub")) .subscribe({ t: Release? -> t?.let { - Toast.makeText(this, if (BuildConfig.VERSION_NAME.contains(it.tagName)) + Toast.makeText(App.getInstance(), if (BuildConfig.VERSION_NAME.contains(it.tagName)) R.string.up_to_date else R.string.new_version, Toast.LENGTH_LONG).show() } }, { throwable -> throwable.printStackTrace() }) diff --git a/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt b/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt index d1041c515..4d69efd5c 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt @@ -48,7 +48,9 @@ class EmojiAdapter(listener: BaseViewHolder.OnItemClickListener) } override fun publishResults(var1: CharSequence, results: Filter.FilterResults) { - insertItems(results.values as List) + results.values?.let { + insertItems(it as List) + } } } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImagePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImagePresenter.java index 532f13f91..303ef6ee6 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImagePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/popup/EditorLinkImagePresenter.java @@ -28,7 +28,7 @@ public class EditorLinkImagePresenter extends BasePresenter view.onUploaded(null, null)); - }); + }, false); } else { if (getView() != null) getView().onUploaded(null, null); } diff --git a/app/src/main/java/com/fastaccess/ui/modules/filter/issues/fragment/FilterIssueFragment.java b/app/src/main/java/com/fastaccess/ui/modules/filter/issues/fragment/FilterIssueFragment.java index 3881656e3..dbbac5130 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/filter/issues/fragment/FilterIssueFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/filter/issues/fragment/FilterIssueFragment.java @@ -10,17 +10,15 @@ import com.evernote.android.state.State; import com.fastaccess.R; -import com.fastaccess.data.dao.PullsIssuesParser; import com.fastaccess.data.dao.model.Issue; import com.fastaccess.data.dao.types.IssueState; import com.fastaccess.helper.InputHelper; import com.fastaccess.provider.rest.loadmore.OnLoadMore; +import com.fastaccess.provider.scheme.SchemeParser; import com.fastaccess.ui.adapter.IssuesAdapter; import com.fastaccess.ui.base.BaseFragment; import com.fastaccess.ui.modules.filter.issues.FilterIssuesActivityMvp; import com.fastaccess.ui.modules.repos.extras.popup.IssuePopupFragment; -import com.fastaccess.ui.modules.repos.issues.issue.details.IssuePagerActivity; -import com.fastaccess.ui.modules.repos.pull_requests.pull_request.details.PullRequestPagerActivity; import com.fastaccess.ui.widgets.StateLayout; import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView; import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller; @@ -132,19 +130,7 @@ public class FilterIssueFragment extends BaseFragment Date: Mon, 28 Aug 2017 18:09:46 +0200 Subject: [PATCH 09/49] this commit fixes #773 --- .../repos/code/commit/details/CommitPagerActivity.java | 8 ++++---- app/src/main/res/menu/share_menu.xml | 8 ++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java index 0110f1628..79ccfc1c9 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java @@ -137,6 +137,7 @@ public static void createIntentForOffline(@NonNull Context context, @NonNull Com getMenuInflater().inflate(R.menu.share_menu, menu); menu.findItem(R.id.browser).setVisible(true).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); menu.findItem(R.id.copyUrl).setVisible(true).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.findItem(R.id.copySha).setVisible(true).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); menu.findItem(R.id.share).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); return super.onCreateOptionsMenu(menu); } @@ -154,14 +155,13 @@ public static void createIntentForOffline(@NonNull Context context, @NonNull Com } else if (item.getItemId() == R.id.copyUrl) { if (getPresenter().getCommit() != null) AppHelper.copyToClipboard(this, getPresenter().getCommit().getHtmlUrl()); return true; + } else if (item.getItemId() == R.id.copySha) { + if (getPresenter().getCommit() != null) AppHelper.copyToClipboard(this, getPresenter().getCommit().getSha()); + return true; } return super.onOptionsItemSelected(item); } - @Override public boolean onPrepareOptionsMenu(Menu menu) { - return super.onPrepareOptionsMenu(menu); - } - @Override public void onSetup() { hideProgress(); if (getPresenter().getCommit() == null) { diff --git a/app/src/main/res/menu/share_menu.xml b/app/src/main/res/menu/share_menu.xml index b66cc86da..8e4179463 100644 --- a/app/src/main/res/menu/share_menu.xml +++ b/app/src/main/res/menu/share_menu.xml @@ -22,4 +22,12 @@ android:visible="false" app:showAsAction="never"/> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 777e1fb0a..873974bbe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -555,4 +555,5 @@ Edit Gist Content expand + Copy SHA From ac3516b2ce848d71640efe8a92f02e317e15d35d Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Mon, 28 Aug 2017 18:29:44 +0200 Subject: [PATCH 10/49] this commit fixes #754 and fixes #738 --- .../ui/modules/code/CodeViewerActivity.java | 8 +- .../repos/code/prettifier/ViewerFragment.java | 4 + .../repos/code/prettifier/ViewerMvp.java | 4 + .../code/prettifier/ViewerPresenter.java | 12 +++ .../files/PullRequestFilesFragment.java | 30 +++++- .../layout/pull_request_files_layout.xml | 96 +++++++++++++++++++ .../main/res/menu/download_browser_menu.xml | 4 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/layouts/main_layouts/layout/pull_request_files_layout.xml diff --git a/app/src/main/java/com/fastaccess/ui/modules/code/CodeViewerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/code/CodeViewerActivity.java index c7b96073a..218a1741c 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/code/CodeViewerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/code/CodeViewerActivity.java @@ -102,7 +102,13 @@ public static Intent createIntent(@NonNull Context context, @NonNull String url, @Override public boolean onOptionsItemSelected(MenuItem item) { if (InputHelper.isEmpty(url)) return super.onOptionsItemSelected(item); - if (item.getItemId() == R.id.download) { + if (item.getItemId() == R.id.viewAsCode) { + ViewerFragment viewerFragment = (ViewerFragment) AppHelper.getFragmentByTag(getSupportFragmentManager(), ViewerFragment.TAG); + if (viewerFragment != null) { + viewerFragment.onViewAsCode(); + } + return true; + } else if (item.getItemId() == R.id.download) { if (ActivityHelper.checkAndRequestReadWritePermission(this)) { RestProvider.downloadFile(this, url); } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java index 121aea971..96f78085a 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerFragment.java @@ -109,6 +109,10 @@ private static ViewerFragment newInstance(@NonNull Bundle bundle) { ActivityHelper.startCustomTab(getActivity(), url); } + @Override public void onViewAsCode() { + getPresenter().onLoadContentAsStream(); + } + @Override public void showProgress(@StringRes int resId) { onShowMdProgress(); } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java index 7f264b717..d0662e1d2 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerMvp.java @@ -29,12 +29,16 @@ interface View extends BaseMvp.FAView, PrettifyWebView.OnContentChangedListener void onShowMdProgress(); void openUrl(@NonNull String url); + + void onViewAsCode(); } interface Presenter extends BaseMvp.FAPresenter { void onHandleIntent(@Nullable Bundle intent); + void onLoadContentAsStream(); + String downloadedStream(); boolean isMarkDown(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java index ec1437e05..573659e51 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/prettifier/ViewerPresenter.java @@ -69,6 +69,18 @@ class ViewerPresenter extends BasePresenter implements ViewerMvp } } + @Override public void onLoadContentAsStream() { + boolean isImage = MarkDownProvider.isImage(url) && !"svg".equalsIgnoreCase(MimeTypeMap.getFileExtensionFromUrl(url)); + if (isImage || MarkDownProvider.isArchive(url)) { + return; + } + makeRestCall(RestProvider.getRepoService(isEnterprise()).getFileAsStream(url), + body -> { + downloadedStream = body; + sendToView(view -> view.onSetCode(body)); + }); + } + @Override public String downloadedStream() { return downloadedStream; } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java index cd5622b00..a864e2d16 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java @@ -14,6 +14,7 @@ import com.fastaccess.data.dao.CommitFileChanges; import com.fastaccess.data.dao.CommitFileModel; import com.fastaccess.data.dao.CommitLinesModel; +import com.fastaccess.data.dao.model.PullRequest; import com.fastaccess.helper.ActivityHelper; import com.fastaccess.helper.BundleConstant; import com.fastaccess.helper.Bundler; @@ -22,7 +23,9 @@ import com.fastaccess.ui.adapter.CommitFilesAdapter; import com.fastaccess.ui.base.BaseFragment; import com.fastaccess.ui.modules.main.premium.PremiumActivity; +import com.fastaccess.ui.modules.repos.issues.issue.details.IssuePagerMvp; import com.fastaccess.ui.modules.reviews.AddReviewDialogFragment; +import com.fastaccess.ui.widgets.FontTextView; import com.fastaccess.ui.widgets.StateLayout; import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView; import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller; @@ -45,10 +48,14 @@ public class PullRequestFilesFragment extends BaseFragment toggleMap = new LinkedHashMap<>(); + @BindView(R.id.changes) FontTextView changes; + @BindView(R.id.addition) FontTextView addition; + @BindView(R.id.deletion) FontTextView deletion; private PullRequestFilesMvp.PatchCallback viewCallback; private OnLoadMore onLoadMore; private CommitFilesAdapter adapter; + private IssuePagerMvp.IssuePrCallback issueCallback; public static PullRequestFilesFragment newInstance(@NonNull String repoId, @NonNull String login, long number) { PullRequestFilesFragment view = new PullRequestFilesFragment(); @@ -60,8 +67,16 @@ public static PullRequestFilesFragment newInstance(@NonNull String repoId, @NonN return view; } - @Override public void onAttach(Context context) { + @SuppressWarnings("unchecked") @Override public void onAttach(Context context) { super.onAttach(context); + if (getParentFragment() instanceof IssuePagerMvp.IssuePrCallback) { + issueCallback = (IssuePagerMvp.IssuePrCallback) getParentFragment(); + } else if (context instanceof IssuePagerMvp.IssuePrCallback) { + issueCallback = (IssuePagerMvp.IssuePrCallback) context; + } else { + throw new IllegalArgumentException(String.format("%s or parent fragment must implement IssuePagerMvp.IssuePrCallback", context.getClass() + .getSimpleName())); + } if (getParentFragment() instanceof PullRequestFilesMvp.PatchCallback) { viewCallback = (PullRequestFilesMvp.PatchCallback) getParentFragment(); } else if (context instanceof PullRequestFilesMvp.PatchCallback) { @@ -70,6 +85,7 @@ public static PullRequestFilesFragment newInstance(@NonNull String repoId, @NonN } @Override public void onDetach() { + issueCallback = null; viewCallback = null; super.onDetach(); } @@ -88,13 +104,14 @@ public static PullRequestFilesFragment newInstance(@NonNull String repoId, @NonN } @Override protected int fragmentLayout() { - return R.layout.micro_grid_refresh_list; + return R.layout.pull_request_files_layout; } @Override protected void onFragmentCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { if (getArguments() == null) { throw new NullPointerException("Bundle is null, therefore, PullRequestFilesFragment can't be proceeded."); } + setupChanges(); stateLayout.setEmptyText(R.string.no_commits); stateLayout.setOnReloadListener(this); refresh.setOnRefreshListener(this); @@ -112,6 +129,15 @@ public static PullRequestFilesFragment newInstance(@NonNull String repoId, @NonN fastScroller.attachRecyclerView(recycler); } + private void setupChanges() { + PullRequest pullRequest = issueCallback.getData(); + if (pullRequest != null) { + addition.setText(String.valueOf(pullRequest.getAdditions())); + deletion.setText(String.valueOf(pullRequest.getDeletions())); + changes.setText(String.valueOf(pullRequest.getChangedFiles())); + } + } + @NonNull @Override public PullRequestFilesPresenter providePresenter() { return new PullRequestFilesPresenter(); } diff --git a/app/src/main/res/layouts/main_layouts/layout/pull_request_files_layout.xml b/app/src/main/res/layouts/main_layouts/layout/pull_request_files_layout.xml new file mode 100644 index 000000000..de055b8e0 --- /dev/null +++ b/app/src/main/res/layouts/main_layouts/layout/pull_request_files_layout.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/download_browser_menu.xml b/app/src/main/res/menu/download_browser_menu.xml index f5224de12..1c6b5db7b 100644 --- a/app/src/main/res/menu/download_browser_menu.xml +++ b/app/src/main/res/menu/download_browser_menu.xml @@ -3,6 +3,10 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + Content
expand Copy SHA + View as code From 9b6447cf3fac6149139ad27ddef81dfa02036874 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Tue, 29 Aug 2017 20:27:01 +0200 Subject: [PATCH 11/49] in process of editing files (making commits from app) --- app/src/main/AndroidManifest.xml | 5 + .../fastaccess/data/service/ContentService.kt | 20 +++ .../com/fastaccess/ui/base/MainNavDrawer.kt | 3 +- .../ui/modules/editor/EditorActivity.kt | 23 +-- .../repos/code/files/RepoFilesFragment.java | 12 ++ .../modules/repos/git/EditRepoFileActivity.kt | 140 ++++++++++++++++++ .../ui/modules/repos/git/EditRepoFileMvp.kt | 23 +++ .../repos/git/EditRepoFilePresenter.kt | 43 ++++++ .../files/PullRequestFilesFragment.java | 6 +- .../layout/edit_repo_file_layout.xml | 52 +++++++ .../row_layouts/layout/file_path_row_item.xml | 8 +- app/src/main/res/menu/download_share_menu.xml | 6 + app/src/main/res/menu/drawer_menu.xml | 5 +- 13 files changed, 324 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/fastaccess/data/service/ContentService.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileActivity.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileMvp.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt create mode 100644 app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d222417b9..ed531fce6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -232,6 +232,11 @@ android:configChanges="keyboard|orientation|screenSize" android:screenOrientation="portrait" android:windowSoftInputMode="stateAlwaysHidden"/> + +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/base/MainNavDrawer.kt b/app/src/main/java/com/fastaccess/ui/base/MainNavDrawer.kt index 4b4c3470a..03cb104eb 100644 --- a/app/src/main/java/com/fastaccess/ui/base/MainNavDrawer.kt +++ b/app/src/main/java/com/fastaccess/ui/base/MainNavDrawer.kt @@ -11,7 +11,6 @@ import android.widget.TextView import com.fastaccess.R import com.fastaccess.data.dao.model.Login import com.fastaccess.data.dao.model.PinnedRepos -import com.fastaccess.helper.Logger import com.fastaccess.helper.PrefGetter import com.fastaccess.helper.RxHelper import com.fastaccess.provider.scheme.SchemeParser @@ -24,6 +23,7 @@ import com.fastaccess.ui.modules.main.MainActivity import com.fastaccess.ui.modules.main.premium.PremiumActivity import com.fastaccess.ui.modules.notification.NotificationActivity import com.fastaccess.ui.modules.pinned.PinnedReposActivity +import com.fastaccess.ui.modules.repos.issues.create.CreateIssueActivity import com.fastaccess.ui.modules.trending.TrendingActivity import com.fastaccess.ui.modules.user.UserPagerActivity import com.fastaccess.ui.widgets.AvatarLayout @@ -180,6 +180,7 @@ class MainNavDrawer(val view: BaseActivity<*, *>, private val extraNav: Navigati item.itemId == R.id.orgs -> view.onOpenOrgsDialog() item.itemId == R.id.notifications -> view.startActivity(Intent(view, NotificationActivity::class.java)) item.itemId == R.id.trending -> view.startActivity(Intent(view, TrendingActivity::class.java)) + item.itemId == R.id.reportBug -> view.startActivity(CreateIssueActivity.startForResult(view)) } } }, 250) diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt index dd300dc25..37d0e14d6 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt @@ -23,7 +23,6 @@ import com.fastaccess.data.dao.EditReviewCommentModel import com.fastaccess.data.dao.model.Comment import com.fastaccess.helper.* import com.fastaccess.provider.emoji.Emoji -import com.fastaccess.provider.markdown.CachedComments import com.fastaccess.provider.markdown.MarkDownProvider import com.fastaccess.ui.base.BaseActivity import com.fastaccess.ui.widgets.FontTextView @@ -51,21 +50,13 @@ class EditorActivity : BaseActivity(), EditorMv @BindView(R.id.parentView) lateinit var parentView: View @BindView(R.id.autocomplete) lateinit var mention: ListView - @State - @BundleConstant.ExtraType - var extraType: String? = null - @State - var itemId: String? = null - @State - var login: String? = null - @State - var issueNumber: Int = 0 - @State - var commentId: Long = 0 - @State - var sha: String? = null - @State - var reviewComment: EditReviewCommentModel? = null + @State @BundleConstant.ExtraType var extraType: String? = null + @State var itemId: String? = null + @State var login: String? = null + @State var issueNumber: Int = 0 + @State var commentId: Long = 0 + @State var sha: String? = null + @State var reviewComment: EditReviewCommentModel? = null override fun layout(): Int = R.layout.editor_layout diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java index c3bdc61ff..6a1d1306b 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java @@ -9,6 +9,7 @@ import android.widget.PopupMenu; import com.fastaccess.R; +import com.fastaccess.data.dao.model.Login; import com.fastaccess.data.dao.model.RepoFile; import com.fastaccess.data.dao.types.FilesType; import com.fastaccess.helper.ActivityHelper; @@ -24,6 +25,7 @@ import com.fastaccess.ui.modules.code.CodeViewerActivity; import com.fastaccess.ui.modules.repos.code.files.activity.RepoFilesActivity; import com.fastaccess.ui.modules.repos.code.files.paths.RepoFilePathFragment; +import com.fastaccess.ui.modules.repos.git.EditRepoFileActivity; import com.fastaccess.ui.widgets.AppbarRefreshLayout; import com.fastaccess.ui.widgets.StateLayout; import com.fastaccess.ui.widgets.dialog.MessageDialogView; @@ -43,6 +45,7 @@ public class RepoFilesFragment extends BaseFragment { switch (item1.getItemId()) { case R.id.share: @@ -95,6 +103,10 @@ public class RepoFilesFragment extends BaseFragment(), EditRepoFileMvp.View { + + @BindView(R.id.markDownLayout) lateinit var markDownLayout: MarkDownLayout + @BindView(R.id.editText) lateinit var editText: MarkdownEditText + + override fun layout(): Int = R.layout.edit_repo_file_layout + + override fun isTransparent(): Boolean = false + + override fun canBack(): Boolean = true + + override fun isSecured(): Boolean = true + + override fun providePresenter(): EditRepoFilePresenter = EditRepoFilePresenter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + markDownLayout.markdownListener = this + setToolbarIcon(R.drawable.ic_clear) + if (savedInstanceState == null) { + presenter.onInit(intent) + } + val path = presenter.path + if (!path.isNullOrBlank()) { + title = Uri.parse(path)?.lastPathSegment + toolbar?.let { + it.subtitle = "${presenter.login}/${presenter.repoId}" + } + } + invalidateOptionsMenu() + } + + override fun onSetText(content: String?) { + hideProgress() + editText.setText(content) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.done_menu, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.submit) { + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + if (menu.findItem(R.id.submit) != null) { + menu.findItem(R.id.submit).isEnabled = true + } + presenter.isEdit?.let { + menu.findItem(R.id.submit).setIcon(R.drawable.ic_done) + } + return super.onPrepareOptionsMenu(menu) + } + + override fun onAppendLink(title: String?, link: String?, isLink: Boolean) { + if (isLink) { + MarkDownProvider.addLink(editText, InputHelper.toString(title), InputHelper.toString(link)) + } else { + editText.setText(String.format("%s\n", editText.text)) + MarkDownProvider.addPhoto(editText, InputHelper.toString(title), InputHelper.toString(link)) + } + } + + override fun getEditText(): EditText = editText + + override fun getSavedText(): CharSequence? = editText.savedText + + override fun fragmentManager(): FragmentManager = supportFragmentManager + + @SuppressLint("SetTextI18n") + override fun onEmojiAdded(emoji: Emoji?) { + markDownLayout.onEmojiAdded(emoji) + } + + companion object { + val EDIT_RQ = 2017 + + fun startForResult(activity: Activity, repoId: String, login: String, + path: String, contentUrl: String, isEdit: Boolean) { + val bundle = Bundler.start() + .put(BundleConstant.ID, repoId) + .put(BundleConstant.EXTRA, login) + .put(BundleConstant.EXTRA_TWO, path) + .put(BundleConstant.EXTRA_THREE, contentUrl) + .put(BundleConstant.EXTRA_TYPE, isEdit) + .put(BundleConstant.IS_ENTERPRISE, LinkParserHelper.isEnterprise(contentUrl)) + .end() + val intent = Intent(activity, EditRepoFileActivity::class.java) + intent.putExtras(bundle) + activity.startActivityForResult(intent, EDIT_RQ) + } + + fun startForResult(activity: Fragment, repoId: String, login: String, + path: String, contentUrl: String, isEdit: Boolean) { + val bundle = Bundler.start() + .put(BundleConstant.ID, repoId) + .put(BundleConstant.EXTRA, login) + .put(BundleConstant.EXTRA_TWO, path) + .put(BundleConstant.EXTRA_THREE, contentUrl) + .put(BundleConstant.EXTRA_TYPE, isEdit) + .put(BundleConstant.IS_ENTERPRISE, LinkParserHelper.isEnterprise(contentUrl)) + .end() + val intent = Intent(activity.context, EditRepoFileActivity::class.java) + intent.putExtras(bundle) + activity.startActivityForResult(intent, EDIT_RQ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileMvp.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileMvp.kt new file mode 100644 index 000000000..b8652491c --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileMvp.kt @@ -0,0 +1,23 @@ +package com.fastaccess.ui.modules.repos.git + +import android.content.Intent +import com.fastaccess.ui.base.mvp.BaseMvp +import com.fastaccess.ui.modules.editor.emoji.EmojiMvp +import com.fastaccess.ui.modules.editor.popup.EditorLinkImageMvp +import com.fastaccess.ui.widgets.markdown.MarkDownLayout + +/** + * Created by kosh on 29/08/2017. + */ +interface EditRepoFileMvp { + + interface View : BaseMvp.FAView, EditorLinkImageMvp.EditorLinkCallback, + MarkDownLayout.MarkdownListener, EmojiMvp.EmojiCallback { + + fun onSetText(content: String?) + } + + interface Presenter { + fun onInit(intent: Intent?) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt new file mode 100644 index 000000000..7a2d22464 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt @@ -0,0 +1,43 @@ +package com.fastaccess.ui.modules.repos.git + +import android.content.Intent +import com.fastaccess.helper.BundleConstant +import com.fastaccess.provider.rest.RestProvider +import com.fastaccess.ui.base.mvp.presenter.BasePresenter + +/** + * Created by kosh on 29/08/2017. + */ +class EditRepoFilePresenter : BasePresenter(), EditRepoFileMvp.Presenter { + + @com.evernote.android.state.State var path: String? = null + @com.evernote.android.state.State var repoId: String? = null + @com.evernote.android.state.State var login: String? = null + @com.evernote.android.state.State var isEdit: Boolean? = null + @com.evernote.android.state.State var contentUrl: String? = null + var downloadedContent: String? = null + + override fun onInit(intent: Intent?) { + if (downloadedContent.isNullOrBlank()) { + intent?.let { + it.extras?.let { + repoId = it.getString(BundleConstant.ID) + login = it.getString(BundleConstant.EXTRA) + path = it.getString(BundleConstant.EXTRA_TWO) + contentUrl = it.getString(BundleConstant.EXTRA_THREE) + isEdit = it.getBoolean(BundleConstant.EXTRA_TYPE) + loadContent() + } + } + } else { + sendToView { it.onSetText(downloadedContent) } + } + } + + private fun loadContent() { + contentUrl?.let { + makeRestCall(RestProvider.getRepoService(isEnterprise) + .getFileAsStream(it), { sendToView({ v -> v.onSetText(it) }) }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java index a864e2d16..f95095b50 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java @@ -197,7 +197,11 @@ private void setupChanges() { @Override public void onPatchClicked(int groupPosition, int childPosition, View v, CommitFileModel commit, CommitLinesModel item) { if (item.getText().startsWith("@@")) return; if (PrefGetter.isProEnabled()) { - AddReviewDialogFragment.Companion.newInstance(item, Bundler.start().put(BundleConstant.ITEM, commit.getFilename()).end()) + AddReviewDialogFragment.Companion.newInstance(item, Bundler.start() + .put(BundleConstant.ITEM, commit.getFilename()) + .put(BundleConstant.EXTRA_TWO, groupPosition) + .put(BundleConstant.EXTRA_THREE, childPosition) + .end()) .show(getChildFragmentManager(), "AddReviewDialogFragment"); } else { PremiumActivity.Companion.startActivity(getContext()); diff --git a/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml new file mode 100644 index 000000000..40730cad5 --- /dev/null +++ b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layouts/row_layouts/layout/file_path_row_item.xml b/app/src/main/res/layouts/row_layouts/layout/file_path_row_item.xml index ae73d7f7c..e25e17472 100644 --- a/app/src/main/res/layouts/row_layouts/layout/file_path_row_item.xml +++ b/app/src/main/res/layouts/row_layouts/layout/file_path_row_item.xml @@ -1,13 +1,15 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/download_share_menu.xml b/app/src/main/res/menu/download_share_menu.xml index 465ea2ac3..5544661ee 100644 --- a/app/src/main/res/menu/download_share_menu.xml +++ b/app/src/main/res/menu/download_share_menu.xml @@ -2,6 +2,12 @@ + - + Date: Wed, 30 Aug 2017 09:39:36 +0200 Subject: [PATCH 12/49] this commit fixes #906 --- .../com/fastaccess/helper/PrefGetter.java | 5 +++- .../ui/base/BaseDialogFragment.java | 24 ++++++++++++------- .../ui/modules/theme/ThemeActivity.kt | 7 +++++- .../dialog/ProgressDialogFragment.java | 4 +++- .../main_layouts/layout/theme_viewpager.xml | 20 +++++++++++++--- app/src/main/res/values/strings.xml | 2 ++ .../main/res/xml/customization_settings.xml | 16 +++++++++---- 7 files changed, 60 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/fastaccess/helper/PrefGetter.java b/app/src/main/java/com/fastaccess/helper/PrefGetter.java index 73cbcb1fb..054a907d5 100644 --- a/app/src/main/java/com/fastaccess/helper/PrefGetter.java +++ b/app/src/main/java/com/fastaccess/helper/PrefGetter.java @@ -14,7 +14,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.net.URLDecoder; /** * Created by Kosh on 10 Nov 2016, 3:43 PM @@ -465,4 +464,8 @@ public static void setNotificationSound(@NonNull Uri uri) { public static boolean isGistDisabled() { return PrefHelper.getBoolean(DISABLE_AUTO_PLAY_GIF); } + + public static boolean isAppAnimationDisabled() { + return PrefHelper.getBoolean("app_animation"); + } } diff --git a/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java b/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java index 59c28dd22..f728a6fcc 100644 --- a/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java +++ b/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java @@ -19,6 +19,7 @@ import com.fastaccess.R; import com.fastaccess.helper.AnimHelper; import com.fastaccess.helper.AppHelper; +import com.fastaccess.helper.PrefGetter; import com.fastaccess.ui.base.mvp.BaseMvp; import com.fastaccess.ui.base.mvp.presenter.BasePresenter; @@ -70,12 +71,17 @@ public abstract class BaseDialogFragment AnimHelper.revealDialog(dialog, - getResources().getInteger(android.R.integer.config_longAnimTime))); + if (PrefGetter.isAppAnimationDisabled()) { + dialog.setOnShowListener(dialogInterface -> AnimHelper.revealDialog(dialog, + getResources().getInteger(android.R.integer.config_longAnimTime))); + } return dialog; } diff --git a/app/src/main/java/com/fastaccess/ui/modules/theme/ThemeActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/theme/ThemeActivity.kt index 7f776c809..aade56189 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/theme/ThemeActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/theme/ThemeActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.view.View import android.view.ViewAnimationUtils import butterknife.BindView +import butterknife.OnClick import com.fastaccess.R import com.fastaccess.data.dao.FragmentPagerAdapterModel import com.fastaccess.helper.PrefGetter @@ -14,6 +15,7 @@ import com.fastaccess.ui.adapter.FragmentsPagerAdapter import com.fastaccess.ui.base.BaseActivity import com.fastaccess.ui.base.mvp.BaseMvp import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import com.fastaccess.ui.modules.main.premium.PremiumActivity import com.fastaccess.ui.modules.theme.fragment.ThemeFragmentMvp import com.fastaccess.ui.widgets.CardsPagerTransformerBasic import com.fastaccess.ui.widgets.ViewPagerView @@ -28,8 +30,11 @@ class ThemeActivity : BaseActivity @BindView(R.id.pager) lateinit var pager: ViewPagerView @BindView(R.id.parentLayout) lateinit var parentLayout: View - override fun layout(): Int = R.layout.theme_viewpager + @OnClick(R.id.premium) fun onOpenPremium() { + PremiumActivity.startActivity(this) + } + override fun layout(): Int = R.layout.theme_viewpager override fun isTransparent(): Boolean = false diff --git a/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java b/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java index 1e2a70ed8..012fcb9af 100644 --- a/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java +++ b/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java @@ -10,6 +10,7 @@ import com.fastaccess.helper.AnimHelper; import com.fastaccess.helper.Bundler; +import com.fastaccess.helper.PrefGetter; /** * Created by Kosh on 09 Dec 2016, 5:18 PM @@ -39,7 +40,8 @@ public class ProgressDialogFragment extends DialogFragment { progressDialog.setCancelable(isCancelable); setCancelable(isCancelable); if (getActivity() != null && !getActivity().isFinishing()) { - progressDialog.setOnShowListener(dialogInterface -> AnimHelper.revealDialog(progressDialog, 200)); + if (PrefGetter.isAppAnimationDisabled()) + progressDialog.setOnShowListener(dialogInterface -> AnimHelper.revealDialog(progressDialog, 200)); } return progressDialog; } diff --git a/app/src/main/res/layouts/main_layouts/layout/theme_viewpager.xml b/app/src/main/res/layouts/main_layouts/layout/theme_viewpager.xml index a8afbc6e9..1d5cefac5 100644 --- a/app/src/main/res/layouts/main_layouts/layout/theme_viewpager.xml +++ b/app/src/main/res/layouts/main_layouts/layout/theme_viewpager.xml @@ -1,7 +1,8 @@ - @@ -9,7 +10,20 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c580aeb42..5cd0ef795 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -557,4 +557,6 @@ expand Copy SHA View as code + In App Animations + Disable in App animations everywhere. diff --git a/app/src/main/res/xml/customization_settings.xml b/app/src/main/res/xml/customization_settings.xml index 5efd0847e..48c341949 100644 --- a/app/src/main/res/xml/customization_settings.xml +++ b/app/src/main/res/xml/customization_settings.xml @@ -10,29 +10,37 @@ + + + From c4c2da6fbfc28479d6f80043b062374d43b5c4d7 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Wed, 30 Aug 2017 09:48:08 +0200 Subject: [PATCH 13/49] made every animation related to #906 --- .../com/fastaccess/helper/ActivityHelper.java | 36 ++++++++++++------- .../ui/base/BaseDialogFragment.java | 4 +-- .../dialog/ProgressDialogFragment.java | 2 +- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/fastaccess/helper/ActivityHelper.java b/app/src/main/java/com/fastaccess/helper/ActivityHelper.java index f59b1cb1a..352051a3c 100644 --- a/app/src/main/java/com/fastaccess/helper/ActivityHelper.java +++ b/app/src/main/java/com/fastaccess/helper/ActivityHelper.java @@ -117,24 +117,36 @@ public static void start(@NonNull Activity activity, Intent intent, @NonNull Vie } public static void startReveal(@NonNull Activity activity, Intent intent, @NonNull View sharedElement, int requestCode) { - ActivityOptionsCompat options = ActivityOptionsCompat.makeClipRevealAnimation(sharedElement, sharedElement.getWidth() / 2, - sharedElement.getHeight() / 2, - sharedElement.getWidth(), sharedElement.getHeight()); - activity.startActivityForResult(intent, requestCode, options.toBundle()); + if (!PrefGetter.isAppAnimationDisabled()) { + ActivityOptionsCompat options = ActivityOptionsCompat.makeClipRevealAnimation(sharedElement, sharedElement.getWidth() / 2, + sharedElement.getHeight() / 2, + sharedElement.getWidth(), sharedElement.getHeight()); + activity.startActivityForResult(intent, requestCode, options.toBundle()); + } else { + activity.startActivityForResult(intent, requestCode); + } } public static void startReveal(@NonNull Fragment fragment, Intent intent, @NonNull View sharedElement, int requestCode) { - ActivityOptionsCompat options = ActivityOptionsCompat.makeClipRevealAnimation(sharedElement, sharedElement.getWidth() / 2, - sharedElement.getHeight() / 2, - sharedElement.getWidth(), sharedElement.getHeight()); - fragment.startActivityForResult(intent, requestCode, options.toBundle()); + if (!PrefGetter.isAppAnimationDisabled()) { + ActivityOptionsCompat options = ActivityOptionsCompat.makeClipRevealAnimation(sharedElement, sharedElement.getWidth() / 2, + sharedElement.getHeight() / 2, + sharedElement.getWidth(), sharedElement.getHeight()); + fragment.startActivityForResult(intent, requestCode, options.toBundle()); + } else { + fragment.startActivityForResult(intent, requestCode); + } } public static void startReveal(@NonNull Activity activity, Intent intent, @NonNull View sharedElement) { - ActivityOptionsCompat options = ActivityOptionsCompat.makeClipRevealAnimation(sharedElement, sharedElement.getWidth() / 2, - sharedElement.getHeight() / 2, - sharedElement.getWidth(), sharedElement.getHeight()); - activity.startActivity(intent, options.toBundle()); + if (!PrefGetter.isAppAnimationDisabled()) { + ActivityOptionsCompat options = ActivityOptionsCompat.makeClipRevealAnimation(sharedElement, sharedElement.getWidth() / 2, + sharedElement.getHeight() / 2, + sharedElement.getWidth(), sharedElement.getHeight()); + activity.startActivity(intent, options.toBundle()); + } else { + activity.startActivity(intent); + } } @SafeVarargs public static void start(@NonNull Activity activity, @NonNull Intent intent, diff --git a/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java b/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java index f728a6fcc..68f841e39 100644 --- a/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java +++ b/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java @@ -71,7 +71,7 @@ public abstract class BaseDialogFragment AnimHelper.revealDialog(dialog, getResources().getInteger(android.R.integer.config_longAnimTime))); } diff --git a/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java b/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java index 012fcb9af..8a1222d65 100644 --- a/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java +++ b/app/src/main/java/com/fastaccess/ui/widgets/dialog/ProgressDialogFragment.java @@ -40,7 +40,7 @@ public class ProgressDialogFragment extends DialogFragment { progressDialog.setCancelable(isCancelable); setCancelable(isCancelable); if (getActivity() != null && !getActivity().isFinishing()) { - if (PrefGetter.isAppAnimationDisabled()) + if (!PrefGetter.isAppAnimationDisabled()) progressDialog.setOnShowListener(dialogInterface -> AnimHelper.revealDialog(progressDialog, 200)); } return progressDialog; From 074048796d467ed081d0a621fa889e69d7d65271 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Wed, 30 Aug 2017 12:30:06 +0200 Subject: [PATCH 14/49] fixed build --- .../com/fastaccess/ui/base/BaseDialogFragment.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java b/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java index 68f841e39..ed7db43e5 100644 --- a/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java +++ b/app/src/main/java/com/fastaccess/ui/base/BaseDialogFragment.java @@ -71,7 +71,7 @@ public abstract class BaseDialogFragment Date: Wed, 30 Aug 2017 12:44:19 +0200 Subject: [PATCH 15/49] fixed crashes reported by fabric from 4.1.0 --- .../data/dao/model/AbstractRepo.java | 44 +++++++-------- .../provider/timeline/TimelineProvider.java | 5 +- .../editor/comment/CommentEditorFragment.kt | 2 + .../overview/ProfileOverviewFragment.java | 1 + .../overview/ProfileOverviewPresenter.java | 6 +-- .../ui/modules/repos/wiki/WikiPresenter.kt | 54 ++++++++++--------- .../ui/modules/trending/TrendingActivity.kt | 10 ++-- 7 files changed, 66 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java b/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java index 4413be490..2993bfd5f 100644 --- a/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java +++ b/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java @@ -147,30 +147,32 @@ public static Repo getRepo(long id) { public static Disposable saveStarred(@NonNull List models, @NonNull String starredUser) { return RxHelper.getSingle(Single.fromPublisher(s -> { - Login login = Login.getUser(); - if (login != null) { - BlockingEntityStore dataSource = App.getInstance().getDataStore().toBlocking(); - if (login.getLogin().equalsIgnoreCase(starredUser)) { - dataSource.delete(Repo.class) - .where(STARRED_USER.eq(starredUser)) - .get() - .value(); - if (!models.isEmpty()) { - for (Repo repo : models) { - dataSource.delete(Repo.class).where(Repo.ID.eq(repo.getId())).get().value(); - repo.setStarredUser(starredUser); - dataSource.insert(repo); + try { + Login login = Login.getUser(); + if (login != null) { + BlockingEntityStore dataSource = App.getInstance().getDataStore().toBlocking(); + if (login.getLogin().equalsIgnoreCase(starredUser)) { + dataSource.delete(Repo.class) + .where(STARRED_USER.eq(starredUser)) + .get() + .value(); + if (!models.isEmpty()) { + for (Repo repo : models) { + dataSource.delete(Repo.class).where(Repo.ID.eq(repo.getId())).get().value(); + repo.setStarredUser(starredUser); + dataSource.insert(repo); + } } + } else { + dataSource.delete(Repo.class) + .where(STARRED_USER.notEqual(login.getLogin()) + .or(STATUSES_URL.isNull())) + .get() + .value(); } - } else { - dataSource.delete(Repo.class) - .where(STARRED_USER.notEqual(login.getLogin()) - .or(STATUSES_URL.isNull())) - .get() - .value(); } - } - s.onNext(""); + s.onNext(""); + } catch (Exception ignored) {} s.onComplete(); })).subscribe(o -> {/*donothing*/}, Throwable::printStackTrace); } diff --git a/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java b/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java index b3e5cae53..708d63603 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/TimelineProvider.java @@ -86,7 +86,8 @@ public class TimelineProvider { } else if (event == IssueEventType.assigned || event == IssueEventType.unassigned) { spannableBuilder .append(" "); - if (user != null && user.getLogin().equalsIgnoreCase(issueEventModel.getAssignee().getLogin())) { + if ((user != null && issueEventModel.getAssignee() != null) && user.getLogin() + .equalsIgnoreCase(issueEventModel.getAssignee().getLogin())) { spannableBuilder .append(event == IssueEventType.assigned ? "self-assigned this" : "removed their assignment"); } else { @@ -94,7 +95,7 @@ public class TimelineProvider { .append(event == IssueEventType.assigned ? "assigned" : "unassigned"); spannableBuilder .append(" ") - .bold(issueEventModel.getAssignee().getLogin()); + .bold(issueEventModel.getAssignee() != null ? issueEventModel.getAssignee().getLogin() : ""); } } else if (event == IssueEventType.locked || event == IssueEventType.unlocked) { spannableBuilder diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt index 0effcdc21..44e4d631e 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt @@ -39,6 +39,7 @@ class CommentEditorFragment : BaseFragment if (focused) onToggleButtons(toggleButtons) } } override fun getEditText(): EditText = commentText diff --git a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java index 6b84a01df..0432f365c 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java @@ -281,6 +281,7 @@ public static ProfileOverviewFragment newInstance(@NonNull String login) { } @Override public void onInitPinnedRepos(@NonNull List nodes) { + if (pinnedReposTextView == null) return; if (!nodes.isEmpty()) { pinnedReposTextView.setVisibility(VISIBLE); pinnedReposCard.setVisibility(VISIBLE); diff --git a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java index fcc43ebaf..76302e9a2 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java @@ -110,7 +110,7 @@ class ProfileOverviewPresenter extends BasePresenter im .query(GetPinnedReposQuery.builder() .login(login) .build()); - manageObservable(Rx2Apollo.from(apolloCall) + manageDisposable(RxHelper.getObservable(Rx2Apollo.from(apolloCall)) .filter(dataResponse -> !dataResponse.hasErrors()) .flatMap(dataResponse -> { if (dataResponse.data() != null && dataResponse.data().user() != null) { @@ -121,11 +121,11 @@ class ProfileOverviewPresenter extends BasePresenter im .map(GetPinnedReposQuery.Edge::node) .toList() .toObservable() - .doOnNext(nodes1 -> { + .subscribe(nodes1 -> { nodes.clear(); nodes.addAll(nodes1); sendToView(view -> view.onInitPinnedRepos(nodes)); - })); + }, Throwable::printStackTrace)); } @Override public void onWorkOffline(@NonNull String login) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/wiki/WikiPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/wiki/WikiPresenter.kt index 659abc191..d51e31997 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/wiki/WikiPresenter.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/wiki/WikiPresenter.kt @@ -42,37 +42,41 @@ class WikiPresenter : BasePresenter(), WikiMvp.Presenter { private fun getWikiContent(body: String?): Observable { return Observable.fromPublisher { s -> - val document: Document = Jsoup.parse(body, "") - val wikiWrapper = document.select("#wiki-wrapper") - if (wikiWrapper.isNotEmpty()) { - val cloneUrl = wikiWrapper.select(".clone-url") + try { + val document: Document = Jsoup.parse(body, "") + val wikiWrapper = document.select("#wiki-wrapper") + if (wikiWrapper.isNotEmpty()) { + val cloneUrl = wikiWrapper.select(".clone-url") // val bottomRightBar = wikiWrapper.select(".wiki-custom-sidebar") - if (cloneUrl.isNotEmpty()) { - cloneUrl.remove() - } + if (cloneUrl.isNotEmpty()) { + cloneUrl.remove() + } // if (bottomRightBar.isNotEmpty()) { // bottomRightBar.remove() // } - val headerHtml = wikiWrapper.select(".gh-header .gh-header-meta") - val revision = headerHtml.select("a.history") - if (revision.isNotEmpty()) { - revision.remove() - } - val header = "
${headerHtml.html()}
" - val wikiContent = wikiWrapper.select(".wiki-content") - val content = header + wikiContent.select(".wiki-body").html() - val rightBarList = wikiContent.select(".wiki-pages").select("li") - val sidebarList = arrayListOf() - if (rightBarList.isNotEmpty()) { - rightBarList.onEach { - val sidebarTitle = it.select("a").text() - val sidebarLink = it.select("a").attr("href") - sidebarList.add(WikiSideBarModel(sidebarTitle, sidebarLink)) + val headerHtml = wikiWrapper.select(".gh-header .gh-header-meta") + val revision = headerHtml.select("a.history") + if (revision.isNotEmpty()) { + revision.remove() + } + val header = "
${headerHtml.html()}
" + val wikiContent = wikiWrapper.select(".wiki-content") + val content = header + wikiContent.select(".wiki-body").html() + val rightBarList = wikiContent.select(".wiki-pages").select("li") + val sidebarList = arrayListOf() + if (rightBarList.isNotEmpty()) { + rightBarList.onEach { + val sidebarTitle = it.select("a").text() + val sidebarLink = it.select("a").attr("href") + sidebarList.add(WikiSideBarModel(sidebarTitle, sidebarLink)) + } } + s.onNext(WikiContentModel(content, "", sidebarList)) + } else { + s.onNext(WikiContentModel("

No Wiki

", "", arrayListOf())) } - s.onNext(WikiContentModel(content, "", sidebarList)) - } else { - s.onNext(WikiContentModel("

No Wiki

", "", arrayListOf())) + } catch (e: Exception) { + e.printStackTrace() } s.onComplete() } diff --git a/app/src/main/java/com/fastaccess/ui/modules/trending/TrendingActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/trending/TrendingActivity.kt index 1e5e88a80..b8cb6ef5e 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/trending/TrendingActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/trending/TrendingActivity.kt @@ -190,18 +190,18 @@ class TrendingActivity : BaseActivity(), Tr val bundle = intent.extras if (bundle != null) { val lang: String = bundle.getString(BundleConstant.EXTRA) - val query: String = bundle.getString(BundleConstant.EXTRA_TWO) + val query: String? = bundle.getString(BundleConstant.EXTRA_TWO) if (!lang.isEmpty()) { selectedTitle = lang } - if (!query.isEmpty()) { - when (query.toLowerCase()) { + if (query.isNullOrEmpty()) { + daily.isSelected = true + } else { + when (query?.toLowerCase()) { "daily" -> daily.isSelected = true "weekly" -> weekly.isSelected = true "monthly" -> monthly.isSelected = true } - } else { - daily.isSelected = true } } else { daily.isSelected = true From 84ad22bd9936669150b06d48022cb2d7c93c5b4c Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Wed, 30 Aug 2017 20:56:48 +0200 Subject: [PATCH 16/49] this commit fixes #908 fixes #903 and fixes #901 --- .../provider/markdown/MarkDownProvider.java | 19 ++++++----- .../fastaccess/provider/theme/ThemeEngine.kt | 24 +++++++++----- .../ui/modules/editor/EditorActivity.kt | 7 +--- .../editor/comment/CommentEditorFragment.kt | 26 ++++++++++----- .../create/dialog/AddGistBottomSheetDialog.kt | 8 +---- .../ui/modules/repos/RepoPagerActivity.java | 10 ++++++ .../ui/modules/repos/RepoPagerMvp.java | 3 +- .../repos/code/files/RepoFilesFragment.java | 2 +- .../modules/repos/git/EditRepoFileActivity.kt | 13 ++------ .../repos/issues/RepoIssuesPagerFragment.java | 23 ++++++++++++- .../repos/issues/RepoIssuesPagerMvp.java | 2 ++ .../issue/RepoClosedIssuesFragment.java | 7 +++- .../issue/RepoOpenedIssuesFragment.java | 7 +++- .../RepoPullRequestPagerFragment.java | 32 +++++++++++++++---- .../RepoPullRequestPagerMvp.java | 4 ++- .../pull_request/RepoPullRequestFragment.java | 16 ++++++++-- .../reviews/changes/ReviewChangesActivity.kt | 18 ++++------- .../ui/widgets/markdown/MarkDownLayout.kt | 11 +++++++ .../recyclerview/scroll/InfiniteScroll.java | 3 ++ .../layout/comment_box_layout.xml | 5 --- 20 files changed, 161 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java index a97ccd85d..0282ace24 100644 --- a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java +++ b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java @@ -253,10 +253,7 @@ public static void addPhoto(@NonNull EditText editText) { public static void addPhoto(@NonNull EditText editText, @NonNull String title, @NonNull String link) { String result = "![" + InputHelper.toString(title) + "](" + InputHelper.toString(link) + ")\n"; - String text = InputHelper.toString(editText); - text += result; - editText.setText(text); - editText.setSelection(text.length()); + insertAtCursor(editText, result); } public static void addLink(@NonNull EditText editText) { @@ -265,10 +262,7 @@ public static void addLink(@NonNull EditText editText) { public static void addLink(@NonNull EditText editText, @NonNull String title, @NonNull String link) { String result = "[" + InputHelper.toString(title) + "](" + InputHelper.toString(link) + ")\n"; - String text = InputHelper.toString(editText); - text += result; - editText.setText(text); - editText.setSelection(text.length()); + insertAtCursor(editText, result); } private static boolean hasNewLine(@NonNull String source, int selectionStart) { @@ -313,4 +307,13 @@ public static boolean isArchive(@Nullable String name) { return false; } + + private static void insertAtCursor(@NonNull EditText editText, @NonNull String text) { + String oriContent = editText.getText().toString(); + int index = editText.getSelectionStart() >= 0 ? editText.getSelectionStart() : 0; + StringBuilder builder = new StringBuilder(oriContent); + builder.insert(index, text); + editText.setText(builder.toString()); + editText.setSelection(index + text.length()); + } } diff --git a/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt b/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt index 81477840a..3fbeb4c54 100644 --- a/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt +++ b/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt @@ -89,7 +89,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.ThemeDark PrefGetter.LIGHT_BLUE -> return R.style.ThemeDark_LightBlue PrefGetter.CYAN -> return R.style.ThemeDark_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.ThemeDark_Green + PrefGetter.GREEN -> return R.style.ThemeDark_Green + PrefGetter.TEAL -> return R.style.ThemeDark_Teal PrefGetter.LIGHT_GREEN -> return R.style.ThemeDark_LightGreen PrefGetter.LIME -> return R.style.ThemeDark_Lime PrefGetter.YELLOW -> return R.style.ThemeDark_Yellow @@ -107,7 +108,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.ThemeAmlod PrefGetter.LIGHT_BLUE -> return R.style.ThemeAmlod_LightBlue PrefGetter.CYAN -> return R.style.ThemeAmlod_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.ThemeAmlod_Green + PrefGetter.TEAL -> return R.style.ThemeAmlod_Teal + PrefGetter.GREEN -> return R.style.ThemeAmlod_Green PrefGetter.LIGHT_GREEN -> return R.style.ThemeAmlod_LightGreen PrefGetter.LIME -> return R.style.ThemeAmlod_Lime PrefGetter.YELLOW -> return R.style.ThemeAmlod_Yellow @@ -125,7 +127,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.ThemeMidNighBlue PrefGetter.LIGHT_BLUE -> return R.style.ThemeMidNighBlue_LightBlue PrefGetter.CYAN -> return R.style.ThemeMidNighBlue_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.ThemeMidNighBlue_Green + PrefGetter.TEAL -> return R.style.ThemeMidNighBlue_Teal + PrefGetter.GREEN -> return R.style.ThemeMidNighBlue_Green PrefGetter.LIGHT_GREEN -> return R.style.ThemeMidNighBlue_LightGreen PrefGetter.LIME -> return R.style.ThemeMidNighBlue_Lime PrefGetter.YELLOW -> return R.style.ThemeMidNighBlue_Yellow @@ -143,7 +146,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.ThemeBluish PrefGetter.LIGHT_BLUE -> return R.style.ThemeBluish_LightBlue PrefGetter.CYAN -> return R.style.ThemeBluish_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.ThemeBluish_Green + PrefGetter.TEAL -> return R.style.ThemeBluish_Teal + PrefGetter.GREEN -> return R.style.ThemeBluish_Green PrefGetter.LIGHT_GREEN -> return R.style.ThemeBluish_LightGreen PrefGetter.LIME -> return R.style.ThemeBluish_Lime PrefGetter.YELLOW -> return R.style.ThemeBluish_Yellow @@ -186,7 +190,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.DialogThemeDark PrefGetter.LIGHT_BLUE -> return R.style.DialogThemeDark_LightBlue PrefGetter.CYAN -> return R.style.DialogThemeDark_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.DialogThemeDark_Green + PrefGetter.TEAL -> return R.style.DialogThemeDark_Teal + PrefGetter.GREEN -> return R.style.DialogThemeDark_Green PrefGetter.LIGHT_GREEN -> return R.style.DialogThemeDark_LightGreen PrefGetter.LIME -> return R.style.DialogThemeDark_Lime PrefGetter.YELLOW -> return R.style.DialogThemeDark_Yellow @@ -204,7 +209,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.DialogThemeAmlod PrefGetter.LIGHT_BLUE -> return R.style.DialogThemeAmlod_LightBlue PrefGetter.CYAN -> return R.style.DialogThemeAmlod_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.DialogThemeAmlod_Green + PrefGetter.TEAL -> return R.style.DialogThemeAmlod_Teal + PrefGetter.GREEN -> return R.style.DialogThemeAmlod_Green PrefGetter.LIGHT_GREEN -> return R.style.DialogThemeAmlod_LightGreen PrefGetter.LIME -> return R.style.DialogThemeAmlod_Lime PrefGetter.YELLOW -> return R.style.DialogThemeAmlod_Yellow @@ -222,7 +228,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.DialogThemeLight PrefGetter.LIGHT_BLUE -> return R.style.DialogThemeLight_LightBlue PrefGetter.CYAN -> return R.style.DialogThemeLight_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.DialogThemeLight_Green + PrefGetter.TEAL -> return R.style.DialogThemeLight_Teal + PrefGetter.GREEN -> return R.style.DialogThemeLight_Green PrefGetter.LIGHT_GREEN -> return R.style.DialogThemeLight_LightGreen PrefGetter.LIME -> return R.style.DialogThemeLight_Lime PrefGetter.YELLOW -> return R.style.DialogThemeLight_Yellow @@ -240,7 +247,8 @@ object ThemeEngine { PrefGetter.BLUE -> return R.style.DialogThemeBluish PrefGetter.LIGHT_BLUE -> return R.style.DialogThemeBluish_LightBlue PrefGetter.CYAN -> return R.style.DialogThemeBluish_Cyan - PrefGetter.TEAL, PrefGetter.GREEN -> return R.style.DialogThemeBluish_Green + PrefGetter.TEAL -> return R.style.DialogThemeBluish_Teal + PrefGetter.GREEN -> return R.style.DialogThemeBluish_Green PrefGetter.LIGHT_GREEN -> return R.style.DialogThemeBluish_LightGreen PrefGetter.LIME -> return R.style.DialogThemeBluish_Lime PrefGetter.YELLOW -> return R.style.DialogThemeBluish_Yellow diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt index 37d0e14d6..6122416fa 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt @@ -175,12 +175,7 @@ class EditorActivity : BaseActivity(), EditorMv } override fun onAppendLink(title: String?, link: String?, isLink: Boolean) { - if (isLink) { - MarkDownProvider.addLink(editText, InputHelper.toString(title), InputHelper.toString(link)) - } else { - editText.setText(String.format("%s\n", editText.text)) - MarkDownProvider.addPhoto(editText, InputHelper.toString(title), InputHelper.toString(link)) - } + markDownLayout.onAppendLink(title, link, isLink) } override fun getEditText(): EditText = editText diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt index 44e4d631e..5e3fe89c0 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt @@ -18,7 +18,6 @@ import com.fastaccess.helper.Bundler import com.fastaccess.helper.InputHelper import com.fastaccess.helper.ViewHelper import com.fastaccess.provider.emoji.Emoji -import com.fastaccess.provider.markdown.MarkDownProvider import com.fastaccess.ui.base.BaseFragment import com.fastaccess.ui.base.mvp.BaseMvp import com.fastaccess.ui.base.mvp.presenter.BasePresenter @@ -27,6 +26,8 @@ import com.fastaccess.ui.modules.editor.emoji.EmojiMvp import com.fastaccess.ui.modules.editor.popup.EditorLinkImageMvp import com.fastaccess.ui.widgets.markdown.MarkDownLayout import com.fastaccess.ui.widgets.markdown.MarkdownEditText +import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent +import net.yslibrary.android.keyboardvisibilityevent.Unregistrar /** * Created by kosh on 21/08/2017. @@ -41,6 +42,7 @@ class CommentEditorFragment : BaseFragment if (focused) onToggleButtons(toggleButtons) } + } + + override fun onStart() { + super.onStart() + keyboardListener = KeyboardVisibilityEvent.registerEventListener(activity, { + TransitionManager.beginDelayedTransition((view as ViewGroup?)!!) + toggleButtons.isActivated = it + markdownBtnHolder.visibility = if (!it) View.GONE else View.VISIBLE + }) + } + + override fun onStop() { + keyboardListener?.unregister() + super.onStop() } override fun getEditText(): EditText = commentText @@ -136,12 +151,7 @@ class CommentEditorFragment : BaseFragment counts = new HashSet<>(); + private RepoPagerMvp.View repoCallback; public static RepoIssuesPagerFragment newInstance(@NonNull String repoId, @NonNull String login) { RepoIssuesPagerFragment view = new RepoIssuesPagerFragment(); @@ -47,6 +50,20 @@ public static RepoIssuesPagerFragment newInstance(@NonNull String repoId, @NonNu return view; } + @Override public void onAttach(Context context) { + super.onAttach(context); + if (getParentFragment() instanceof RepoPagerMvp.View) { + repoCallback = (RepoPagerMvp.View) getParentFragment(); + } else if (context instanceof RepoPagerMvp.View) { + repoCallback = (RepoPagerMvp.View) context; + } + } + + @Override public void onDetach() { + repoCallback = null; + super.onDetach(); + } + @Override protected int fragmentLayout() { return R.layout.centered_tabbed_viewpager; } @@ -95,6 +112,10 @@ public static RepoIssuesPagerFragment newInstance(@NonNull String repoId, @NonNu return pager != null ? pager.getCurrentItem() : 0; } + @Override public void onScrolled(boolean isUp) { + if (repoCallback != null) repoCallback.onScrolled(isUp); + } + @Override public void onSetBadge(int tabIndex, int count) { TabsCountStateModel model = new TabsCountStateModel(); model.setTabIndex(tabIndex); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/RepoIssuesPagerMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/RepoIssuesPagerMvp.java index 37ff394d8..2fc218cbd 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/RepoIssuesPagerMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/RepoIssuesPagerMvp.java @@ -19,6 +19,8 @@ interface View extends BaseMvp.FAView, RepoPagerMvp.TabsBadgeListener { void onChangeIssueSort(boolean isLastUpdated); @IntRange(from = 0, to = 1) int getCurrentItem(); + + void onScrolled(boolean isUp); } interface Presenter extends BaseMvp.FAPresenter {} diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoClosedIssuesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoClosedIssuesFragment.java index 4a0643a5a..3d8a1b100 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoClosedIssuesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoClosedIssuesFragment.java @@ -156,7 +156,12 @@ public static RepoClosedIssuesFragment newInstance(@NonNull String repoId, @NonN @NonNull @Override public OnLoadMore getLoadMore() { if (onLoadMore == null) { - onLoadMore = new OnLoadMore<>(getPresenter()); + onLoadMore = new OnLoadMore(getPresenter()) { + @Override public void onScrolled(boolean isUp) { + super.onScrolled(isUp); + if (pagerCallback != null) pagerCallback.onScrolled(isUp); + } + }; } onLoadMore.setParameter(IssueState.closed); return onLoadMore; diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoOpenedIssuesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoOpenedIssuesFragment.java index 9de1e247b..c27db2dac 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoOpenedIssuesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoOpenedIssuesFragment.java @@ -162,7 +162,12 @@ public static RepoOpenedIssuesFragment newInstance(@NonNull String repoId, @NonN @NonNull @Override public OnLoadMore getLoadMore() { if (onLoadMore == null) { - onLoadMore = new OnLoadMore<>(getPresenter()); + onLoadMore = new OnLoadMore(getPresenter()) { + @Override public void onScrolled(boolean isUp) { + super.onScrolled(isUp); + if (pagerCallback != null) pagerCallback.onScrolled(isUp); + } + }; } onLoadMore.setParameter(IssueState.open); return onLoadMore; diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerFragment.java index 17a26fdbb..11bb369f0 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerFragment.java @@ -1,5 +1,6 @@ package com.fastaccess.ui.modules.repos.pull_requests; +import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -9,6 +10,7 @@ import android.widget.TextView; import com.annimon.stream.Stream; +import com.evernote.android.state.State; import com.fastaccess.R; import com.fastaccess.data.dao.FragmentPagerAdapterModel; import com.fastaccess.data.dao.TabsCountStateModel; @@ -17,13 +19,13 @@ import com.fastaccess.helper.ViewHelper; import com.fastaccess.ui.adapter.FragmentsPagerAdapter; import com.fastaccess.ui.base.BaseFragment; +import com.fastaccess.ui.modules.repos.RepoPagerMvp; import com.fastaccess.ui.widgets.SpannableBuilder; import com.fastaccess.ui.widgets.ViewPagerView; import java.util.HashSet; import butterknife.BindView; -import com.evernote.android.state.State; /** * Created by Kosh on 31 Dec 2016, 1:36 AM @@ -37,7 +39,7 @@ public class RepoPullRequestPagerFragment extends BaseFragment counts = new HashSet<>(); - + private RepoPagerMvp.View repoCallback; public static RepoPullRequestPagerFragment newInstance(@NonNull String repoId, @NonNull String login) { RepoPullRequestPagerFragment view = new RepoPullRequestPagerFragment(); @@ -48,6 +50,20 @@ public static RepoPullRequestPagerFragment newInstance(@NonNull String repoId, @ return view; } + @Override public void onAttach(Context context) { + super.onAttach(context); + if (getParentFragment() instanceof RepoPagerMvp.View) { + repoCallback = (RepoPagerMvp.View) getParentFragment(); + } else if (context instanceof RepoPagerMvp.View) { + repoCallback = (RepoPagerMvp.View) context; + } + } + + @Override public void onDetach() { + repoCallback = null; + super.onDetach(); + } + @Override protected int fragmentLayout() { return R.layout.centered_tabbed_viewpager; } @@ -92,6 +108,14 @@ public static RepoPullRequestPagerFragment newInstance(@NonNull String repoId, @ } } + @Override public int getCurrentItem() { + return pager != null ? pager.getCurrentItem() : 0; + } + + @Override public void onScrolled(boolean isUp) { + if (repoCallback != null) repoCallback.onScrolled(isUp); + } + private void updateCount(@NonNull TabsCountStateModel model) { TextView tv = ViewHelper.getTabTextView(tabs, model.getTabIndex()); tv.setText(SpannableBuilder.builder() @@ -101,8 +125,4 @@ private void updateCount(@NonNull TabsCountStateModel model) { .bold(String.valueOf(model.getCount())) .append(")")); } - - @Override public int getCurrentItem() { - return pager != null ? pager.getCurrentItem() : 0; - } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerMvp.java index c3a7c2456..1eee1f770 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/RepoPullRequestPagerMvp.java @@ -9,10 +9,12 @@ * Created by Kosh on 31 Dec 2016, 1:35 AM */ -interface RepoPullRequestPagerMvp { +public interface RepoPullRequestPagerMvp { interface View extends BaseMvp.FAView, RepoPagerMvp.TabsBadgeListener { @IntRange(from = 0, to = 1) int getCurrentItem(); + + void onScrolled(boolean isUp); } interface Presenter extends BaseMvp.FAPresenter {} diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/RepoPullRequestFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/RepoPullRequestFragment.java index 1699c02a0..3c5ab9579 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/RepoPullRequestFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/RepoPullRequestFragment.java @@ -21,6 +21,7 @@ import com.fastaccess.ui.base.BaseFragment; import com.fastaccess.ui.modules.repos.RepoPagerMvp; import com.fastaccess.ui.modules.repos.extras.popup.IssuePopupFragment; +import com.fastaccess.ui.modules.repos.pull_requests.RepoPullRequestPagerMvp; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.details.PullRequestPagerActivity; import com.fastaccess.ui.widgets.StateLayout; import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView; @@ -41,6 +42,7 @@ public class RepoPullRequestFragment extends BaseFragment onLoadMore; private PullRequestAdapter adapter; + private RepoPullRequestPagerMvp.View pagerCallback; private RepoPagerMvp.TabsBadgeListener tabsBadgeListener; public static RepoPullRequestFragment newInstance(@NonNull String repoId, @NonNull String login, @NonNull IssueState issueState) { @@ -55,6 +57,11 @@ public static RepoPullRequestFragment newInstance(@NonNull String repoId, @NonNu @Override public void onAttach(Context context) { super.onAttach(context); + if (getParentFragment() instanceof RepoPullRequestPagerMvp.View) { + pagerCallback = (RepoPullRequestPagerMvp.View) getParentFragment(); + } else if (context instanceof RepoPullRequestPagerMvp.View) { + pagerCallback = (RepoPullRequestPagerMvp.View) context; + } if (getParentFragment() instanceof RepoPagerMvp.TabsBadgeListener) { tabsBadgeListener = (RepoPagerMvp.TabsBadgeListener) getParentFragment(); } else if (context instanceof RepoPagerMvp.TabsBadgeListener) { @@ -130,9 +137,7 @@ public static RepoPullRequestFragment newInstance(@NonNull String repoId, @NonNu } @Override public void showProgress(@StringRes int resId) { - refresh.setRefreshing(true); - stateLayout.showProgress(); } @@ -148,7 +153,12 @@ public static RepoPullRequestFragment newInstance(@NonNull String repoId, @NonNu @NonNull @Override public OnLoadMore getLoadMore() { if (onLoadMore == null) { - onLoadMore = new OnLoadMore<>(getPresenter()); + onLoadMore = new OnLoadMore(getPresenter()) { + @Override public void onScrolled(boolean isUp) { + super.onScrolled(isUp); + if (pagerCallback != null) pagerCallback.onScrolled(isUp); + } + }; } onLoadMore.setParameter(getIssueState()); return onLoadMore; diff --git a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt index 4b9b6200e..8e428ebd6 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt @@ -27,18 +27,12 @@ class ReviewChangesActivity : BaseActivity 0); if (layoutManager == null) { initLayoutManager(recyclerView.getLayoutManager()); } @@ -103,5 +104,7 @@ public void initialize(int page, int previousTotal) { public abstract boolean onLoadMore(int page, int totalItemsCount); + public void onScrolled(boolean isUp) {} + } diff --git a/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml b/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml index c982486ce..ee8c26677 100644 --- a/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/comment_box_layout.xml @@ -8,11 +8,6 @@ android:background="?colorPrimary" android:orientation="vertical"> - - Date: Thu, 31 Aug 2017 16:18:05 +0200 Subject: [PATCH 17/49] this commit fixes #910 and #911 --- app/src/main/AndroidManifest.xml | 3 +- .../data/dao/FragmentPagerAdapterModel.java | 6 +- .../fastaccess/data/service/IssueService.java | 4 +- .../java/com/fastaccess/helper/AppHelper.java | 2 +- .../com/fastaccess/helper/PrefGetter.java | 4 +- .../fastaccess/provider/theme/ThemeEngine.kt | 68 ++--- .../modules/theme/fragment/ThemeFragment.kt | 12 +- .../pretty/helper/GithubHelper.java | 3 +- .../row_layouts/layout/comments_row_item.xml | 2 +- app/src/main/res/values/theme_amlod.xml | 6 +- app/src/main/res/values/theme_midnight.xml | 232 ++++++++++++++++++ .../main/res/values/theme_midnight_blue.xml | 204 --------------- 12 files changed, 294 insertions(+), 252 deletions(-) create mode 100644 app/src/main/res/values/theme_midnight.xml delete mode 100644 app/src/main/res/values/theme_midnight_blue.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ed531fce6..65ec7fe79 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ + xmlns:tools="http://schemas.android.com/tools" + android:installLocation="auto"> diff --git a/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java b/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java index ee63cad4e..d5258b844 100644 --- a/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java @@ -152,7 +152,8 @@ private FragmentPagerAdapterModel(String title, Fragment fragment) { } @NonNull public static List buildForGist(@NonNull Context context, @NonNull Gist gistsModel) { - return Stream.of(new FragmentPagerAdapterModel(context.getString(R.string.files), GistFilesListFragment.newInstance(gistsModel.getFilesAsList(), false)), + return Stream.of(new FragmentPagerAdapterModel(context.getString(R.string.files), GistFilesListFragment.newInstance(gistsModel + .getFilesAsList(), false)), new FragmentPagerAdapterModel(context.getString(R.string.comments), GistCommentsFragment.newInstance(gistsModel.getGistId()))) .collect(Collectors.toList()); } @@ -217,7 +218,8 @@ private FragmentPagerAdapterModel(String title, Fragment fragment) { return Stream.of(new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeLight)), new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeDark)), new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeAmlod)), - new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeBluish))) + new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeBluish)), + new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeMidnight))) .collect(Collectors.toList()); } diff --git a/app/src/main/java/com/fastaccess/data/service/IssueService.java b/app/src/main/java/com/fastaccess/data/service/IssueService.java index 6a8835dba..5c78fc795 100644 --- a/app/src/main/java/com/fastaccess/data/service/IssueService.java +++ b/app/src/main/java/com/fastaccess/data/service/IssueService.java @@ -51,11 +51,11 @@ Observable getIssue(@Path("owner") String owner, @Path("repo") String rep Observable> getTimeline(@Path("owner") String owner, @Path("repo") String repo, @Path("issue_number") int issue_number); - @GET("repos/{owner}/{repo}/issues/{issue_number}/timeline") + @GET("repos/{owner}/{repo}/issues/{issue_number}/timeline?per_page=100") @Headers("Accept: application/vnd.github.mockingbird-preview,application/vnd.github.VERSION.full+json," + " application/vnd.github.squirrel-girl-preview") Observable> getTimeline(@Path("owner") String owner, @Path("repo") String repo, - @Path("issue_number") int issue_number, @Query("page") int page); + @Path("issue_number") int issue_number, @Query("page") int page); @POST("repos/{owner}/{repo}/issues") Observable createIssue(@Path("owner") String owner, @Path("repo") String repo, diff --git a/app/src/main/java/com/fastaccess/helper/AppHelper.java b/app/src/main/java/com/fastaccess/helper/AppHelper.java index fff86780b..4137ecd4a 100644 --- a/app/src/main/java/com/fastaccess/helper/AppHelper.java +++ b/app/src/main/java/com/fastaccess/helper/AppHelper.java @@ -63,7 +63,7 @@ public static void copyToClipboard(@NonNull Context context, @NonNull String uri public static boolean isNightMode(@NonNull Resources resources) { @PrefGetter.ThemeType int themeType = PrefGetter.getThemeType(resources); - return themeType == PrefGetter.DARK || themeType == PrefGetter.AMLOD || themeType == PrefGetter.BLUISH; + return themeType != PrefGetter.LIGHT; } public static String getFastHubIssueTemplate(boolean enterprise) { diff --git a/app/src/main/java/com/fastaccess/helper/PrefGetter.java b/app/src/main/java/com/fastaccess/helper/PrefGetter.java index 054a907d5..f24bdf38b 100644 --- a/app/src/main/java/com/fastaccess/helper/PrefGetter.java +++ b/app/src/main/java/com/fastaccess/helper/PrefGetter.java @@ -24,8 +24,8 @@ public class PrefGetter { public static final int LIGHT = 1; public static final int DARK = 2; public static final int AMLOD = 3; - public static final int MID_NIGHT_BLUE = 4; - public static final int BLUISH = 5; + public static final int BLUISH = 4; + public static final int MID_NIGHT_BLUE = 5; public static final int RED = 1; public static final int PINK = 2; diff --git a/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt b/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt index 3fbeb4c54..362efc7f4 100644 --- a/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt +++ b/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt @@ -44,7 +44,7 @@ object ThemeEngine { PrefGetter.LIGHT -> activity.setTheme(R.style.AppTheme_AboutActivity_Light) PrefGetter.DARK -> activity.setTheme(R.style.AppTheme_AboutActivity_Dark) PrefGetter.AMLOD -> activity.setTheme(R.style.AppTheme_AboutActivity_Amlod) - PrefGetter.MID_NIGHT_BLUE -> activity.setTheme(R.style.AppTheme_AboutActivity_MidNightBlue) + PrefGetter.MID_NIGHT_BLUE -> activity.setTheme(R.style.AppTheme_AboutActivity_Midnight) PrefGetter.BLUISH -> activity.setTheme(R.style.AppTheme_AboutActivity_Bluish) } setTaskDescription(activity) @@ -119,23 +119,23 @@ object ThemeEngine { else -> return R.style.ThemeAmlod } PrefGetter.MID_NIGHT_BLUE -> when (themeColor) { - PrefGetter.RED -> return R.style.ThemeMidNighBlue_Red - PrefGetter.PINK -> return R.style.ThemeMidNighBlue_Pink - PrefGetter.PURPLE -> return R.style.ThemeMidNighBlue_Purple - PrefGetter.DEEP_PURPLE -> return R.style.ThemeMidNighBlue_DeepPurple - PrefGetter.INDIGO -> return R.style.ThemeMidNighBlue_Indigo - PrefGetter.BLUE -> return R.style.ThemeMidNighBlue - PrefGetter.LIGHT_BLUE -> return R.style.ThemeMidNighBlue_LightBlue - PrefGetter.CYAN -> return R.style.ThemeMidNighBlue_Cyan - PrefGetter.TEAL -> return R.style.ThemeMidNighBlue_Teal - PrefGetter.GREEN -> return R.style.ThemeMidNighBlue_Green - PrefGetter.LIGHT_GREEN -> return R.style.ThemeMidNighBlue_LightGreen - PrefGetter.LIME -> return R.style.ThemeMidNighBlue_Lime - PrefGetter.YELLOW -> return R.style.ThemeMidNighBlue_Yellow - PrefGetter.AMBER -> return R.style.ThemeMidNighBlue_Amber - PrefGetter.ORANGE -> return R.style.ThemeMidNighBlue_Orange - PrefGetter.DEEP_ORANGE -> return R.style.ThemeMidNighBlue_DeepOrange - else -> return R.style.ThemeMidNighBlue + PrefGetter.RED -> return R.style.ThemeMidnight_Red + PrefGetter.PINK -> return R.style.ThemeMidnight_Pink + PrefGetter.PURPLE -> return R.style.ThemeMidnight_Purple + PrefGetter.DEEP_PURPLE -> return R.style.ThemeMidnight_DeepPurple + PrefGetter.INDIGO -> return R.style.ThemeMidnight_Indigo + PrefGetter.BLUE -> return R.style.ThemeMidnight + PrefGetter.LIGHT_BLUE -> return R.style.ThemeMidnight_LightBlue + PrefGetter.CYAN -> return R.style.ThemeMidnight_Cyan + PrefGetter.TEAL -> return R.style.ThemeMidnight_Teal + PrefGetter.GREEN -> return R.style.ThemeMidnight_Green + PrefGetter.LIGHT_GREEN -> return R.style.ThemeMidnight_LightGreen + PrefGetter.LIME -> return R.style.ThemeMidnight_Lime + PrefGetter.YELLOW -> return R.style.ThemeMidnight_Yellow + PrefGetter.AMBER -> return R.style.ThemeMidnight_Amber + PrefGetter.ORANGE -> return R.style.ThemeMidnight_Orange + PrefGetter.DEEP_ORANGE -> return R.style.ThemeMidnight_DeepOrange + else -> return R.style.ThemeMidnight } PrefGetter.BLUISH -> when (themeColor) { PrefGetter.RED -> return R.style.ThemeBluish_Red @@ -220,22 +220,22 @@ object ThemeEngine { else -> return R.style.DialogThemeAmlod } PrefGetter.MID_NIGHT_BLUE -> when (themeColor) { - PrefGetter.RED -> return R.style.DialogThemeLight_Red - PrefGetter.PINK -> return R.style.DialogThemeLight_Pink - PrefGetter.PURPLE -> return R.style.DialogThemeLight_Purple - PrefGetter.DEEP_PURPLE -> return R.style.DialogThemeLight_DeepPurple - PrefGetter.INDIGO -> return R.style.DialogThemeLight_Indigo - PrefGetter.BLUE -> return R.style.DialogThemeLight - PrefGetter.LIGHT_BLUE -> return R.style.DialogThemeLight_LightBlue - PrefGetter.CYAN -> return R.style.DialogThemeLight_Cyan - PrefGetter.TEAL -> return R.style.DialogThemeLight_Teal - PrefGetter.GREEN -> return R.style.DialogThemeLight_Green - PrefGetter.LIGHT_GREEN -> return R.style.DialogThemeLight_LightGreen - PrefGetter.LIME -> return R.style.DialogThemeLight_Lime - PrefGetter.YELLOW -> return R.style.DialogThemeLight_Yellow - PrefGetter.AMBER -> return R.style.DialogThemeLight_Amber - PrefGetter.ORANGE -> return R.style.DialogThemeLight_Orange - PrefGetter.DEEP_ORANGE -> return R.style.DialogThemeLight_DeepOrange + PrefGetter.RED -> return R.style.DialogThemeMidnight_Red + PrefGetter.PINK -> return R.style.DialogThemeMidnight_Pink + PrefGetter.PURPLE -> return R.style.DialogThemeMidnight_Purple + PrefGetter.DEEP_PURPLE -> return R.style.DialogThemeMidnight_DeepPurple + PrefGetter.INDIGO -> return R.style.DialogThemeMidnight_Indigo + PrefGetter.BLUE -> return R.style.DialogThemeMidnight + PrefGetter.LIGHT_BLUE -> return R.style.DialogThemeMidnight_LightBlue + PrefGetter.CYAN -> return R.style.DialogThemeMidnight_Cyan + PrefGetter.TEAL -> return R.style.DialogThemeMidnight_Teal + PrefGetter.GREEN -> return R.style.DialogThemeMidnight_Green + PrefGetter.LIGHT_GREEN -> return R.style.DialogThemeMidnight_LightGreen + PrefGetter.LIME -> return R.style.DialogThemeMidnight_Lime + PrefGetter.YELLOW -> return R.style.DialogThemeMidnight_Yellow + PrefGetter.AMBER -> return R.style.DialogThemeMidnight_Amber + PrefGetter.ORANGE -> return R.style.DialogThemeMidnight_Orange + PrefGetter.DEEP_ORANGE -> return R.style.DialogThemeMidnight_DeepOrange else -> return R.style.DialogThemeLight } PrefGetter.BLUISH -> when (themeColor) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/theme/fragment/ThemeFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/theme/fragment/ThemeFragment.kt index dfe09b870..39a3325d6 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/theme/fragment/ThemeFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/theme/fragment/ThemeFragment.kt @@ -11,7 +11,6 @@ import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ProgressBar import butterknife.BindView import butterknife.ButterKnife import butterknife.Unbinder @@ -19,6 +18,7 @@ import com.fastaccess.R import com.fastaccess.helper.* import com.fastaccess.ui.base.BaseFragment import com.fastaccess.ui.modules.main.donation.DonateActivity +import com.fastaccess.ui.modules.main.premium.PremiumActivity import com.fastaccess.ui.widgets.SpannableBuilder /** @@ -113,6 +113,7 @@ class ThemeFragment : BaseFragment setTheme(getString(R.string.dark_theme_mode)) R.style.ThemeAmlod -> applyAmlodTheme() R.style.ThemeBluish -> applyBluishTheme() + R.style.ThemeMidnight -> applyMidnightTheme() } } @@ -134,6 +135,15 @@ class ThemeFragment : BaseFragment@color/amlodWindowBackground
#040408 #010102 - #483078 + #2962FF ?colorAccent ?colorPrimary true @@ -150,7 +150,7 @@ @style/TimeLineBackgroundAmlod #040408 #08080F - #483078 + #2962FF @color/amlodWindowBackground @@ -218,7 +218,7 @@ @style/Theme.Mal.Dark.PopupOverlay #040408 #08080F - #483078 + #2962FF false #eee #ffe0e0e0 diff --git a/app/src/main/res/values/theme_midnight.xml b/app/src/main/res/values/theme_midnight.xml new file mode 100644 index 000000000..35d1ad0d1 --- /dev/null +++ b/app/src/main/res/values/theme_midnight.xml @@ -0,0 +1,232 @@ + + + + #1F2933 + #10FFFFFF + @color/material_green_900 + @color/material_red_900 + @color/material_blue_grey_500 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/theme_midnight_blue.xml b/app/src/main/res/values/theme_midnight_blue.xml deleted file mode 100644 index 1dddbdd03..000000000 --- a/app/src/main/res/values/theme_midnight_blue.xml +++ /dev/null @@ -1,204 +0,0 @@ - - - - #FAFAFA - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 5bccbfcdeb5f98b19e6c8673192df77055c5515c Mon Sep 17 00:00:00 2001 From: caiorrs Date: Thu, 31 Aug 2017 17:56:40 -0300 Subject: [PATCH 18/49] Updated ptBR translation --- app/src/main/res/values-pt-rBR/strings.xml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 9624342f8..dd08c90ed 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -333,7 +333,7 @@ Token Pessoal Entra com autenticação básica Se você está ligado a alguma organização e não consegue vê-las aqui, por favor entre no link abaixo. - \nhttps://help.github.com/articles/about-third-party-application-restrictions\nPS: Você poderia usar o token de acesso para entrar, o que vai + \nhttps://help.github.com/articles/about-third-party-application-restrictions\nPS: Você poderia usar o token de acesso para entrar, o que vai permitir acesso ao FastHub para poder ver a lista de suas organizações. Inserir Selecionar @@ -415,5 +415,18 @@ Original Poster Cancelar Reviews Desabilitar a coloração da barra de navegação nos temas -Desabilitar a coloração da barra de navegação + Desabilitar a coloração da barra de navegação + Escolher som de notificação personalizado + Escolher Som de Notificação + Desabilitar o auto play de GIFs + Dessabilitar Reproduzir GIF + mudanças solicitadas + Google Play Service insdisponível + Editar Gist + Conteúdo + expandir + Copiar SHA + Ver como código + Animações no App + Desabilitar todas as animações no App. From 1b0195d3e9a134671b5cdf7f5a57f50c186b06c3 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Fri, 1 Sep 2017 21:25:23 +0200 Subject: [PATCH 19/49] allowing creating & editing files using commit from app fixes #860 --- app/build.gradle | 1 + .../assets/lottie/code_invite_success.json | 1 + .../data/dao/CommitRequestModel.java | 42 +++++++++ .../fastaccess/data/dao/EditRepoFileModel.kt | 51 +++++++++++ .../fastaccess/data/service/ContentService.kt | 6 +- .../provider/rest/RestProvider.java | 23 +++-- .../modules/main/premium/PremiumActivity.kt | 26 ++++-- .../repos/code/files/RepoFilesFragment.java | 25 +++++- .../files/paths/RepoFilePathFragment.java | 23 +++++ .../modules/repos/git/EditRepoFileActivity.kt | 88 +++++++++++++------ .../ui/modules/repos/git/EditRepoFileMvp.kt | 5 ++ .../repos/git/EditRepoFilePresenter.kt | 45 +++++++--- .../layout-land/repo_file_header_layout.xml | 12 +++ .../repo_file_header_layout.xml | 12 +++ .../layout/edit_repo_file_layout.xml | 59 +++++++++++++ .../layout/pro_features_layout.xml | 22 +++++ .../layout/repo_file_header_layout.xml | 12 +++ 17 files changed, 396 insertions(+), 57 deletions(-) create mode 100644 app/src/main/assets/lottie/code_invite_success.json create mode 100644 app/src/main/java/com/fastaccess/data/dao/CommitRequestModel.java create mode 100644 app/src/main/java/com/fastaccess/data/dao/EditRepoFileModel.kt diff --git a/app/build.gradle b/app/build.gradle index a70280ad7..6937184c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -165,6 +165,7 @@ dependencies { implementation 'com.apollographql.apollo:apollo-rx2-support:0.4.0' implementation 'com.jaredrummler:android-device-names:1.1.4' implementation 'net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.1.0' + implementation 'com.airbnb.android:lottie:2.2.0' implementation project(path: ':jobdispatcher') compileOnly "org.projectlombok:lombok:${lombokVersion}" kapt "org.projectlombok:lombok:${lombokVersion}" diff --git a/app/src/main/assets/lottie/code_invite_success.json b/app/src/main/assets/lottie/code_invite_success.json new file mode 100644 index 000000000..d5882bd9b --- /dev/null +++ b/app/src/main/assets/lottie/code_invite_success.json @@ -0,0 +1 @@ +{"v":"4.6.9","fr":60,"ip":0,"op":230,"w":220,"h":220,"nm":"Intro3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Checker Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[131.817,171.725,0]},"a":{"a":0,"k":[22.562,22.633,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,0.5]},"o":{"x":[0.84,0.84,0.84],"y":[0,0,0.84]},"n":["0p5_1_0p84_0","0p5_1_0p84_0","0p5_0p5_0p84_0p84"],"t":158,"s":[0,0,100],"e":[130,130,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,0.5]},"o":{"x":[0.84,0.84,0.84],"y":[0,0,0.84]},"n":["0p5_1_0p84_0","0p5_1_0p84_0","0p5_0p5_0p84_0p84"],"t":176,"s":[130,130,100],"e":[100,100,100]},{"t":183}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-7.643,1.576],[-2.486,5.923],[7.643,-5.923]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.991},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[22.967,22.739],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-12.322,0],[0,0],[0,-12.323],[0,0],[12.322,0],[0,12.322],[0,0]],"o":[[0,0],[12.322,0],[0,0],[0,12.322],[-12.322,0],[0,0],[0,-12.323]],"v":[[-0.001,-22.383],[-0.001,-22.383],[22.312,-0.071],[22.312,0.07],[-0.001,22.383],[-22.312,0.07],[-22.312,-0.071]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0,0.639,0.231,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[22.562,22.633],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"P1 Outlines","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":76,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":140,"s":[0],"e":[100]},{"t":143}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[78.828,132.057,0]},"a":{"a":0,"k":[1.83,1.816,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.452,-1.566],[0.253,-0.406],[1.165,-1.143],[1.579,-0.389],[0.495,-0.001],[1.579,0.389],[1.141,1.161],[0.253,0.423],[0.445,1.566],[-0.419,1.566],[-0.236,0.423],[-1.124,1.167],[-1.579,0.381],[-0.501,-0.007],[-1.57,-0.38],[-1.148,-1.143],[-0.227,-0.406],[-0.419,-1.566]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.247,0.212,0.192,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1.83,1.816],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"P2 Outlines","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":127,"s":[0],"e":[100]},{"t":130}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[74.772,132.057,0]},"a":{"a":0,"k":[1.83,1.816,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.452,-1.566],[0.253,-0.406],[1.165,-1.143],[1.579,-0.389],[0.495,-0.001],[1.579,0.389],[1.141,1.161],[0.253,0.423],[0.445,1.566],[-0.419,1.566],[-0.236,0.423],[-1.124,1.167],[-1.579,0.381],[-0.501,-0.007],[-1.57,-0.38],[-1.148,-1.143],[-0.227,-0.406],[-0.419,-1.566]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.247,0.212,0.192,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1.83,1.816],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"P2 Outlines","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":119,"s":[0],"e":[100]},{"t":122}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[70.715,132.057,0]},"a":{"a":0,"k":[1.829,1.816,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.452,-1.566],[0.253,-0.406],[1.165,-1.143],[1.579,-0.389],[0.493,-0.001],[1.579,0.389],[1.139,1.161],[0.253,0.423],[0.443,1.566],[-0.419,1.566],[-0.236,0.423],[-1.124,1.167],[-1.579,0.381],[-0.501,-0.007],[-1.572,-0.38],[-1.148,-1.143],[-0.229,-0.406],[-0.419,-1.566]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.247,0.212,0.192,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1.829,1.816],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"p3 Outlines","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":109,"s":[0],"e":[100]},{"t":112}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[66.659,132.057,0]},"a":{"a":0,"k":[1.829,1.816,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.452,-1.566],[0.253,-0.406],[1.165,-1.143],[1.579,-0.389],[0.493,-0.001],[1.579,0.389],[1.139,1.161],[0.253,0.423],[0.443,1.566],[-0.419,1.566],[-0.236,0.423],[-1.124,1.167],[-1.579,0.381],[-0.503,-0.007],[-1.572,-0.38],[-1.148,-1.143],[-0.229,-0.406],[-0.419,-1.566]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.247,0.212,0.192,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1.829,1.816],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"P4 Outlines","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":87,"s":[0],"e":[100]},{"t":92}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[62.601,132.057,0]},"a":{"a":0,"k":[1.829,1.816,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.452,-1.566],[0.254,-0.406],[1.165,-1.143],[1.579,-0.389],[0.495,-0.001],[1.579,0.389],[1.141,1.161],[0.254,0.423],[0.445,1.566],[-0.419,1.566],[-0.236,0.423],[-1.124,1.167],[-1.579,0.381],[-0.501,-0.007],[-1.57,-0.38],[-1.148,-1.143],[-0.227,-0.406],[-0.419,-1.566]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.247,0.212,0.192,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1.829,1.816],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"p5 Outlines","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":76,"s":[2],"e":[100]},{"t":80}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[58.545,132.057,0]},"a":{"a":0,"k":[1.829,1.816,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.452,-1.566],[0.253,-0.406],[1.165,-1.143],[1.579,-0.389],[0.495,-0.001],[1.579,0.389],[1.141,1.161],[0.253,0.423],[0.445,1.566],[-0.419,1.566],[-0.236,0.423],[-1.124,1.167],[-1.579,0.381],[-0.501,-0.007],[-1.57,-0.38],[-1.148,-1.143],[-0.227,-0.406],[-0.419,-1.566]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.247,0.212,0.192,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1.829,1.816],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"Phone Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[68.515,144.03,0]},"a":{"a":0,"k":[29.842,56.541,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-6.469,-0.691],[6.469,-0.691],[6.469,0.691],[-6.469,0.691]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[29.694,60.956],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.146,0],[0,0],[0,-1.145],[0,0],[1.145,0],[0,0],[0,1.145],[0,0]],"o":[[0,0],[1.145,0],[0,0],[0,1.145],[0,0],[-1.146,0],[0,0],[0,-1.145]],"v":[[-16.583,-4.144],[16.583,-4.144],[18.656,-2.069],[18.656,2.07],[16.583,4.144],[-16.583,4.144],[-18.656,2.07],[-18.656,-2.069]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0,0.639,0.231,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[30.039,60.955],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-11.747,-0.691],[11.747,-0.691],[11.747,0.691],[-11.747,0.691]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.792,0.776,0.745,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[23.13,36.097],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-18.657,-4.488],[18.657,-4.488],[18.657,4.488],[-18.657,4.488]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.878,0.878,0.878,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[30.04,44.729],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.735,0],[0,0],[0,-0.735],[0.735,0],[0,0],[0,0.735]],"o":[[0,0],[0.735,0],[0,0.735],[0,0],[-0.735,0],[0,-0.735]],"v":[[-4.637,-1.331],[4.637,-1.331],[5.969,-0.001],[4.637,1.331],[-4.637,1.331],[-5.969,-0.001]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.49,0.451,0.424,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[29.842,6.649],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.838,0],[0,0],[0,-1.838],[1.838,0],[0,0],[0,1.838]],"o":[[0,0],[1.838,0],[0,1.838],[0,0],[-1.838,0],[0,-1.838]],"v":[[-1.313,-3.327],[1.314,-3.327],[4.641,-0.001],[1.314,3.327],[-1.313,3.327],[-4.641,-0.001]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.49,0.451,0.424,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.327},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[29.179,105.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.205,0],[0,0],[0,-2.204],[2.205,0],[0,0],[0,2.204]],"o":[[0,0],[2.205,0],[0,2.204],[0,0],[-2.205,0],[0,-2.204]],"v":[[-1.313,-3.991],[1.314,-3.991],[5.305,0],[1.314,3.991],[-1.313,3.991],[-5.305,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.608,0.608,0.608,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[29.178,105.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-24.536,-43.24],[24.536,-43.24],[24.536,43.24],[-24.536,43.24]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.592,0.592,0.592,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.327},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[29.842,55.211],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":3,"cix":2,"ix":8,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-3.665,0],[0,0],[0,-3.664],[0,0],[3.665,0],[0,0],[0,3.665],[0,0]],"o":[[0,0],[3.665,0],[0,0],[0,3.665],[0,0],[-3.665,0],[0,0],[0,-3.664]],"v":[[-21.879,-55.215],[21.879,-55.215],[28.515,-48.579],[28.515,48.579],[21.879,55.215],[-21.879,55.215],[-28.515,48.579],[-28.515,-48.579]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.588,0.588,0.588,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.327},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.792,0.776,0.745,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[29.842,56.541],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":3,"cix":2,"ix":9,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"Cupon Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.57],"y":[1]},"o":{"x":[0.16],"y":[0]},"n":["0p57_1_0p16_0"],"t":14,"s":[60],"e":[-3]},{"i":{"x":[0.51],"y":[1]},"o":{"x":[0.152],"y":[0]},"n":["0p51_1_0p152_0"],"t":44,"s":[-3],"e":[0]},{"t":72}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.16,"y":0},"n":"0p833_1_0p16_0","t":14,"s":[93.826,135.572,0],"e":[113,84.635,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.16,"y":0},"n":"0p833_1_0p16_0","t":44,"s":[113,84.635,0],"e":[113.826,85.572,0],"to":[0,0,0],"ti":[0,0,0]},{"t":72}]},"a":{"a":0,"k":[41.867,23.511,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[24.029,3.622],[22.827,6.008],[25.297,5.013],[25.696,6.924],[23.08,7.049],[25.178,8.615],[23.714,9.996],[22.278,7.807],[21.932,10.414],[20.061,9.842],[21.216,7.482],[18.796,8.512],[18.328,6.501],[20.926,6.372],[18.852,4.852],[20.276,3.478],[21.784,5.689],[22.139,3.045]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[15.224,0.931],[14.022,3.316],[16.491,2.322],[16.891,4.232],[14.275,4.357],[16.374,5.924],[14.909,7.305],[13.473,5.115],[13.128,7.723],[11.257,7.15],[12.411,4.79],[9.991,5.821],[9.523,3.81],[12.121,3.68],[10.048,2.161],[11.47,0.786],[12.979,2.997],[13.335,0.353]],"c":true}},"nm":"Path 2","mn":"ADBE Vector Shape - Group"},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[6.42,-1.761],[5.217,0.625],[7.686,-0.371],[8.087,1.541],[5.47,1.666],[7.569,3.232],[6.104,4.613],[4.669,2.423],[4.323,5.032],[2.452,4.459],[3.606,2.099],[1.186,3.129],[0.718,1.118],[3.316,0.988],[1.242,-0.533],[2.666,-1.906],[4.175,0.306],[4.53,-2.339]],"c":true}},"nm":"Path 3","mn":"ADBE Vector Shape - Group"},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-2.385,-4.453],[-3.587,-2.067],[-1.118,-3.062],[-0.718,-1.15],[-3.334,-1.026],[-1.236,0.54],[-2.7,1.921],[-4.137,-0.27],[-4.482,2.34],[-6.353,1.768],[-5.199,-0.593],[-7.619,0.437],[-8.087,-1.574],[-5.489,-1.704],[-7.563,-3.223],[-6.139,-4.597],[-4.631,-2.386],[-4.275,-5.031]],"c":true}},"nm":"Path 4","mn":"ADBE Vector Shape - Group"},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-11.19,-7.145],[-12.392,-4.759],[-9.923,-5.755],[-9.523,-3.842],[-12.139,-3.718],[-10.04,-2.152],[-11.505,-0.771],[-12.942,-2.96],[-13.286,-0.352],[-15.158,-0.924],[-14.003,-3.285],[-16.424,-2.255],[-16.891,-4.266],[-14.293,-4.396],[-16.367,-5.916],[-14.944,-7.289],[-13.435,-5.078],[-13.08,-7.722]],"c":true}},"nm":"Path 5","mn":"ADBE Vector Shape - Group"},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-19.995,-9.837],[-21.197,-7.451],[-18.727,-8.447],[-18.328,-6.535],[-20.944,-6.41],[-18.845,-4.844],[-20.31,-3.464],[-21.746,-5.652],[-22.091,-3.044],[-23.963,-3.617],[-22.808,-5.977],[-25.229,-4.947],[-25.696,-6.958],[-23.098,-7.089],[-25.172,-8.607],[-23.748,-9.981],[-22.241,-7.77],[-21.885,-10.414]],"c":true}},"nm":"Path 6","mn":"ADBE Vector Shape - Group"},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge"},{"ty":"fl","c":{"a":0,"k":[0.247,0.212,0.192,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[41.941,23.286],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":8,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.73,-0.223],[0,0],[0.224,-0.73],[0,0],[0.731,0.223],[0,0],[-0.224,0.73],[0,0]],"o":[[0,0],[0.73,0.223],[0,0],[-0.224,0.73],[0,0],[-0.73,-0.224],[0,0],[0.224,-0.73]],"v":[[-32.681,-21.906],[39.344,0.115],[40.261,1.841],[34.408,20.988],[32.681,21.906],[-39.344,-0.114],[-40.261,-1.841],[-34.408,-20.988]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.588,0.588,0.588,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1.382},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.792,0.776,0.745,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[41.867,23.511],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":0,"op":230,"st":0,"bm":0,"sr":1}]} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/data/dao/CommitRequestModel.java b/app/src/main/java/com/fastaccess/data/dao/CommitRequestModel.java new file mode 100644 index 000000000..adc115f76 --- /dev/null +++ b/app/src/main/java/com/fastaccess/data/dao/CommitRequestModel.java @@ -0,0 +1,42 @@ +package com.fastaccess.data.dao; + +/** + * Created by kosh on 31/08/2017. + */ + +public class CommitRequestModel { + + private String message; + private String content; + private String sha; + + public CommitRequestModel(String message, String content, String sha) { + this.message = message; + this.content = content; + this.sha = sha; + } + + public String getSha() { + return sha; + } + + public void setSha(String sha) { + this.sha = sha; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/app/src/main/java/com/fastaccess/data/dao/EditRepoFileModel.kt b/app/src/main/java/com/fastaccess/data/dao/EditRepoFileModel.kt new file mode 100644 index 000000000..5e837e85a --- /dev/null +++ b/app/src/main/java/com/fastaccess/data/dao/EditRepoFileModel.kt @@ -0,0 +1,51 @@ +package com.fastaccess.data.dao + +import android.os.Parcel +import android.os.Parcelable + +/** + * Created by Hashemsergani on 01/09/2017. + */ +data class EditRepoFileModel(val login: String, + val repoId: String, + val path: String?, + val ref: String, + val sha: String?, + val contentUrl: String?, + val fileName: String?, + val isEdit: Boolean) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readByte() != 0.toByte()) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(login) + parcel.writeString(repoId) + parcel.writeString(path) + parcel.writeString(ref) + parcel.writeString(sha) + parcel.writeString(contentUrl) + parcel.writeString(fileName) + parcel.writeByte(if (isEdit) 1 else 0) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): EditRepoFileModel { + return EditRepoFileModel(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/data/service/ContentService.kt b/app/src/main/java/com/fastaccess/data/service/ContentService.kt index d7500a5e5..a9c26cf55 100644 --- a/app/src/main/java/com/fastaccess/data/service/ContentService.kt +++ b/app/src/main/java/com/fastaccess/data/service/ContentService.kt @@ -1,11 +1,12 @@ package com.fastaccess.data.service +import com.fastaccess.data.dao.CommitRequestModel import com.fastaccess.data.dao.GitCommitModel -import com.fastaccess.data.dao.MergeRequestModel import io.reactivex.Observable import retrofit2.http.Body import retrofit2.http.PUT import retrofit2.http.Path +import retrofit2.http.Query /** * Created by kosh on 29/08/2017. @@ -16,5 +17,6 @@ interface ContentService { fun createUpdateFile(@Path("owner") owner: String, @Path("repoId") repoId: String, @Path("path") path: String, - @Body body: MergeRequestModel): Observable + @Query("branch") branch: String, + @Body body: CommitRequestModel): Observable } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java b/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java index 686334902..7ce117057 100644 --- a/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java +++ b/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java @@ -13,6 +13,7 @@ import com.fastaccess.R; import com.fastaccess.data.dao.GitHubErrorResponse; import com.fastaccess.data.dao.NameParser; +import com.fastaccess.data.service.ContentService; import com.fastaccess.data.service.GistService; import com.fastaccess.data.service.IssueService; import com.fastaccess.data.service.NotificationService; @@ -178,6 +179,19 @@ public static int getErrorCode(Throwable throwable) { return provideRetrofit(enterprise).create(SearchService.class); } + @NonNull public static SlackService getSlackService() { + return new Retrofit.Builder() + .baseUrl("https://ok13pknpj4.execute-api.eu-central-1.amazonaws.com/prod/") + .addConverterFactory(new GithubResponseConverter(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + .create(SlackService.class); + } + + @NonNull public static ContentService getContentService(boolean enterprise) { + return provideRetrofit(enterprise).create(ContentService.class); + } + @Nullable public static GitHubErrorResponse getErrorResponse(@NonNull Throwable throwable) { ResponseBody body = null; if (throwable instanceof HttpException) { @@ -191,15 +205,6 @@ public static int getErrorCode(Throwable throwable) { return null; } - @NonNull public static SlackService getSlackService() { - return new Retrofit.Builder() - .baseUrl("https://ok13pknpj4.execute-api.eu-central-1.amazonaws.com/prod/") - .addConverterFactory(new GithubResponseConverter(gson)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build() - .create(SlackService.class); - } - public static void clearHttpClient() { okHttpClient = null; } diff --git a/app/src/main/java/com/fastaccess/ui/modules/main/premium/PremiumActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/main/premium/PremiumActivity.kt index 1320e6f13..86cee4f25 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/main/premium/PremiumActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/main/premium/PremiumActivity.kt @@ -1,5 +1,6 @@ package com.fastaccess.ui.modules.main.premium +import android.animation.Animator import android.app.Activity import android.content.Context import android.content.Intent @@ -10,6 +11,7 @@ import android.widget.FrameLayout import butterknife.BindView import butterknife.OnClick import butterknife.OnEditorAction +import com.airbnb.lottie.LottieAnimationView import com.fastaccess.BuildConfig import com.fastaccess.R import com.fastaccess.helper.AppHelper @@ -28,6 +30,8 @@ class PremiumActivity : BaseActivity(), Premi @BindView(R.id.editText) lateinit var editText: EditText @BindView(R.id.viewGroup) lateinit var viewGroup: FrameLayout @BindView(R.id.progressLayout) lateinit var progressLayout: View + @BindView(R.id.successActivationView) lateinit var successActivationView: LottieAnimationView + @BindView(R.id.successActivationHolder) lateinit var successActivationHolder: View override fun layout(): Int = R.layout.pro_features_layout @@ -87,11 +91,23 @@ class PremiumActivity : BaseActivity(), Premi } override fun onSuccessfullyActivated() { - FabricProvider.logPurchase(InputHelper.toString(editText)) - PrefGetter.setProItems() - PrefGetter.setEnterpriseItem() - showMessage(R.string.success, R.string.success) - successResult() + hideProgress() + successActivationHolder.visibility = View.VISIBLE + successActivationView.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(p0: Animator?) {} + override fun onAnimationEnd(p0: Animator?) { + FabricProvider.logPurchase(InputHelper.toString(editText)) + PrefGetter.setProItems() + PrefGetter.setEnterpriseItem() + showMessage(R.string.success, R.string.success) + successResult() + } + + override fun onAnimationCancel(p0: Animator?) {} + + override fun onAnimationStart(p0: Animator?) {} + }) + successActivationView.playAnimation() } override fun onNoMatch() { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java index df1af1dcd..0b1325dcb 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java @@ -1,5 +1,7 @@ package com.fastaccess.ui.modules.repos.code.files; +import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -9,6 +11,7 @@ import android.widget.PopupMenu; import com.fastaccess.R; +import com.fastaccess.data.dao.EditRepoFileModel; import com.fastaccess.data.dao.model.Login; import com.fastaccess.data.dao.model.RepoFile; import com.fastaccess.data.dao.types.FilesType; @@ -89,7 +92,8 @@ public class RepoFilesFragment extends BaseFragment { switch (item1.getItemId()) { case R.id.share: @@ -104,8 +108,11 @@ public class RepoFilesFragment extends BaseFragment + TransitionManager.beginDelayedTransition(layoutHolder as ViewGroup) + if (editText.isFocused && isOpen) { + fileNameHolder.visibility = View.GONE + commitHolder.visibility = View.GONE + } else { + fileNameHolder.visibility = View.VISIBLE + commitHolder.visibility = View.VISIBLE + } + }) } override fun onSetText(content: String?) { @@ -67,6 +93,7 @@ class EditRepoFileActivity : BaseActivity(), EditRepoFileMvp.Presenter { - @com.evernote.android.state.State var path: String? = null - @com.evernote.android.state.State var repoId: String? = null - @com.evernote.android.state.State var login: String? = null - @com.evernote.android.state.State var isEdit: Boolean? = null - @com.evernote.android.state.State var contentUrl: String? = null + @com.evernote.android.state.State var model: EditRepoFileModel? = null + var downloadedContent: String? = null override fun onInit(intent: Intent?) { if (downloadedContent.isNullOrBlank()) { intent?.let { it.extras?.let { - repoId = it.getString(BundleConstant.ID) - login = it.getString(BundleConstant.EXTRA) - path = it.getString(BundleConstant.EXTRA_TWO) - contentUrl = it.getString(BundleConstant.EXTRA_THREE) - isEdit = it.getBoolean(BundleConstant.EXTRA_TYPE) + model = it.getParcelable(BundleConstant.ITEM) loadContent() } } @@ -34,8 +30,35 @@ class EditRepoFilePresenter : BasePresenter(), EditRepoFil } } + + override fun onSubmit(text: String?, filename: String?, description: String?) { + if (model?.login.isNullOrBlank() || model?.repoId.isNullOrBlank()) return + + sendToView { + it.onSetTextError(text.isNullOrBlank()) + it.onSetFilenameError(filename.isNullOrBlank()) + it.onSetDescriptionError(description.isNullOrBlank()) + } + if (!text.isNullOrBlank() && !description.isNullOrBlank() && !filename.isNullOrBlank()) { + model?.let { + val commitModel = CommitRequestModel(description!!, Base64.encodeToString(text!!.toByteArray(), Base64.DEFAULT), it.sha) + makeRestCall(RestProvider.getContentService(isEnterprise).createUpdateFile(it.login, it.repoId, + if (it.path.isNullOrBlank()) { + filename!! + } else { + if (it.path!!.endsWith("/")) { + "${it.path}$filename" + } else { + "${it.path}/$filename" + } + }, it.ref, commitModel), + { t -> sendToView { it.onSuccessfullyCommitted() } }) + } + } + } + private fun loadContent() { - contentUrl?.let { + model?.contentUrl?.let { makeRestCall(RestProvider.getRepoService(isEnterprise) .getFileAsStream(it), { sendToView({ v -> v.onSetText(it) }) }) } diff --git a/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml b/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml index 12abc7158..ee63afae1 100644 --- a/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml @@ -93,6 +93,18 @@ android:padding="@dimen/spacing_micro" android:src="@drawable/ic_search"/> + + \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml b/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml index 6b9baad5e..c615a0206 100644 --- a/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml @@ -96,6 +96,18 @@ android:contentDescription="@string/download" android:padding="@dimen/spacing_micro" android:src="@drawable/ic_search"/> + + \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml index 40730cad5..740d37093 100644 --- a/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml @@ -1,14 +1,73 @@ + + + + + + + + + + + + + + + + + + + @@ -274,4 +276,24 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml b/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml index d35bf4bce..303eda913 100644 --- a/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml @@ -48,6 +48,7 @@ android:layout_height="wrap_content" android:orientation="horizontal"> + + \ No newline at end of file From c5219a14a673d478dfa27e7d2300f2c8c9be23d3 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sat, 2 Sep 2017 15:55:13 +0200 Subject: [PATCH 20/49] done with deleting, editing & creating files from github --- .../fastaccess/data/service/ContentService.kt | 14 ++- .../repos/code/files/RepoFilesFragment.java | 30 +++++- .../repos/code/files/RepoFilesMvp.java | 7 +- .../repos/code/files/RepoFilesPresenter.java | 11 ++- .../files/paths/RepoFilePathFragment.java | 14 ++- .../modules/repos/git/EditRepoFileActivity.kt | 13 --- .../repos/git/EditRepoFilePresenter.kt | 10 +- .../git/delete/DeleteContentFileCallback.kt | 9 ++ .../delete/DeleteFileBottomSheetFragment.kt | 73 ++++++++++++++ .../layout-land/repo_file_header_layout.xml | 13 +-- .../repo_file_header_layout.xml | 13 +-- .../layout/delete_repo_file_layout.xml | 94 +++++++++++++++++++ .../layout/edit_repo_file_layout.xml | 4 +- .../layout/repo_file_header_layout.xml | 13 +-- app/src/main/res/menu/download_share_menu.xml | 8 ++ app/src/main/res/values/strings.xml | 1 + 16 files changed, 273 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteContentFileCallback.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteFileBottomSheetFragment.kt create mode 100644 app/src/main/res/layouts/main_layouts/layout/delete_repo_file_layout.xml diff --git a/app/src/main/java/com/fastaccess/data/service/ContentService.kt b/app/src/main/java/com/fastaccess/data/service/ContentService.kt index a9c26cf55..ee9328ff4 100644 --- a/app/src/main/java/com/fastaccess/data/service/ContentService.kt +++ b/app/src/main/java/com/fastaccess/data/service/ContentService.kt @@ -3,10 +3,7 @@ package com.fastaccess.data.service import com.fastaccess.data.dao.CommitRequestModel import com.fastaccess.data.dao.GitCommitModel import io.reactivex.Observable -import retrofit2.http.Body -import retrofit2.http.PUT -import retrofit2.http.Path -import retrofit2.http.Query +import retrofit2.http.* /** * Created by kosh on 29/08/2017. @@ -14,9 +11,16 @@ import retrofit2.http.Query interface ContentService { @PUT("repos/{owner}/{repoId}/contents/{path}") - fun createUpdateFile(@Path("owner") owner: String, + fun updateCreateFile(@Path("owner") owner: String, @Path("repoId") repoId: String, @Path("path") path: String, @Query("branch") branch: String, @Body body: CommitRequestModel): Observable + + @HTTP(method = "DELETE", path = "repos/{owner}/{repoId}/contents/{path}", hasBody = true) + fun deleteFile(@Path("owner") owner: String, + @Path("repoId") repoId: String, + @Path("path") path: String, + @Query("branch") branch: String, + @Body body: CommitRequestModel): Observable } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java index 0b1325dcb..e82d22924 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesFragment.java @@ -21,14 +21,17 @@ import com.fastaccess.helper.Bundler; import com.fastaccess.helper.FileHelper; import com.fastaccess.helper.InputHelper; +import com.fastaccess.helper.PrefGetter; import com.fastaccess.provider.markdown.MarkDownProvider; import com.fastaccess.provider.rest.RestProvider; import com.fastaccess.ui.adapter.RepoFilesAdapter; import com.fastaccess.ui.base.BaseFragment; import com.fastaccess.ui.modules.code.CodeViewerActivity; +import com.fastaccess.ui.modules.main.premium.PremiumActivity; import com.fastaccess.ui.modules.repos.code.files.activity.RepoFilesActivity; import com.fastaccess.ui.modules.repos.code.files.paths.RepoFilePathFragment; import com.fastaccess.ui.modules.repos.git.EditRepoFileActivity; +import com.fastaccess.ui.modules.repos.git.delete.DeleteFileBottomSheetFragment; import com.fastaccess.ui.widgets.AppbarRefreshLayout; import com.fastaccess.ui.widgets.StateLayout; import com.fastaccess.ui.widgets.dialog.MessageDialogView; @@ -82,7 +85,7 @@ public class RepoFilesFragment extends BaseFragment { switch (item1.getItemId()) { case R.id.share: @@ -108,10 +112,22 @@ public class RepoFilesFragment extends BaseFragment getCachedFiles(@NonNull String url, @NonNull String ref); + + void onDeleteFile(@NonNull String message, @NonNull RepoFile item); } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java index d6ce3858f..8b7e74184 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java @@ -2,9 +2,11 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.widget.SwipeRefreshLayout; import android.view.View; import com.fastaccess.R; +import com.fastaccess.data.dao.CommitRequestModel; import com.fastaccess.data.dao.RepoPathsManager; import com.fastaccess.data.dao.model.RepoFile; import com.fastaccess.helper.RxHelper; @@ -33,7 +35,7 @@ class RepoFilesPresenter extends BasePresenter implements Rep if (v.getId() != R.id.menu) { getView().onItemClicked(item); } else { - getView().onMenuClicked(item, v); + getView().onMenuClicked(position, item, v); } } @@ -114,4 +116,11 @@ class RepoFilesPresenter extends BasePresenter implements Rep @Nullable @Override public List getCachedFiles(@NonNull String url, @NonNull String ref) { return pathsModel.getPaths(url, ref); } + + @Override public void onDeleteFile(@NonNull String message, @NonNull RepoFile item) { + CommitRequestModel body = new CommitRequestModel(message, null, item.getSha()); + makeRestCall(RestProvider.getContentService(isEnterprise()) + .deleteFile(login, repoId, item.getPath(), ref, body), + gitCommitModel -> sendToView(SwipeRefreshLayout.OnRefreshListener::onRefresh)); + } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/paths/RepoFilePathFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/paths/RepoFilePathFragment.java index 0b243a23e..4a66270cd 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/paths/RepoFilePathFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/paths/RepoFilePathFragment.java @@ -22,9 +22,11 @@ import com.fastaccess.helper.BundleConstant; import com.fastaccess.helper.Bundler; import com.fastaccess.helper.InputHelper; +import com.fastaccess.helper.PrefGetter; import com.fastaccess.provider.rest.RestProvider; import com.fastaccess.ui.adapter.RepoFilePathsAdapter; import com.fastaccess.ui.base.BaseFragment; +import com.fastaccess.ui.modules.main.premium.PremiumActivity; import com.fastaccess.ui.modules.repos.code.files.RepoFilesFragment; import com.fastaccess.ui.modules.repos.extras.branches.pager.BranchesPagerFragment; import com.fastaccess.ui.modules.repos.git.EditRepoFileActivity; @@ -73,10 +75,14 @@ public static RepoFilePathFragment newInstance(@NonNull String login, @NonNull S } @OnClick(R.id.addFile) void onAddFile() { - RepoFile repoFile = !adapter.isEmpty() ? adapter.getItem(adapter.getItemCount() - 1) : null; - EditRepoFileModel fileModel = new EditRepoFileModel(getPresenter().login, getPresenter().repoId, - repoFile != null ? repoFile.getPath() : "", ref, repoFile != null ? repoFile.getSha() : "", null, null, false); - EditRepoFileActivity.Companion.startForResult(this, fileModel, isEnterprise()); + if (PrefGetter.isProEnabled() || PrefGetter.isAllFeaturesUnlocked()) { + RepoFile repoFile = !adapter.isEmpty() ? adapter.getItem(adapter.getItemCount() - 1) : null; + EditRepoFileModel fileModel = new EditRepoFileModel(getPresenter().login, getPresenter().repoId, + repoFile != null ? repoFile.getPath() : "", ref, repoFile != null ? repoFile.getSha() : "", null, null, false); + EditRepoFileActivity.Companion.startForResult(this, fileModel, isEnterprise()); + } else { + PremiumActivity.Companion.startActivity(getContext()); + } } @OnClick(R.id.downloadRepoFiles) void onDownloadRepoFiles() { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileActivity.kt index 62de9e9d7..2b2edb6e4 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFileActivity.kt @@ -6,13 +6,11 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.support.design.widget.TextInputLayout -import android.support.transition.TransitionManager import android.support.v4.app.Fragment import android.support.v4.app.FragmentManager import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.ViewGroup import android.widget.EditText import butterknife.BindView import com.fastaccess.R @@ -23,7 +21,6 @@ import com.fastaccess.provider.emoji.Emoji import com.fastaccess.ui.base.BaseActivity import com.fastaccess.ui.widgets.markdown.MarkDownLayout import com.fastaccess.ui.widgets.markdown.MarkdownEditText -import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent /** * Created by kosh on 29/08/2017. @@ -69,16 +66,6 @@ class EditRepoFileActivity : BaseActivity - TransitionManager.beginDelayedTransition(layoutHolder as ViewGroup) - if (editText.isFocused && isOpen) { - fileNameHolder.visibility = View.GONE - commitHolder.visibility = View.GONE - } else { - fileNameHolder.visibility = View.VISIBLE - commitHolder.visibility = View.VISIBLE - } - }) } override fun onSetText(content: String?) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt index dd4a3835f..2ab4fa76c 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt @@ -5,6 +5,7 @@ import android.util.Base64 import com.fastaccess.data.dao.CommitRequestModel import com.fastaccess.data.dao.EditRepoFileModel import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Logger import com.fastaccess.provider.rest.RestProvider import com.fastaccess.ui.base.mvp.presenter.BasePresenter @@ -22,6 +23,7 @@ class EditRepoFilePresenter : BasePresenter(), EditRepoFil intent?.let { it.extras?.let { model = it.getParcelable(BundleConstant.ITEM) + Logger.e(model) loadContent() } } @@ -42,17 +44,17 @@ class EditRepoFilePresenter : BasePresenter(), EditRepoFil if (!text.isNullOrBlank() && !description.isNullOrBlank() && !filename.isNullOrBlank()) { model?.let { val commitModel = CommitRequestModel(description!!, Base64.encodeToString(text!!.toByteArray(), Base64.DEFAULT), it.sha) - makeRestCall(RestProvider.getContentService(isEnterprise).createUpdateFile(it.login, it.repoId, + val observable = RestProvider.getContentService(isEnterprise).updateCreateFile(it.login, it.repoId, if (it.path.isNullOrBlank()) { filename!! } else { if (it.path!!.endsWith("/")) { "${it.path}$filename" } else { - "${it.path}/$filename" + "${it.path}" } - }, it.ref, commitModel), - { t -> sendToView { it.onSuccessfullyCommitted() } }) + }, it.ref, commitModel) + makeRestCall(observable, { sendToView { it.onSuccessfullyCommitted() } }) } } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteContentFileCallback.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteContentFileCallback.kt new file mode 100644 index 000000000..9713a6472 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteContentFileCallback.kt @@ -0,0 +1,9 @@ +package com.fastaccess.ui.modules.repos.git.delete + +/** + * Created by Hashemsergani on 02/09/2017. + */ +interface DeleteContentFileCallback { + + fun onDelete(message: String, position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteFileBottomSheetFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteFileBottomSheetFragment.kt new file mode 100644 index 000000000..f8208a561 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/git/delete/DeleteFileBottomSheetFragment.kt @@ -0,0 +1,73 @@ +package com.fastaccess.ui.modules.repos.git.delete + +import android.content.Context +import android.os.Bundle +import android.support.design.widget.TextInputLayout +import android.view.View +import butterknife.BindView +import butterknife.OnClick +import com.fastaccess.R +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Bundler +import com.fastaccess.helper.InputHelper +import com.fastaccess.ui.base.BaseBottomSheetDialog + +/** + * Created by Hashemsergani on 02/09/2017. + */ +class DeleteFileBottomSheetFragment : BaseBottomSheetDialog() { + + @BindView(R.id.description) lateinit var description: TextInputLayout + @BindView(R.id.fileName) lateinit var fileName: TextInputLayout + + private var deleteCallback: DeleteContentFileCallback? = null + + + @OnClick(R.id.delete) fun onDeleteClicked() { + description.error = if (InputHelper.isEmpty(description)) getString(R.string.required_field) else null + if (!InputHelper.isEmpty(description)) { + val position = arguments?.getInt(BundleConstant.EXTRA) + position?.let { + deleteCallback?.onDelete(InputHelper.toString(description), position) + } + dismiss() + } + } + + @OnClick(R.id.cancel) fun onCancel() { + dismiss() + } + + override fun onAttach(context: Context?) { + super.onAttach(context) + if (parentFragment is DeleteContentFileCallback) { + deleteCallback = parentFragment as DeleteContentFileCallback + } else if (context is DeleteContentFileCallback) { + deleteCallback = context + } + } + + override fun onDetach() { + deleteCallback = null + super.onDetach() + } + + override fun layoutRes(): Int = R.layout.delete_repo_file_layout + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + fileName.isEnabled = false + fileName.editText?.setText(arguments.getString(BundleConstant.ITEM)) + } + + companion object { + fun newInstance(position: Int, path: String): DeleteFileBottomSheetFragment { + val fragment = DeleteFileBottomSheetFragment() + fragment.arguments = Bundler.start() + .put(BundleConstant.EXTRA, position) + .put(BundleConstant.ITEM, path) + .end() + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml b/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml index ee63afae1..d624a675e 100644 --- a/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout-land/repo_file_header_layout.xml @@ -83,27 +83,28 @@ android:padding="@dimen/spacing_micro" android:src="@drawable/ic_download"/> + + android:src="@drawable/ic_add" + android:visibility="gone" + tools:visibility="visible"/> + android:src="@drawable/ic_search"/> diff --git a/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml b/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml index c615a0206..0b0877102 100644 --- a/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout-sw600dp/repo_file_header_layout.xml @@ -87,27 +87,28 @@ android:padding="@dimen/spacing_micro" android:src="@drawable/ic_download"/> + + android:src="@drawable/ic_add" + android:visibility="gone" + tools:visibility="visible"/> + android:src="@drawable/ic_search"/> \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/delete_repo_file_layout.xml b/app/src/main/res/layouts/main_layouts/layout/delete_repo_file_layout.xml new file mode 100644 index 000000000..fd278f409 --- /dev/null +++ b/app/src/main/res/layouts/main_layouts/layout/delete_repo_file_layout.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml index 740d37093..afd0ec03a 100644 --- a/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml @@ -1,12 +1,12 @@ diff --git a/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml b/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml index 303eda913..e8b6ba28c 100644 --- a/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/repo_file_header_layout.xml @@ -81,26 +81,27 @@ android:src="@drawable/ic_download"/> + android:src="@drawable/ic_add" + android:visibility="gone" + tools:visibility="visible"/> + android:src="@drawable/ic_search"/> + \ No newline at end of file diff --git a/app/src/main/res/menu/download_share_menu.xml b/app/src/main/res/menu/download_share_menu.xml index 5544661ee..882e13a4c 100644 --- a/app/src/main/res/menu/download_share_menu.xml +++ b/app/src/main/res/menu/download_share_menu.xml @@ -8,6 +8,14 @@ android:title="@string/edit" android:visible="false" app:showAsAction="ifRoom"/> + + + Date: Sat, 2 Sep 2017 16:23:45 +0200 Subject: [PATCH 21/49] this commit fixes #834 --- .../provider/scheme/SchemeParser.java | 17 +++++++++-- .../ui/modules/repos/RepoPagerActivity.java | 28 +++++++++++++++---- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java b/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java index 30c5c68de..9d07961a3 100644 --- a/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java +++ b/app/src/main/java/com/fastaccess/provider/scheme/SchemeParser.java @@ -245,10 +245,23 @@ private static boolean getInvitationIntent(@NonNull Uri uri) { @Nullable private static Intent getRepo(@NonNull Context context, @NonNull Uri uri) { List segments = uri.getPathSegments(); - if (segments == null || segments.size() < 2 || segments.size() > 2) return null; + if (segments == null || segments.size() < 2 || segments.size() > 3) return null; String owner = segments.get(0); String repoName = segments.get(1); - return RepoPagerActivity.createIntent(context, repoName, owner); + if (segments.size() == 3) { + String lastPath = uri.getLastPathSegment(); + if ("network".equalsIgnoreCase(lastPath)) { + return RepoPagerActivity.createIntent(context, repoName, owner, RepoPagerMvp.CODE, 3); + } else if ("stargazers".equalsIgnoreCase(lastPath)) { + return RepoPagerActivity.createIntent(context, repoName, owner, RepoPagerMvp.CODE, 2); + } else if ("watchers".equalsIgnoreCase(lastPath)) { + return RepoPagerActivity.createIntent(context, repoName, owner, RepoPagerMvp.CODE, 1); + } else { + return null; + } + } else { + return RepoPagerActivity.createIntent(context, repoName, owner); + } } @Nullable private static Intent getWiki(@NonNull Context context, @NonNull Uri uri) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java index cf5f89eee..2d2a30acc 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java @@ -102,6 +102,7 @@ public class RepoPagerActivity extends BaseActivity Date: Sat, 2 Sep 2017 16:38:44 +0200 Subject: [PATCH 22/49] this commit fixes #755 --- .../fastaccess/data/dao/CommitLinesModel.java | 3 +- .../viewholder/CommitLinesViewHolder.java | 2 + .../details/PullRequestPagerActivity.java | 9 ++- .../files/PullRequestFilesFragment.java | 19 +++-- .../layout/commit_line_row_item.xml | 73 ++++++++++++------- 5 files changed, 69 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/fastaccess/data/dao/CommitLinesModel.java b/app/src/main/java/com/fastaccess/data/dao/CommitLinesModel.java index 9940b56c1..f1c59295e 100644 --- a/app/src/main/java/com/fastaccess/data/dao/CommitLinesModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/CommitLinesModel.java @@ -35,6 +35,7 @@ public int rightLineNo; public boolean noNewLine; public int position; + private boolean hasCommentedOn; @NonNull public static List getLines(@Nullable String text) { ArrayList models = new ArrayList<>(); @@ -82,7 +83,7 @@ token = token.replace("\\ No newline at end of file", ""); } models.add(new CommitLinesModel(token, color, token.startsWith("@@") || !addLeft ? -1 : leftLineNo, - token.startsWith("@@") || !addRight ? -1 : rightLineNo, index != -1, position)); + token.startsWith("@@") || !addRight ? -1 : rightLineNo, index != -1, position, false)); } } } diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitLinesViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitLinesViewHolder.java index f6e3d8834..c4de7aaf4 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitLinesViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitLinesViewHolder.java @@ -26,6 +26,7 @@ public class CommitLinesViewHolder extends BaseViewHolder { @BindView(R.id.textView) AppCompatTextView textView; @BindView(R.id.leftLinNo) AppCompatTextView leftLinNo; @BindView(R.id.rightLinNo) AppCompatTextView rightLinNo; + @BindView(R.id.hasComment) View hasComment; private final int patchAdditionColor; private final int patchDeletionColor; private final int patchRefColor; @@ -44,6 +45,7 @@ public static CommitLinesViewHolder newInstance(@NonNull ViewGroup viewGroup, @N @Override public void bind(@NonNull CommitLinesModel item) { leftLinNo.setText(item.getLeftLineNo() > 0 ? String.valueOf(item.getLeftLineNo()) : " "); rightLinNo.setText(item.getRightLineNo() > 0 ? String.valueOf(item.getRightLineNo()) : " "); + hasComment.setVisibility(item.isHasCommentedOn() ? View.VISIBLE : View.GONE); switch (item.getColor()) { case CommitLinesModel.ADDITION: textView.setBackgroundColor(patchAdditionColor); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java index f653768af..23bb22f4e 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java @@ -45,6 +45,7 @@ import com.fastaccess.ui.modules.repos.extras.labels.LabelsDialogFragment; import com.fastaccess.ui.modules.repos.extras.milestone.create.MilestoneDialogFragment; import com.fastaccess.ui.modules.repos.issues.create.CreateIssueActivity; +import com.fastaccess.ui.modules.repos.pull_requests.pull_request.details.files.PullRequestFilesFragment; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.details.timeline.timeline.PullRequestTimelineFragment; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.merge.MergePullRequestDialogFragment; import com.fastaccess.ui.modules.reviews.changes.ReviewChangesActivity; @@ -111,7 +112,6 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo .show(getSupportFragmentManager(), MessageDialogView.TAG); } - @OnClick(R.id.submitReviews) void onSubmitReviews(View view) { addPrReview(view); } @@ -445,9 +445,14 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } protected void hideAndClearReviews() { - onUpdateTimeline(); getPresenter().getCommitComment().clear(); AnimHelper.mimicFabVisibility(false, prReviewHolder, null); + if (pager == null || pager.getAdapter() == null) return; + PullRequestFilesFragment fragment = (PullRequestFilesFragment) pager.getAdapter().instantiateItem(pager, 2); + if (fragment != null) { + fragment.onRefresh(); + } + } private void addPrReview(@NonNull View view) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java index f95095b50..cb907c98b 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/files/PullRequestFilesFragment.java @@ -215,13 +215,20 @@ private void setupChanges() { CommentRequestModel commentRequestModel = new CommentRequestModel(); commentRequestModel.setBody(comment); commentRequestModel.setPath(path); - if (item.getRightLineNo() > 0 && item.getLeftLineNo() > 0) { - commentRequestModel.setPosition(item.getPosition()); - } else { - commentRequestModel.setPosition(item.getPosition()); -// commentRequestModel.setLine(item.getRightLineNo() > 0 ? item.getRightLineNo() : item.getLeftLineNo()); - } + commentRequestModel.setPosition(item.getPosition()); if (viewCallback != null) viewCallback.onAddComment(commentRequestModel); + int groupPosition = bundle.getInt(BundleConstant.EXTRA_TWO); + int childPosition = bundle.getInt(BundleConstant.EXTRA_THREE); + CommitFileChanges commitFileChanges = adapter.getItem(groupPosition); + List models = commitFileChanges.getLinesModel(); + if (models != null && !models.isEmpty()) { + CommitLinesModel current = models.get(childPosition); + if (current != null) { + current.setHasCommentedOn(true); + } + models.set(childPosition, current); + adapter.notifyItemChanged(groupPosition); + } } } diff --git a/app/src/main/res/layouts/row_layouts/layout/commit_line_row_item.xml b/app/src/main/res/layouts/row_layouts/layout/commit_line_row_item.xml index fe1aff7d5..4a69d1d67 100644 --- a/app/src/main/res/layouts/row_layouts/layout/commit_line_row_item.xml +++ b/app/src/main/res/layouts/row_layouts/layout/commit_line_row_item.xml @@ -5,40 +5,57 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" + android:orientation="vertical" android:paddingBottom="@dimen/spacing_micro" android:paddingTop="@dimen/spacing_micro"> - + android:orientation="horizontal"> - + - + + + - + + + + + android:layout_height="0.5dp" + android:layout_marginBottom="@dimen/spacing_micro" + android:layout_marginTop="@dimen/spacing_micro" + android:background="?colorAccent" + android:visibility="gone"/> \ No newline at end of file From 2accc91905a47612c6cb0a043b1d501e09dc8ee5 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sat, 2 Sep 2017 17:44:06 +0200 Subject: [PATCH 23/49] this commit fixes #613 and fixes #916 --- app/src/main/AndroidManifest.xml | 11 +++ .../fastaccess/data/service/RepoService.java | 6 ++ .../viewholder/PullStatusViewHolder.java | 11 ++- .../code/commit/RepoCommitsFragment.java | 14 +++- .../code/commit/RepoCommitsPresenter.java | 27 ++++--- .../history/FileCommitHistoryActivity.kt | 76 +++++++++++++++++++ .../repos/code/files/RepoFilesPresenter.java | 5 +- .../PullRequestTimelinePresenter.java | 4 +- app/src/main/res/values/strings.xml | 1 + 9 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/history/FileCommitHistoryActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65ec7fe79..8cd02dd1d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -233,12 +233,23 @@ android:configChanges="keyboard|orientation|screenSize" android:screenOrientation="portrait" android:windowSoftInputMode="stateAlwaysHidden"/> + + + + + + > getCommits(@Path("owner") String owner, @Path("repo") String repo, @NonNull @Query("sha") String branch, @Query("page") int page); + @NonNull @GET("repos/{owner}/{repo}/commits") + Observable> getCommits(@Path("owner") String owner, @Path("repo") String repo, + @NonNull @Query("sha") String branch, + @NonNull @Query("path") String path, + @Query("page") int page); + @NonNull @GET("repos/{owner}/{repo}/releases") @Headers("Accept: application/vnd.github.VERSION.full+json") Observable> getReleases(@Path("owner") String owner, @Path("repo") String repo, @Query("page") int page); diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java index 1be01dbeb..9cca05caf 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java @@ -11,6 +11,7 @@ import com.fastaccess.data.dao.PullRequestStatusModel; import com.fastaccess.data.dao.types.StatusStateType; import com.fastaccess.helper.InputHelper; +import com.fastaccess.helper.Logger; import com.fastaccess.provider.scheme.SchemeParser; import com.fastaccess.ui.widgets.FontTextView; import com.fastaccess.ui.widgets.ForegroundImageView; @@ -44,12 +45,20 @@ public static PullStatusViewHolder newInstance(@NonNull ViewGroup parent) { } @Override public void bind(@NonNull PullRequestStatusModel pullRequestStatusModel) { + Logger.e(pullRequestStatusModel.getState()); if (pullRequestStatusModel.getState() != null) { StatusStateType stateType = pullRequestStatusModel.getState(); stateImage.setImageResource(stateType.getDrawableRes()); if (stateType == StatusStateType.failure) { stateImage.tintDrawableColor(red); - status.setText(R.string.checks_failed); + if (pullRequestStatusModel.isMergable()) { + status.setText(R.string.checks_failed); + } else { + status.setText(SpannableBuilder.builder() + .append(status.getResources().getString(R.string.checks_failed)) + .append("\n") + .append(status.getResources().getString(R.string.can_not_merge_pr))); + } } else if (stateType == StatusStateType.pending) { if (pullRequestStatusModel.isMergable()) { stateImage.setImageResource(R.drawable.ic_check_small); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/RepoCommitsFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/RepoCommitsFragment.java index 70698d0c8..1eb82bdd1 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/RepoCommitsFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/RepoCommitsFragment.java @@ -43,12 +43,22 @@ public class RepoCommitsFragment extends BaseFragment implements @com.evernote.android.state.State String login; @com.evernote.android.state.State String repoId; @com.evernote.android.state.State String branch; + @com.evernote.android.state.State String path; private int page; private int previousTotal; private int lastPage = Integer.MAX_VALUE; @@ -63,16 +67,18 @@ class RepoCommitsPresenter extends BasePresenter implements return false; } if (repoId == null || login == null) return false; - makeRestCall(RestProvider.getRepoService(isEnterprise()).getCommits(login, repoId, branch, page), - response -> { - if (response != null && response.getItems() != null) { - lastPage = response.getLast(); - if (getCurrentPage() == 1) { - manageDisposable(Commit.save(response.getItems(), repoId, login)); - } - } - sendToView(view -> view.onNotifyAdapter(response != null ? response.getItems() : null, page)); - }); + Observable> observable = InputHelper.isEmpty(path) + ? RestProvider.getRepoService(isEnterprise()).getCommits(login, repoId, branch, page) + : RestProvider.getRepoService(isEnterprise()).getCommits(login, repoId, branch, path, page); + makeRestCall(observable, response -> { + if (response != null && response.getItems() != null) { + lastPage = response.getLast(); + if (getCurrentPage() == 1) { + manageDisposable(Commit.save(response.getItems(), repoId, login)); + } + } + sendToView(view -> view.onNotifyAdapter(response != null ? response.getItems() : null, page)); + }); return true; } @@ -80,6 +86,7 @@ class RepoCommitsPresenter extends BasePresenter implements repoId = bundle.getString(BundleConstant.ID); login = bundle.getString(BundleConstant.EXTRA); branch = bundle.getString(BundleConstant.EXTRA_TWO); + path = bundle.getString(BundleConstant.EXTRA_THREE); if (!InputHelper.isEmpty(branch)) { getCommitCount(branch); } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/history/FileCommitHistoryActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/history/FileCommitHistoryActivity.kt new file mode 100644 index 000000000..4c81c5433 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/history/FileCommitHistoryActivity.kt @@ -0,0 +1,76 @@ +package com.fastaccess.ui.modules.repos.code.commit.history + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import com.evernote.android.state.State +import com.fastaccess.R +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Bundler +import com.fastaccess.ui.base.BaseActivity +import com.fastaccess.ui.base.mvp.BaseMvp +import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import com.fastaccess.ui.modules.repos.RepoPagerActivity +import com.fastaccess.ui.modules.repos.code.commit.RepoCommitsFragment + +/** + * Created by Hashemsergani on 02/09/2017. + */ +class FileCommitHistoryActivity : BaseActivity>() { + + @State var login: String? = null + @State var repoId: String? = null + + override fun layout(): Int = R.layout.activity_fragment_layout + + override fun providePresenter(): BasePresenter = BasePresenter() + + override fun isTransparent(): Boolean = true + + override fun canBack(): Boolean = true + + override fun isSecured(): Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null && intent != null) { + repoId = intent.extras.getString(BundleConstant.ID) + login = intent.extras.getString(BundleConstant.EXTRA) + supportFragmentManager + .beginTransaction() + .replace(R.id.container, RepoCommitsFragment.newInstance(intent.extras!!), RepoCommitsFragment::class.java.simpleName) + .commit() + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + if (item?.itemId == android.R.id.home) { + repoId?.let { + val intent = RepoPagerActivity.createIntent(this, it, login!!) + val bundle = intent.extras + bundle.putBoolean(BundleConstant.IS_ENTERPRISE, isEnterprise) + intent.putExtras(bundle) + startActivity(intent) + finish() + } + return true + } + return super.onOptionsItemSelected(item) + } + + companion object { + fun startActivity(context: Context, login: String, repoId: String, branch: String, path: String, + enterprise: Boolean) { + val intent = Intent(context, FileCommitHistoryActivity::class.java) + intent.putExtras(Bundler.start() + .put(BundleConstant.ID, repoId) + .put(BundleConstant.EXTRA, login) + .put(BundleConstant.EXTRA_TWO, branch) + .put(BundleConstant.EXTRA_THREE, path) + .put(BundleConstant.IS_ENTERPRISE, enterprise) + .end()) + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java index 8b7e74184..14c5602c6 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/files/RepoFilesPresenter.java @@ -12,6 +12,7 @@ import com.fastaccess.helper.RxHelper; import com.fastaccess.provider.rest.RestProvider; import com.fastaccess.ui.base.mvp.presenter.BasePresenter; +import com.fastaccess.ui.modules.repos.code.commit.history.FileCommitHistoryActivity; import java.util.ArrayList; import java.util.List; @@ -39,7 +40,9 @@ class RepoFilesPresenter extends BasePresenter implements Rep } } - @Override public void onItemLongClick(int position, View v, RepoFile item) {} + @Override public void onItemLongClick(int position, View v, RepoFile item) { + FileCommitHistoryActivity.Companion.startActivity(v.getContext(), login, repoId, ref, item.getPath(), isEnterprise()); + } @Override public void onError(@NonNull Throwable throwable) { onWorkOffline(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java index 4cee0cf78..53f5807c4 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java @@ -353,9 +353,9 @@ public class PullRequestTimelinePresenter extends BasePresenter> observable = Observable.zip( RestProvider.getIssueService(isEnterprise()).getTimeline(login, repoId, number, page), RestProvider.getReviewService(isEnterprise()).getPrReviewComments(login, repoId, number), - RestProvider.getPullRequestService(isEnterprise()).getPullStatus(login, repoId, parameter.getHead().getRef()) + RestProvider.getPullRequestService(isEnterprise()).getPullStatus(login, repoId, parameter.getHead().getSha()) .onErrorReturn(throwable -> RestProvider.getPullRequestService(isEnterprise()).getPullStatus(login, repoId, - parameter.getBase().getRef()).blockingFirst(new PullRequestStatusModel())), + parameter.getBase().getSha()).blockingFirst(new PullRequestStatusModel())), (response, comments, status) -> { if (response != null) { lastPage = response.getLast(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1dc6d8773..2750fc1ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -560,4 +560,5 @@ View as code In App Animations Disable in App animations everywhere. + This PR can\'t be merged now. From 27e2b690ceeb5597fa1a71fd929cf26a0050cf90 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sat, 2 Sep 2017 18:19:41 +0200 Subject: [PATCH 24/49] Edited readme to include new feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8b8f51fa6..a9dd95120 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ We have configured snapshots of FastHub, which can be downloaded from [AppVeyor - Wiki - **Repositories** - Browse & Read Wiki + - Edit, Create & Delete files (commit) - Search Repos - Browse and search Repos - See your public, private and forked Repos From 4afe53721721f7dd2b1b1134441ce790c7d9a57c Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sat, 2 Sep 2017 23:35:47 +0200 Subject: [PATCH 25/49] Updated license to include some legal stuff. --- LICENSE | 3 +++ 1 file changed, 3 insertions(+) diff --git a/LICENSE b/LICENSE index 9cecc1d46..b8c2b32be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,9 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + +P.S: All Fasthub Pro features shouldn't be distributed or reimplemented in any fork or librated version. These features should only be available from FastHub original project & therefore it's prohibited for anyone to provide them for free or sell them for themselves. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. From 6439d1d977976cc094427a3ff3e40d1f30ba515d Mon Sep 17 00:00:00 2001 From: Astro36 Date: Sun, 3 Sep 2017 16:10:26 +0900 Subject: [PATCH 26/49] Add korean translatation --- app/src/main/res/values-ko/strings.xml | 430 +++++++++++++++++++++++++ app/src/main/res/values/arrays.xml | 2 + 2 files changed, 432 insertions(+) create mode 100644 app/src/main/res/values-ko/strings.xml diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..f572342d8 --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,430 @@ + + 로딩 중, 잠시만 기다려주세요 + 행동 + 설정 + 제거 + 취소 + OK + 정보가 없습니다. + 검색 + FastHub를 계속 사용하려면 로그인하세요 + FastHub를 사용하기 위해서 GitHub에 로그인하세요 + 로그인 실패 + 로그인 + 공유 + 새로고침 + 프로필 + 오류 + 종료하려면 다시 한 번 눌러주세요 + Opened + Closed + 저장소 선택 + Followers + Following + 개요 + Follow + Unfollow + 사용자 + 세부 + 파일을 다운로드하여 해당 컨텐츠를 보세요 + 최소 글자 (3) + 파일을 찾을 수 없습니다 + readme를 찾을 수 없습니다 + 다운로드 중… + 파일 다운로드 중… + Released + Drafted + Releases + 내용 없음 + 기여자 + 기여 + by + 영어로 요구 사항를 보내주세요 + Issue 닫기 + Issue 다시 열기 + 다시 열기 + 닫기 + 성공적으로 다시 열었습니다 + 대화 잠금은 다음을 의미합니다:\n·다른 사람은 이 Issue에 새로운 댓글을 남길 수 없습니다.\n·너와 이 저장소에 접근할 수 있는 공동작업자는 다른 사람이 볼 수 있는 댓글을 남길 수 있습니다\n·너는 언제든 이 Issue를 잠금해제 할 수 있습니다.\n + 대화 잠금해제는 다음을 의미합니다:\n·모두가 이 Issue에 덧글을 남길 수 있습니다.\n·너는 언제든 이 Issue를 잠글 수 있습니다.\n + 대화 잠금 + 대화 잠금해제 + Issue 닫기 오류, 잠시 후 다시 시도해주세요. + Issue 다시 열기 오류, 잠시 후 다시 시도해주세요. + Issue 닫기 성공 +

설명 없음

+ 제목 1 + 제목 2 + 제목 3 + 굵게 + 기울임 + 취소선 + 순서없는 목록 + 순서있는 목록 + 제목 + 인용구 + 링크 + 이미지 + 제거 + 추가 + 변경 + 상태 + Are you sure? + 성공 + to + 댓글을 삭제하는 중에 오류가 발생했습니다 + 제거 + 댓글 + 댓글 + 병합 성공 + 파일 메뉴 + 파일 + 다운로드 + 뒤로가기 + 상위 폴더 + 코드 뷰어 + 브라우저로 열기 + 큰 파일 + 파일이 너무 커서 열 수 없습니다.\n"예"를 눌러 다운로드하십시오. + 뷰어 + 보내기 + 이곳에 입력해주세요 + 설명 + 파일 이름 + 확장명을 가진 파일 이름 + 비공개 Gist + 공개 Gist + 다음으로 보내기 + 삭제 + Gist를 삭제하는 중에 오류가 발생했습니다. + 파일 없음 + 필수 입력란 + 제출 성공 + Gist 생성 + 클리어 + 사용자 + 제목 + 파일 + 이정표 + 스스로 담당 + Issue 보내기 + Issue를 생성하는 중에 오류가 발생했습니다. + Issue 생성 + 수정을 계속하려면 강조 표시를 선택 해제하세요. + 알림 + 읽지 않은 알림이 있습니다 + 열기 + 새로운 창 열기 + 알림 종류 + 꼬리표 + 꼬리표 없음 + 꼬리표 추가 성공 + 피드백 보내기 + 로그아웃 + 피드백 감사합니다 + 현재 버전 + 버전 + 개발을 지원하려면 광고를 활성화하세요 + 사용자 이름 + 비밀번호 + 이중 인증 코드 + 로그인 + Gist 설명 + 사용자 아바타를 클릭하여 사용자의 프로필을 열 수 있습니다. + 포크 이벤트를 길게 클릭하여 원본 또는 분기된 저장소를 엽니 다. + Release 다운로드 + 설정 + 파일 다운로드 또는 디렉토리 공유 + 댓글을 탭하면 작성자의 태그를 지정하거나 댓글을 수정할 수 있습니다.\n길게 누르면 삭제됩니다. + Star/unstar 저장소 + 구독 + 저장소 구독/구독해제 + 사이드바에서 더 빨리 액세스 할 수 있도록 저장소를 고정하세요. + 모두 닫기 + URL을 찾을 수 없습니다 + 마지막 업데이트 + 미리보기 + 구문 강조기 + 구문 강조기를 활성화/비활성화합니다. + \n더 많은 설정을 보려면 Markdown 편집기 아이콘을 스크롤하세요. + 생성일 + 파일 생성 날짜 + 파일 업데이트 날짜 + 모두 읽음으로 표시 + 모든 알림 + 읽지 않음 + 모든 + 저장소 삭제 + 저장소 삭제는 되돌릴 수 없습니다. + 30분 + 20분 + 10분 + 5분 + 1분 + 1시간 + 2시간 + 3시간 + 생성됨 + 커밋됨 + 다운로드됨 + 팔로우됨 + Issue 댓글 + 구성원 + Pull Request 댓글 + 푸시됨 + + 삭제됨 + 알 수 없음 + 커밋 댓글 + 분기 변경 + 담당자 + 수정 + \u2022 수정됨 + Issue 업데이트 + Pull Request 업데이트 + 이정표 없음 + 추가 + 완료 + + 이정표 생성 + 이정표를 생성하는 중에 오류가 발생했습니다. + 만기일 + 담당자 없음 + 이 분기 + 커밋이 선택한 분기로 전환되었습니다. + 일반 + FastHub이 새 알림을 확인하는 빈도를 변경합니다. + 동기화 간격 + 항상 + 행동 + 사용자 정의 + 목록 효과 활성화 + 목록 효과 + 앱 종료 확인 다이얼로그를 비활성화합니다. + 앱 종료 확인 비활성화 + 복원 + 백업 + 백업 성공! + 복원할 백업 선택 + 허락되지 않은 권한 + 마지막 업데이트: %s + 지금 + 저장되지 않은 변경사항을 삭제하시겠습니까? + 비공개 + 원형 아바타 대신 둥근 사각형 아바타를 사용합니다. + 둥근 사각형 아바타 + 앱 평점 매기기 + 개발자 + GitHub에서 포크하기 + 이메일 보내기 + FastHub에 관한 질문 + 피드백 + 오류 보고 + 오류가 있습니까? 이곳에 입력해주세요. + 앱 정보 + 알림 + 끄기 + 인증되지 않은 사용자 + 이중 인증이 필요합니다 + Issue 없음 + URL 복사 + 복사됨 + 커밋 메시지 + 서버와 통신하는 중 오류가 발생했습니다. + API를 요청하는 중에 오류가 발생했습니다. + 서버를 요청하는 중 오류가 발생했습니다. 잠시 후 다시 시도하십시오. + 알림을 읽음으로 표시 + Gist 포크 + 기본 브라우저로 로그인 (OAuth) + 또는 + 알림 읽음 비활성화 + 알림을 클릭하면 읽음으로 표시 기능 사용을 비활성화합니다. + 테마 + 기본 테마 선택 + 테마 강조 색상 선택 + 테마 강조 색상 + 웹사이트 + 개발 지원 + 대단히 감사합니다! + 테마가 제대로 적용되지 않으면, 앱을 수동으로 재시작해주세요. + 고정 + 고정됨 + 고정해제 + 아직 고정된 저장소가 없으므로 여기에서 볼 수 있도록 고정하세요.\nP.S: 많이 액세스할수록 저장소는 위에 배치될 것입니다. + + 아니요 + Feeds 없음 + Gists 없음 + 덧글 없음 + 알림 없음 + Follower 없음 + Following 없음 + 저장소 없음 + 즐겨찾기된 저장소 없음 + 커밋 없음 + 기여자 없음 + 릴리즈 없음 + 닫힌 Issue 없음 + 열린 Issue 없음 + 이벤트 없음 + 열린 Pull Request 없음 + 닫힌 Pull Request 없음 + 검색 결과 없음 + 파일을 보기 위해 FastHub에서 파일을 저장하려면 권한을 허용하세요. + 공개 Gist + 광고 활성화 + Issue 없음 + 읽지 않은 알림 없음 + 내 Gist + 변경사항 + 클릭하여 알림 목록을 열거나 옆으로 밀어 닫으세요 + 길게 누르면 어디서나 기본 화면으로 이동합니다 + 생성됨 + 담당됨 + 언급됨 + 이름 + 색상 + 꼬리표 샹성 + 조직 + 조직 + 구성원 + + Members + 구성원 없음 + 팀 없음 + 조직 없음 + 너의 조직을 찾을 수 없습니까? + 읽음으로 표시 + 효과 + 팝업 효과를 활성화합니다. + 팝업 효과 + 이정표 + 담당자 + 몇몇 검사를 통과하지 못했습니다 + 몇몇 검사가 지연되었습니다 + 모든 검사를 통과했습니다 + 정렬 + 최신 순 + 오래된 순 + 많은 댓글 순 + 최신 댓글 순 + 최신 업데이트 순 + 최소 최신 업데이트 순 + 최신 버전입니다 + 새로운 버전이 있습니다 + 검색 내용을 입력해주세요 + 길게 누르면 Issue 티켓을 즉시 생성 할 수 있습니다 + 이 Pull Request는 합병될 수 있습니다 + 검토됨 + 검토 취소됨 + 변경 승인됨 + 반응 없음 + 반응 + 줄 바꿈 + 기본적으로 코드 뷰어에서 코드 줄 바꿈 + 코드 줄 바꿈 + 오픈소스 라이브러리 + 알림음을 활성화합니다. + 알림음 활성화 + 알림 활성화 + 개인 토큰으로 로그인 + 개인 토큰 + 기본 인증으로 로그인 + If you are actually tied to an organizations and you can\'t see them here please follow link + below.\nhttps://help.github.com/articles/about-third-party-application-restrictions\nPS: 조직에 FastHub 엑세스 권한을 부여하고 액세스 토큰을 사용해서 로그인할 수 있습니다.\n또한 https://github.com/settings/applications에서 FastHub를 찾아 조직 액세스로 스크롤 한 다음 승인 버튼을 클릭하세요. + 삽입 + 선택 + 사진 선택 + $2.00 지원 + $5.00 지원 + $10.00 지원 + $20.00 지원 + 언어 + 언어 + 언어 선택 + 언어를 선택하세요. + 구독 취소 + from + in + 구독 + 이 저장소에서 Issue가 사용 중지되었습니다 + 엑세스 토큰 + 기본 인증 + 로그인 종류 선택 + In Files + In Paths + 요청 검토 + Slack 참여 + FastHub Slack에 참여하시겠습니까? + 초대 성공 + 답글 + 이미지 불러오기에 실패했습니다. + 병합 희망 + 구독자 + 검토자 + 검토자 없음 + %2$s%3$s을 사용하여 %1$s에서 보냈습니다. + 서명 활성화 + 서명을 통해 전송을 활성화합니다. + 서명 상자 활성화 + 확인란을 사용하여 텍스트 편집기에서 서명을 활성화/비활성화 할 수 있습니다. + 태그 + 댓글 달기 + 배너 소개 + With FastHub 2.5.0, you can now better express yourself with banners + for your profile page.\n\nAnyone using the FastHub app, will see your header, and you\'ll + begin seeing other peoples headers as well! If you\'d like to create a banner for yourself, + make it 1280x384 or divisible, otherwise, it may get cropped.\n\nYou can add or change your + banner at any time, by creating a gist described "header.fst" with a file containing a + direct link to the header image.\n\nOr even simpler, just use the built-in image chooser! + + 배너 선택 + 이미지를 불러오는데 오류가 발생했습니다. 다시 시도하세요. + 급상승 + GitHub 제한 때문에 emojies로 정렬이 실제로 작동하지 않습니다 + 위로 스크롤 + 아래로 스크롤 + 참여됨 + 모두 선택됨 + 모두 선택 취소됨 + 구분선 + 사용자 없음 + 급상승 없음 + 초기화 + 적용 + 필터 + 종류 + 정렬 순서 + 담당자 추가 성공 + 검토자 추가 성공 + 이정표 추가 성공 + Feed + 프리미엄 테마 + 코드 색상 표 + Please do login to your GitHub account in order to access everything from GitHub API + otherwise you\'ll end up being kicked out every time you access anything rather than your Enterprise GitHub due to the token transmitted to + GitHub API is coming from your Enterprise Account. + 계정 추가 + 계정 선택 + 일치하지 않음 + 경고 + 소유자 + 원본 포스터 + 검토 취소 + 테마에서 색상 네비게이션 바를 비활성화합니다. + 무색 네비게이션 + 알림 소리를 선택합니다. + 알림 소리 선택 + GIF 자동 재생 사용을 비활성화합니다. + + 요청된 변경 사항 + Google Play 서비스를 사용할 수 없음 + Gist 수정 + 내용 + 확장 + SHA 복사 + 코드로 보기 + 앱 애니메이션 + 모든 앱 애니메이션을 비활성화합니다. + 이 Pull Request는 현재 병합될 수 없습니다. +
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index f67089a79..faeb0a9a3 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -130,6 +130,7 @@ Český Español Български + 한국어 @@ -148,6 +149,7 @@ cs es bg + ko From b5312fe815cc38ce3b523a7b6437ff02104314e1 Mon Sep 17 00:00:00 2001 From: Astro36 Date: Sun, 3 Sep 2017 16:19:52 +0900 Subject: [PATCH 27/49] Fix some typos --- app/src/main/res/values-ko/strings.xml | 130 ++++++++++++------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f572342d8..68ba7cef9 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -5,7 +5,7 @@ 제거 취소 OK - 정보가 없습니다. + 데이터 없음 검색 FastHub를 계속 사용하려면 로그인하세요 FastHub를 사용하기 위해서 GitHub에 로그인하세요 @@ -16,14 +16,14 @@ 프로필 오류 종료하려면 다시 한 번 눌러주세요 - Opened - Closed + 열림 + 닫힘 저장소 선택 - Followers - Following + 팔로워 + 팔로잉 개요 - Follow - Unfollow + 팔로우 + 언팔로우 사용자 세부 파일을 다운로드하여 해당 컨텐츠를 보세요 @@ -32,26 +32,26 @@ readme를 찾을 수 없습니다 다운로드 중… 파일 다운로드 중… - Released - Drafted - Releases + 릴리즈됨 + 초안 저장됨 + 릴리즈 내용 없음 기여자 기여 by 영어로 요구 사항를 보내주세요 - Issue 닫기 - Issue 다시 열기 + 이슈 닫기 + 이슈 다시 열기 다시 열기 닫기 성공적으로 다시 열었습니다 - 대화 잠금은 다음을 의미합니다:\n·다른 사람은 이 Issue에 새로운 댓글을 남길 수 없습니다.\n·너와 이 저장소에 접근할 수 있는 공동작업자는 다른 사람이 볼 수 있는 댓글을 남길 수 있습니다\n·너는 언제든 이 Issue를 잠금해제 할 수 있습니다.\n - 대화 잠금해제는 다음을 의미합니다:\n·모두가 이 Issue에 덧글을 남길 수 있습니다.\n·너는 언제든 이 Issue를 잠글 수 있습니다.\n + 대화 잠금은 다음을 의미합니다:\n·다른 사람은 이 이슈에 새로운 댓글을 남길 수 없습니다.\n·너와 이 저장소에 접근할 수 있는 공동작업자는 다른 사람이 볼 수 있는 댓글을 남길 수 있습니다\n·언제든 이 이슈를 잠금해제 할 수 있습니다.\n + 대화 잠금해제는 다음을 의미합니다:\n·모두가 이 이슈에 덧글을 남길 수 있습니다.\n·언제든 이 이슈를 잠글 수 있습니다.\n 대화 잠금 대화 잠금해제 - Issue 닫기 오류, 잠시 후 다시 시도해주세요. - Issue 다시 열기 오류, 잠시 후 다시 시도해주세요. - Issue 닫기 성공 + 이슈 닫기 오류, 잠시 후 다시 시도해주세요 + 이슈 다시 열기 오류, 잠시 후 다시 시도해주세요 + 이슈 닫기 성공

설명 없음

제목 1 제목 2 @@ -69,7 +69,7 @@ 추가 변경 상태 - Are you sure? + 계속 하시겠습니까? 성공 to 댓글을 삭제하는 중에 오류가 발생했습니다 @@ -85,7 +85,7 @@ 코드 뷰어 브라우저로 열기 큰 파일 - 파일이 너무 커서 열 수 없습니다.\n"예"를 눌러 다운로드하십시오. + 파일이 너무 커서 열 수 없습니다.\n"예"를 눌러 다운로드하세요 뷰어 보내기 이곳에 입력해주세요 @@ -106,11 +106,11 @@ 제목 파일 이정표 - 스스로 담당 - Issue 보내기 - Issue를 생성하는 중에 오류가 발생했습니다. - Issue 생성 - 수정을 계속하려면 강조 표시를 선택 해제하세요. + 자신을 담당자에 할당 + 이슈 보내기 + 이슈를 생성하는 중에 오류가 발생했습니다 + 이슈 생성 + 수정을 계속하려면 강조 표시를 선택 해제하세요 알림 읽지 않은 알림이 있습니다 열기 @@ -130,16 +130,16 @@ 이중 인증 코드 로그인 Gist 설명 - 사용자 아바타를 클릭하여 사용자의 프로필을 열 수 있습니다. - 포크 이벤트를 길게 클릭하여 원본 또는 분기된 저장소를 엽니 다. + 사용자 아바타를 클릭하여 사용자의 프로필을 열 수 있습니다 + 포크 이벤트를 길게 클릭하여 원본 또는 분기된 저장소를 엽니 다 Release 다운로드 설정 파일 다운로드 또는 디렉토리 공유 - 댓글을 탭하면 작성자의 태그를 지정하거나 댓글을 수정할 수 있습니다.\n길게 누르면 삭제됩니다. - Star/unstar 저장소 + 댓글을 탭하면 작성자의 태그를 지정하거나 댓글을 수정할 수 있습니다.\n길게 누르면 삭제됩니다 + 저장소 즐겨찾기/즐겨찾기해제 구독 저장소 구독/구독해제 - 사이드바에서 더 빨리 액세스 할 수 있도록 저장소를 고정하세요. + 사이드바에서 더 빨리 액세스 할 수 있도록 저장소를 고정하세요 모두 닫기 URL을 찾을 수 없습니다 마지막 업데이트 @@ -155,7 +155,7 @@ 읽지 않음 모든 저장소 삭제 - 저장소 삭제는 되돌릴 수 없습니다. + 저장소 삭제는 되돌릴 수 없습니다 30분 20분 10분 @@ -168,9 +168,9 @@ 커밋됨 다운로드됨 팔로우됨 - Issue 댓글 + 이슈 댓글 구성원 - Pull Request 댓글 + 풀 리퀘스트 댓글 푸시됨 삭제됨 @@ -180,27 +180,27 @@ 담당자 수정 \u2022 수정됨 - Issue 업데이트 - Pull Request 업데이트 + 이슈 업데이트 + 풀 리퀘스트 업데이트 이정표 없음 추가 완료 이정표 생성 - 이정표를 생성하는 중에 오류가 발생했습니다. + 이정표를 생성하는 중에 오류가 발생했습니다 만기일 담당자 없음 이 분기 - 커밋이 선택한 분기로 전환되었습니다. + 커밋이 선택한 분기로 전환되었습니다 일반 - FastHub이 새 알림을 확인하는 빈도를 변경합니다. + FastHub이 새 알림을 확인하는 빈도를 변경합니다 동기화 간격 항상 행동 사용자 정의 목록 효과 활성화 목록 효과 - 앱 종료 확인 다이얼로그를 비활성화합니다. + 앱 종료 확인 다이얼로그를 비활성화합니다 앱 종료 확인 비활성화 복원 백업 @@ -211,7 +211,7 @@ 지금 저장되지 않은 변경사항을 삭제하시겠습니까? 비공개 - 원형 아바타 대신 둥근 사각형 아바타를 사용합니다. + 원형 아바타 대신 둥근 사각형 아바타를 사용합니다 둥근 사각형 아바타 앱 평점 매기기 개발자 @@ -226,19 +226,19 @@ 끄기 인증되지 않은 사용자 이중 인증이 필요합니다 - Issue 없음 + 이슈 없음 URL 복사 복사됨 커밋 메시지 - 서버와 통신하는 중 오류가 발생했습니다. - API를 요청하는 중에 오류가 발생했습니다. - 서버를 요청하는 중 오류가 발생했습니다. 잠시 후 다시 시도하십시오. + 서버와 통신하는 중 오류가 발생했습니다 + API를 요청하는 중에 오류가 발생했습니다 + 서버 요청 오류, 잠시 후 다시 시도하세요 알림을 읽음으로 표시 Gist 포크 기본 브라우저로 로그인 (OAuth) 또는 알림 읽음 비활성화 - 알림을 클릭하면 읽음으로 표시 기능 사용을 비활성화합니다. + 알림을 클릭하면 읽음으로 표시 기능 사용을 비활성화합니다 테마 기본 테마 선택 테마 강조 색상 선택 @@ -246,7 +246,7 @@ 웹사이트 개발 지원 대단히 감사합니다! - 테마가 제대로 적용되지 않으면, 앱을 수동으로 재시작해주세요. + 테마가 제대로 적용되지 않으면, 앱을 수동으로 재시작해주세요 고정 고정됨 고정해제 @@ -264,16 +264,16 @@ 커밋 없음 기여자 없음 릴리즈 없음 - 닫힌 Issue 없음 - 열린 Issue 없음 + 닫힌 이슈 없음 + 열린 이슈 없음 이벤트 없음 - 열린 Pull Request 없음 - 닫힌 Pull Request 없음 + 열린 풀 리퀘스트 없음 + 닫힌 풀 리퀘스트 없음 검색 결과 없음 - 파일을 보기 위해 FastHub에서 파일을 저장하려면 권한을 허용하세요. + 파일을 보기 위해 FastHub에서 파일을 저장하려면 권한을 허용하세요 공개 Gist 광고 활성화 - Issue 없음 + 이슈 없음 읽지 않은 알림 없음 내 Gist 변경사항 @@ -296,7 +296,7 @@ 너의 조직을 찾을 수 없습니까? 읽음으로 표시 효과 - 팝업 효과를 활성화합니다. + 팝업 효과를 활성화합니다 팝업 효과 이정표 담당자 @@ -313,8 +313,8 @@ 최신 버전입니다 새로운 버전이 있습니다 검색 내용을 입력해주세요 - 길게 누르면 Issue 티켓을 즉시 생성 할 수 있습니다 - 이 Pull Request는 합병될 수 있습니다 + 길게 누르면 이슈 티켓을 즉시 생성 할 수 있습니다 + 이 풀 리퀘스트는 합병될 수 있습니다 검토됨 검토 취소됨 변경 승인됨 @@ -324,7 +324,7 @@ 기본적으로 코드 뷰어에서 코드 줄 바꿈 코드 줄 바꿈 오픈소스 라이브러리 - 알림음을 활성화합니다. + 알림음을 활성화합니다 알림음 활성화 알림 활성화 개인 토큰으로 로그인 @@ -347,7 +347,7 @@ from in 구독 - 이 저장소에서 Issue가 사용 중지되었습니다 + 이 저장소에서 이슈가 사용 중지되었습니다 엑세스 토큰 기본 인증 로그인 종류 선택 @@ -358,14 +358,14 @@ FastHub Slack에 참여하시겠습니까? 초대 성공 답글 - 이미지 불러오기에 실패했습니다. + 이미지 불러오기에 실패했습니다 병합 희망 구독자 검토자 검토자 없음 - %2$s%3$s을 사용하여 %1$s에서 보냈습니다. + %2$s%3$s을 사용하여 %1$s에서 보냈습니다 서명 활성화 - 서명을 통해 전송을 활성화합니다. + 서명을 통해 전송을 활성화합니다 서명 상자 활성화 확인란을 사용하여 텍스트 편집기에서 서명을 활성화/비활성화 할 수 있습니다. 태그 @@ -379,7 +379,7 @@ direct link to the header image.\n\nOr even simpler, just use the built-in image chooser!
배너 선택 - 이미지를 불러오는데 오류가 발생했습니다. 다시 시도하세요. + 이미지 불러오기 오류, 다시 시도하세요 급상승 GitHub 제한 때문에 emojies로 정렬이 실제로 작동하지 않습니다 위로 스크롤 @@ -411,11 +411,11 @@ 소유자 원본 포스터 검토 취소 - 테마에서 색상 네비게이션 바를 비활성화합니다. + 테마에서 색상 네비게이션 바를 비활성화합니다 무색 네비게이션 - 알림 소리를 선택합니다. + 알림 소리를 선택합니다 알림 소리 선택 - GIF 자동 재생 사용을 비활성화합니다. + GIF 자동 재생 사용을 비활성화합니다 요청된 변경 사항 Google Play 서비스를 사용할 수 없음 @@ -425,6 +425,6 @@ SHA 복사 코드로 보기 앱 애니메이션 - 모든 앱 애니메이션을 비활성화합니다. - 이 Pull Request는 현재 병합될 수 없습니다. + 모든 앱 애니메이션을 비활성화합니다 + 이 풀 리퀘스트는 현재 병합될 수 없습니다 From 0fa42eeef9a4cd9489445857c495dd724d4db25b Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sun, 3 Sep 2017 12:38:14 +0200 Subject: [PATCH 28/49] this commit fixes #918 & fixes #921 --- .../provider/markdown/MarkDownProvider.java | 2 +- .../fastaccess/provider/theme/ThemeEngine.kt | 3 +- .../details/PullRequestPagerActivity.java | 14 +- .../details/PullRequestPagerMvp.java | 3 +- .../reviews/changes/ReviewChangesActivity.kt | 133 +++++++----------- .../reviews/changes/ReviewChangesMvp.kt | 4 + .../ui/widgets/markdown/MarkDownLayout.kt | 9 +- .../recyclerview/DynamicRecyclerView.java | 1 + 8 files changed, 75 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java index 0282ace24..f83ccd947 100644 --- a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java +++ b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java @@ -308,7 +308,7 @@ public static boolean isArchive(@Nullable String name) { return false; } - private static void insertAtCursor(@NonNull EditText editText, @NonNull String text) { + public static void insertAtCursor(@NonNull EditText editText, @NonNull String text) { String oriContent = editText.getText().toString(); int index = editText.getSelectionStart() >= 0 ? editText.getSelectionStart() : 0; StringBuilder builder = new StringBuilder(oriContent); diff --git a/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt b/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt index 362efc7f4..74d303294 100644 --- a/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt +++ b/app/src/main/java/com/fastaccess/provider/theme/ThemeEngine.kt @@ -13,7 +13,6 @@ import com.fastaccess.ui.base.BaseActivity import com.fastaccess.ui.modules.login.LoginActivity import com.fastaccess.ui.modules.login.chooser.LoginChooserActivity import com.fastaccess.ui.modules.main.donation.DonateActivity -import com.fastaccess.ui.modules.reviews.changes.ReviewChangesActivity /** * Created by Kosh on 07 Jun 2017, 6:52 PM @@ -267,5 +266,5 @@ object ThemeEngine { } private fun hasTheme(activity: BaseActivity<*, *>) = (activity is LoginChooserActivity || activity is LoginActivity || - activity is DonateActivity || activity is ReviewChangesActivity) + activity is DonateActivity) } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java index 23bb22f4e..51bac251f 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java @@ -171,9 +171,6 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } else { getPresenter().onRefresh(); } - } else if (requestCode == BundleConstant.REVIEW_REQUEST_CODE) { - hideAndClearReviews(); - pager.setCurrentItem(0); } } } @@ -444,6 +441,11 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo commentEditorFragment.onCreateComment(text, bundle); } + @Override public void onSuccessfullyReviewed() { + hideAndClearReviews(); + pager.setCurrentItem(0); + } + protected void hideAndClearReviews() { getPresenter().getCommitComment().clear(); AnimHelper.mimicFabVisibility(false, prReviewHolder, null); @@ -464,9 +466,11 @@ private void addPrReview(@NonNull View view) { requestModel.setComments(getPresenter().getCommitComment().isEmpty() ? null : getPresenter().getCommitComment()); requestModel.setCommitId(pullRequest.getHead().getSha()); boolean isAuthor = author != null && Login.getUser().getLogin().equalsIgnoreCase(author.getLogin()); - ReviewChangesActivity.Companion.startForResult(this, view, requestModel, getPresenter().getRepoId(), + + ReviewChangesActivity.Companion.startForResult(requestModel, getPresenter().getRepoId(), getPresenter().getLogin(), pullRequest.getNumber(), isAuthor, isEnterprise(), pullRequest.isMerged() - || pullRequest.getState() == IssueState.closed); + || pullRequest.getState() == IssueState.closed) + .show(getSupportFragmentManager(), ReviewChangesActivity.class.getSimpleName()); } private void initTabs(@NonNull PullRequest pullRequest) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerMvp.java index 170e0c5ff..c3e4cba09 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerMvp.java @@ -17,6 +17,7 @@ import com.fastaccess.ui.modules.repos.issues.issue.details.IssuePagerMvp; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.details.files.PullRequestFilesMvp; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.merge.MergePullReqeustMvp; +import com.fastaccess.ui.modules.reviews.changes.ReviewChangesMvp; import com.fastaccess.ui.widgets.SpannableBuilder; import java.util.ArrayList; @@ -30,7 +31,7 @@ public interface PullRequestPagerMvp { interface View extends BaseMvp.FAView, LabelsMvp.SelectedLabelsListener, AssigneesMvp.SelectedAssigneesListener, MergePullReqeustMvp.MergeCallback, IssuePagerMvp.IssuePrCallback, PullRequestFilesMvp.PatchCallback, - CommentEditorFragment.CommentListener { + CommentEditorFragment.CommentListener, ReviewChangesMvp.ReviewSubmissionCallback { void onSetupIssue(boolean update); diff --git a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt index 8e428ebd6..d25a0c7ca 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt @@ -1,28 +1,25 @@ package com.fastaccess.ui.modules.reviews.changes -import android.app.Activity -import android.content.Intent +import android.content.Context import android.os.Bundle -import android.support.annotation.StringRes +import android.support.v4.content.ContextCompat import android.support.v7.widget.Toolbar -import android.view.Menu -import android.view.MenuItem import android.view.View import android.widget.Spinner import butterknife.BindView import com.evernote.android.state.State import com.fastaccess.R import com.fastaccess.data.dao.ReviewRequestModel -import com.fastaccess.helper.* -import com.fastaccess.provider.theme.ThemeEngine -import com.fastaccess.ui.base.BaseActivity +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Bundler +import com.fastaccess.helper.InputHelper +import com.fastaccess.ui.base.BaseDialogFragment import com.fastaccess.ui.modules.editor.comment.CommentEditorFragment -import com.fastaccess.ui.widgets.dialog.ProgressDialogFragment /** * Created by Kosh on 25 Jun 2017, 1:25 AM */ -class ReviewChangesActivity : BaseActivity(), ReviewChangesMvp.View { +class ReviewChangesActivity : BaseDialogFragment(), ReviewChangesMvp.View { @BindView(R.id.toolbar) lateinit var toolbar: Toolbar @BindView(R.id.reviewMethod) lateinit var spinner: Spinner @@ -31,55 +28,53 @@ class ReviewChangesActivity : BaseActivity { + toolbar.navigationIcon = ContextCompat.getDrawable(context, R.drawable.ic_clear) + toolbar.inflateMenu(R.menu.done_menu) + toolbar.setNavigationOnClickListener { dismiss() } + toolbar.setOnMenuItemClickListener { + if (it.itemId == R.id.submit) { if (spinner.selectedItemPosition != 0 && commentEditorFragment?.getEditText()?.text.isNullOrEmpty()) { commentEditorFragment?.getEditText()?.error = getString(R.string.required_field) } else { @@ -87,47 +82,25 @@ class ReviewChangesActivity : BaseActivity { - return super.onOptionsItemSelected(item) } + return@setOnMenuItemClickListener true + } + + if (isAuthor || isClosed) { + spinner.setSelection(2, true) + spinner.isEnabled = false } } override fun onSuccessfullySubmitted() { - setResult(Activity.RESULT_OK) - finish() + subimssionCallback?.onSuccessfullyReviewed() + dismiss() } override fun onErrorSubmitting() { showErrorMessage(getString(R.string.network_error)) } - override fun showProgress(@StringRes resId: Int) { - var msg = getString(R.string.in_progress) - if (resId != 0) { - msg = getString(resId) - } - if (!isProgressShowing && !isFinishing) { - var fragment = AppHelper.getFragmentByTag(supportFragmentManager, - ProgressDialogFragment.TAG) as ProgressDialogFragment? - if (fragment == null) { - isProgressShowing = true - fragment = ProgressDialogFragment.newInstance(msg, false) - fragment.show(supportFragmentManager, ProgressDialogFragment.TAG) - } - } - } - - override fun hideProgress() { - val fragment = AppHelper.getFragmentByTag(supportFragmentManager, ProgressDialogFragment.TAG) as ProgressDialogFragment? - if (fragment != null) { - isProgressShowing = false - fragment.dismiss() - } - } - override fun showMessage(titleRes: Int, msgRes: Int) { hideProgress() super.showMessage(titleRes, msgRes) @@ -148,8 +121,9 @@ class ReviewChangesActivity : BaseActivity ViewHelper.showKeyboard(editText) emoji?.let { - editText.setText(if (editText.text.isNullOrEmpty()) { - ":${it.aliases[0]}:" - } else { - "${editText.text} :${it.aliases[0]}:" - }) + MarkDownProvider.insertAtCursor(editText, it.aliases[0]) editText.setSelection(editText.text.length) } } diff --git a/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/DynamicRecyclerView.java b/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/DynamicRecyclerView.java index 27f3de755..f110d1fa9 100644 --- a/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/DynamicRecyclerView.java +++ b/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/DynamicRecyclerView.java @@ -64,6 +64,7 @@ public DynamicRecyclerView(@NonNull Context context, AttributeSet attrs, int def } } + public void removeBottomDecoration() { if (bottomPaddingDecoration != null) { removeItemDecoration(bottomPaddingDecoration); From a71cfb8a88caa93280ef133b1491151b2d277dd0 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sun, 3 Sep 2017 12:39:32 +0200 Subject: [PATCH 29/49] hide progress & make it sticky --- .../ui/modules/reviews/changes/ReviewChangesActivity.kt | 1 + .../ui/modules/reviews/changes/ReviewChangesPresenter.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt index d25a0c7ca..5477272f5 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt @@ -93,6 +93,7 @@ class ReviewChangesActivity : BaseDialogFragment(), ReviewCha } else { sendToView { it.onErrorSubmitting() } } - }) + }, false) } } \ No newline at end of file From 0a78168ab11236bf8a1baa1ba30decf163498dae Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sun, 3 Sep 2017 12:49:43 +0200 Subject: [PATCH 30/49] fixed PR status & emoji alias --- .../ui/adapter/viewholder/PullStatusViewHolder.java | 2 -- .../timeline/timeline/PullRequestTimelinePresenter.java | 5 +++-- .../com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java index 9cca05caf..82fb891a5 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java @@ -11,7 +11,6 @@ import com.fastaccess.data.dao.PullRequestStatusModel; import com.fastaccess.data.dao.types.StatusStateType; import com.fastaccess.helper.InputHelper; -import com.fastaccess.helper.Logger; import com.fastaccess.provider.scheme.SchemeParser; import com.fastaccess.ui.widgets.FontTextView; import com.fastaccess.ui.widgets.ForegroundImageView; @@ -45,7 +44,6 @@ public static PullStatusViewHolder newInstance(@NonNull ViewGroup parent) { } @Override public void bind(@NonNull PullRequestStatusModel pullRequestStatusModel) { - Logger.e(pullRequestStatusModel.getState()); if (pullRequestStatusModel.getState() != null) { StatusStateType stateType = pullRequestStatusModel.getState(); stateImage.setImageResource(stateType.getDrawableRes()); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java index 53f5807c4..8e17d13f1 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java @@ -26,6 +26,7 @@ import com.fastaccess.helper.ActivityHelper; import com.fastaccess.helper.BundleConstant; import com.fastaccess.helper.InputHelper; +import com.fastaccess.helper.Logger; import com.fastaccess.provider.rest.RestProvider; import com.fastaccess.provider.scheme.SchemeParser; import com.fastaccess.provider.timeline.CommentsHelper; @@ -361,8 +362,8 @@ public class PullRequestTimelinePresenter extends BasePresenter models = TimelineConverter.INSTANCE.convert(response.getItems(), comments); if (page == 1 && status != null) { - status.setMergable(parameter.isMergable()); - if (status.getState() != null && status.getStatuses() != null) models.add(0, new TimelineModel(status)); + status.setMergable(parameter.isMergeable()); + if (status.getState() != null) models.add(0, new TimelineModel(status)); } return models; } else { diff --git a/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt b/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt index cdec7b99e..979f9fa3c 100644 --- a/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt +++ b/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt @@ -140,7 +140,7 @@ class MarkDownLayout : LinearLayout { markdownListener?.getEditText()?.let { editText -> ViewHelper.showKeyboard(editText) emoji?.let { - MarkDownProvider.insertAtCursor(editText, it.aliases[0]) + MarkDownProvider.insertAtCursor(editText, ":${it.aliases[0]}:") editText.setSelection(editText.text.length) } } From d96a262982997f119596a888cb4a13c15c461e0b Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sun, 3 Sep 2017 13:05:59 +0200 Subject: [PATCH 31/49] this commit adds author association to comments. --- app/src/main/java/com/fastaccess/App.java | 2 +- .../data/dao/ReviewCommentModel.java | 27 +++++++++++++------ .../data/dao/model/AbstractComment.java | 3 +++ .../viewholder/CommitCommentsViewHolder.kt | 8 ++++++ .../viewholder/PullStatusViewHolder.java | 2 ++ .../viewholder/ReviewCommentsViewHolder.java | 21 +++++++++------ .../TimelineCommentsViewHolder.java | 21 +++++++++------ 7 files changed, 59 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/fastaccess/App.java b/app/src/main/java/com/fastaccess/App.java index 5b1f1453f..6182df764 100644 --- a/app/src/main/java/com/fastaccess/App.java +++ b/app/src/main/java/com/fastaccess/App.java @@ -72,7 +72,7 @@ private void setupPreference() { public ReactiveEntityStore getDataStore() { if (dataStore == null) { EntityModel model = Models.DEFAULT; - DatabaseSource source = new DatabaseSource(this, model, "FastHub-DB", 11); + DatabaseSource source = new DatabaseSource(this, model, "FastHub-DB", 12); Configuration configuration = source.getConfiguration(); if (BuildConfig.DEBUG) { source.setTableCreationMode(TableCreationMode.CREATE_NOT_EXISTS); diff --git a/app/src/main/java/com/fastaccess/data/dao/ReviewCommentModel.java b/app/src/main/java/com/fastaccess/data/dao/ReviewCommentModel.java index fbfb288eb..eaf66c5db 100644 --- a/app/src/main/java/com/fastaccess/data/dao/ReviewCommentModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/ReviewCommentModel.java @@ -30,6 +30,7 @@ public class ReviewCommentModel implements Parcelable { private String htmlUrl; private String pullRequestUrl; private ReactionsModel reactions; + private String authorAssociation; public ReviewCommentModel() {} @@ -161,6 +162,14 @@ public void setReactions(ReactionsModel reactions) { this.reactions = reactions; } + public String getAuthorAssociation() { + return authorAssociation; + } + + public void setAuthorAssociation(String authorAssociation) { + this.authorAssociation = authorAssociation; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; @@ -174,6 +183,14 @@ public void setReactions(ReactionsModel reactions) { return (int) (id ^ (id >>> 32)); } + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { @@ -194,6 +211,7 @@ public void setReactions(ReactionsModel reactions) { dest.writeString(this.htmlUrl); dest.writeString(this.pullRequestUrl); dest.writeParcelable(this.reactions, flags); + dest.writeString(this.authorAssociation); } protected ReviewCommentModel(Parcel in) { @@ -216,6 +234,7 @@ protected ReviewCommentModel(Parcel in) { this.htmlUrl = in.readString(); this.pullRequestUrl = in.readString(); this.reactions = in.readParcelable(ReactionsModel.class.getClassLoader()); + this.authorAssociation = in.readString(); } public static final Creator CREATOR = new Creator() { @@ -223,12 +242,4 @@ protected ReviewCommentModel(Parcel in) { @Override public ReviewCommentModel[] newArray(int size) {return new ReviewCommentModel[size];} }; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } } diff --git a/app/src/main/java/com/fastaccess/data/dao/model/AbstractComment.java b/app/src/main/java/com/fastaccess/data/dao/model/AbstractComment.java index eb00a9914..ffcb09919 100644 --- a/app/src/main/java/com/fastaccess/data/dao/model/AbstractComment.java +++ b/app/src/main/java/com/fastaccess/data/dao/model/AbstractComment.java @@ -54,6 +54,7 @@ String issueId; String pullRequestId; @Convert(ReactionsConverter.class) ReactionsModel reactions; + String authorAssociation; public static Disposable saveForGist(@NonNull List models, @NonNull String gistId) { return RxHelper.getSingle(Single.fromPublisher(s -> { @@ -188,6 +189,7 @@ public static Single> getPullRequestComments(@NonNull String repoI dest.writeString(this.issueId); dest.writeString(this.pullRequestId); dest.writeParcelable(this.reactions, flags); + dest.writeString(this.authorAssociation); } protected AbstractComment(Parcel in) { @@ -211,6 +213,7 @@ protected AbstractComment(Parcel in) { this.issueId = in.readString(); this.pullRequestId = in.readString(); this.reactions = in.readParcelable(ReactionsModel.class.getClassLoader()); + this.authorAssociation = in.readString(); } public static final Creator CREATOR = new Creator() { diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitCommentsViewHolder.kt b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitCommentsViewHolder.kt index 9cf75c62f..6b26f0d50 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitCommentsViewHolder.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/CommitCommentsViewHolder.kt @@ -4,6 +4,7 @@ import android.support.transition.ChangeBounds import android.support.transition.TransitionManager import android.view.View import android.view.ViewGroup +import android.widget.TextView import butterknife.BindView import com.fastaccess.R import com.fastaccess.data.dao.model.Comment @@ -43,6 +44,7 @@ class CommitCommentsViewHolder private constructor(view: View, adapter: BaseRecy @BindView(R.id.commentMenu) lateinit var commentMenu: ForegroundImageView @BindView(R.id.comment) lateinit var comment: FontTextView @BindView(R.id.commentOptions) lateinit var commentOptions: View + @BindView(R.id.owner) lateinit var owner: TextView override fun onClick(v: View) { if (v.id == R.id.toggle || v.id == R.id.toggleHolder) { @@ -68,6 +70,12 @@ class CommitCommentsViewHolder private constructor(view: View, adapter: BaseRecy } else { comment.text = "" } + if (t.authorAssociation != null && !"none".equals(t.authorAssociation, ignoreCase = true)) { + owner.text = t.authorAssociation.toLowerCase() + owner.visibility = View.VISIBLE + } else { + owner.visibility = View.GONE + } if (t.createdAt == t.updatedAt) { date.text = String.format("%s %s", ParseDateFormat.getTimeAgo(t.updatedAt), itemView .resources.getString(R.string.edited)) diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java index 82fb891a5..6bb63784c 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullStatusViewHolder.java @@ -11,6 +11,7 @@ import com.fastaccess.data.dao.PullRequestStatusModel; import com.fastaccess.data.dao.types.StatusStateType; import com.fastaccess.helper.InputHelper; +import com.fastaccess.helper.Logger; import com.fastaccess.provider.scheme.SchemeParser; import com.fastaccess.ui.widgets.FontTextView; import com.fastaccess.ui.widgets.ForegroundImageView; @@ -44,6 +45,7 @@ public static PullStatusViewHolder newInstance(@NonNull ViewGroup parent) { } @Override public void bind(@NonNull PullRequestStatusModel pullRequestStatusModel) { + Logger.e(pullRequestStatusModel.getState(), pullRequestStatusModel.isMergable()); if (pullRequestStatusModel.getState() != null) { StatusStateType stateType = pullRequestStatusModel.getState(); stateImage.setImageResource(stateType.getDrawableRes()); diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewCommentsViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewCommentsViewHolder.java index 5753b250c..dd6eeff40 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewCommentsViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ReviewCommentsViewHolder.java @@ -108,18 +108,23 @@ public static ReviewCommentsViewHolder newInstance(ViewGroup viewGroup, BaseRecy avatarView.setUrl(commentModel.getUser().getAvatarUrl(), commentModel.getUser().getLogin(), commentModel.getUser() .isOrganizationType(), LinkParserHelper.isEnterprise(commentModel.getHtmlUrl())); name.setText(commentModel.getUser().getLogin()); - boolean isRepoOwner = TextUtils.equals(commentModel.getUser().getLogin(), repoOwner); - if (isRepoOwner) { + if (commentModel.getAuthorAssociation() != null && !"none".equalsIgnoreCase(commentModel.getAuthorAssociation())) { + owner.setText(commentModel.getAuthorAssociation().toLowerCase()); owner.setVisibility(View.VISIBLE); - owner.setText(R.string.owner); } else { - boolean isPoster = TextUtils.equals(commentModel.getUser().getLogin(), poster); - if (isPoster) { + boolean isRepoOwner = TextUtils.equals(commentModel.getUser().getLogin(), repoOwner); + if (isRepoOwner) { owner.setVisibility(View.VISIBLE); - owner.setText(R.string.original_poster); + owner.setText(R.string.owner); } else { - owner.setText(null); - owner.setVisibility(View.GONE); + boolean isPoster = TextUtils.equals(commentModel.getUser().getLogin(), poster); + if (isPoster) { + owner.setVisibility(View.VISIBLE); + owner.setText(R.string.original_poster); + } else { + owner.setText(null); + owner.setVisibility(View.GONE); + } } } } diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/TimelineCommentsViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/TimelineCommentsViewHolder.java index bf92290ec..920fa1e3b 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/TimelineCommentsViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/TimelineCommentsViewHolder.java @@ -120,18 +120,23 @@ public static TimelineCommentsViewHolder newInstance(@NonNull ViewGroup viewGrou avatar.setUrl(commentsModel.getUser().getAvatarUrl(), commentsModel.getUser().getLogin(), false, LinkParserHelper.isEnterprise(commentsModel.getHtmlUrl())); name.setText(commentsModel.getUser() != null ? commentsModel.getUser().getLogin() : "Anonymous"); - boolean isRepoOwner = TextUtils.equals(commentsModel.getUser().getLogin(), repoOwner); - if (isRepoOwner) { + if (commentsModel.getAuthorAssociation() != null && !"none".equalsIgnoreCase(commentsModel.getAuthorAssociation())) { + owner.setText(commentsModel.getAuthorAssociation().toLowerCase()); owner.setVisibility(View.VISIBLE); - owner.setText(R.string.owner); } else { - boolean isPoster = TextUtils.equals(commentsModel.getUser().getLogin(), poster); - if (isPoster) { + boolean isRepoOwner = TextUtils.equals(commentsModel.getUser().getLogin(), repoOwner); + if (isRepoOwner) { owner.setVisibility(View.VISIBLE); - owner.setText(R.string.original_poster); + owner.setText(R.string.owner); } else { - owner.setText(null); - owner.setVisibility(View.GONE); + boolean isPoster = TextUtils.equals(commentsModel.getUser().getLogin(), poster); + if (isPoster) { + owner.setVisibility(View.VISIBLE); + owner.setText(R.string.original_poster); + } else { + owner.setText(null); + owner.setVisibility(View.GONE); + } } } } else { From 3549271c657d7e781b113f287f59c3da515c7d91 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sun, 3 Sep 2017 15:44:30 +0200 Subject: [PATCH 32/49] fix emoji cursor after insertion --- .../com/fastaccess/provider/markdown/MarkDownProvider.java | 4 ++-- .../com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java index f83ccd947..7f2088d33 100644 --- a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java +++ b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java @@ -252,7 +252,7 @@ public static void addPhoto(@NonNull EditText editText) { } public static void addPhoto(@NonNull EditText editText, @NonNull String title, @NonNull String link) { - String result = "![" + InputHelper.toString(title) + "](" + InputHelper.toString(link) + ")\n"; + String result = "![" + InputHelper.toString(title) + "](" + InputHelper.toString(link) + ")"; insertAtCursor(editText, result); } @@ -261,7 +261,7 @@ public static void addLink(@NonNull EditText editText) { } public static void addLink(@NonNull EditText editText, @NonNull String title, @NonNull String link) { - String result = "[" + InputHelper.toString(title) + "](" + InputHelper.toString(link) + ")\n"; + String result = "[" + InputHelper.toString(title) + "](" + InputHelper.toString(link) + ")"; insertAtCursor(editText, result); } diff --git a/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt b/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt index 979f9fa3c..c4f09ca18 100644 --- a/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt +++ b/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt @@ -87,8 +87,10 @@ class MarkDownLayout : LinearLayout { Snackbar.make(this, R.string.error_highlighting_editor, Snackbar.LENGTH_SHORT).show() } else { when { - v.id == R.id.link -> EditorLinkImageDialogFragment.newInstance(true).show(it.fragmentManager(), "BannerDialogFragment") - v.id == R.id.image -> EditorLinkImageDialogFragment.newInstance(false).show(it.fragmentManager(), "BannerDialogFragment") + v.id == R.id.link -> EditorLinkImageDialogFragment.newInstance(true) + .show(it.fragmentManager(), "EditorLinkImageDialogFragment") + v.id == R.id.image -> EditorLinkImageDialogFragment.newInstance(false) + .show(it.fragmentManager(), "EditorLinkImageDialogFragment") v.id == R.id.addEmoji -> { ViewHelper.hideKeyboard(it.getEditText()) EmojiBottomSheet().show(it.fragmentManager(), "EmojiBottomSheet") @@ -141,7 +143,6 @@ class MarkDownLayout : LinearLayout { ViewHelper.showKeyboard(editText) emoji?.let { MarkDownProvider.insertAtCursor(editText, ":${it.aliases[0]}:") - editText.setSelection(editText.text.length) } } } From bf8d05f1277a2b1a1d8c66f2ee9deef0abc2d457 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Sun, 3 Sep 2017 16:31:53 +0200 Subject: [PATCH 33/49] releasing 4.2.0 --- app/build.gradle | 4 +- .../data/dao/FragmentPagerAdapterModel.java | 4 +- app/src/main/res/raw/changelog.html | 81 ++++++++++--------- 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6937184c0..2dbbecd87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "com.fastaccess.github" minSdkVersion 21 targetSdkVersion 26 - versionCode 410 - versionName "4.1.0" + versionCode 420 + versionName "4.2.0" buildConfigString "GITHUB_CLIENT_ID", (buildProperties.secrets['github_client_id'] | buildProperties.notThere['github_client_id']).string buildConfigString "GITHUB_SECRET", (buildProperties.secrets['github_secret'] | buildProperties.notThere['github_secret']).string buildConfigString "IMGUR_CLIENT_ID", (buildProperties.secrets['imgur_client_id'] | buildProperties.notThere['imgur_client_id']).string diff --git a/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java b/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java index d5258b844..09eb11ff1 100644 --- a/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java @@ -218,8 +218,8 @@ private FragmentPagerAdapterModel(String title, Fragment fragment) { return Stream.of(new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeLight)), new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeDark)), new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeAmlod)), - new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeBluish)), - new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeMidnight))) + new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeBluish))) +// new FragmentPagerAdapterModel("", ThemeFragment.Companion.newInstance(R.style.ThemeMidnight))) .collect(Collectors.toList()); } diff --git a/app/src/main/res/raw/changelog.html b/app/src/main/res/raw/changelog.html index 5ccaf8194..4ee8ca706 100644 --- a/app/src/main/res/raw/changelog.html +++ b/app/src/main/res/raw/changelog.html @@ -6,48 +6,49 @@ -

FastHub changelog -

-

Version 4.1.0 (Gist Editing, Gif images) -

-

Bugs , Enhancements & new Features (4.1.0) -

+

FastHub changelog

+

Version 4.2.0 (Create, Edit & Delete files (make Commits))

+
+

Reporting Issues or Feature Requests in Google Play review section they’ll be ignored or might even gets your account to be blocked to use + FastHub, you are using the app for GitHub, so please do report the issues in FastHub repo instead, a quicker way to report an issue in FastHub + repo is added to the Drawer Menu, PLEASE USE IT.

+
+

Bugs , Enhancements & new Features (3.2.0)

    -
  • (New) Added Pinned Repos from GitHub Profile.
  • -
  • (New) Gist Editing.
  • -
  • (New) Allow uploading multiple files to a gist.
  • -
  • (New) GIF Images in comments.
  • -
  • (New) Added quick commenting in Issues, Prs, Commits & Gists.
  • -
  • (New) Added a custom ringtone for notifications.
  • -
  • (New) Bulgarian language support thanks to (@petarov)
  • -
  • (New) French language support thanks to (@ptt-homme)
  • -
  • (New) Add/Search & Render GitHub emojis.
  • -
  • (New) Clicking on Labels, assignees, milestones will open up search with that specific event.
  • -
  • (New) Rendering SVG images in webview.
  • -
  • (Enhancement) Source files are now shown.
  • -
  • (Enhancement) Confirm to mark all Notifications as read
  • -
  • (Enhancement) Handling search and Invitations links.
  • -
  • (Enhancement) Comments on Pr commits are now shown
  • -
  • (Enhancement) FastScroller everywhere
  • -
  • (Enhancement) Clicking on topics will search topics.
  • -
  • (Enhancement) More Markdown support, strikethrough, hr & others.
  • -
  • (Enhancement) Table Rendering in comments.
  • -
  • (Enhancement) Rewrite of the PR & Issue timeline to use less API calls (will load much faster).
  • -
  • (Enhancement) PR Reviews sort order.
  • -
  • (Enhancement) Markdown editor everywhere.
  • -
  • (Enhancement) Correct wording to match github.
  • -
  • (Enhancement) Better handling of cross-ref events
  • -
  • (Fix) Added search icon in search places where clicking on search on the keyboard doesn’t work for some devices.
  • -
  • (Fix) Readme Appbar flicker on scroll thanks to (@TheAndroidMaster)
  • -
  • (Fix) Reload after closing an issue
  • -
  • (Fix) Private repos for organizations.
  • -
  • (Fix) Labels being removed it some cases.
  • -
  • (Fix) Embedding a fork of Firebase job-dispatcher with fixes to avoid crashes happen on some devices.
  • -
  • (Fix) Removed Signature - Checkbox & made signature to be disabled by default. thanks to @jakeWharton -
  • -
  • A lot more features, enhancements & bug fixes.
  • +
  • (New) Make commits on repos from FastHub (this only applies for repo owners so far) PRO
  • +
  • (New) Long pressing files/directories will open up their Commit/Git History.
  • +
  • (New) Indicates PR review line has a comment.
  • +
  • (New) Showing number of addition, deletion & files in PR files tab.
  • +
  • (New) Untoggle MD rendering to see their actual code.
  • +
  • (New) Settings option to disable Animations in FastHub.
  • +
  • (New) Copy SHA from Commit.
  • +
  • (New) Forks, Watchers & Stargazers links handling.
  • +
  • (Enhancement) Loading larger number of comments per page.
  • +
  • (Enhancement) Hide fab while scrolling in Issues & PR tab.
  • +
  • (Fix) PR Status where it got somehow broken in previous release.
  • +
  • (Fix) A text under PR status will indicates if the PR is mergable or not.
  • +
  • (Fix) Comments text selection where sometime they aren’t selectable.
  • +
  • (Fix) PR Review footer is invisible in (4.1.0)
  • +
  • (Fix) Cursor position after inserting emoji, links & images.
  • +
  • (Fix) Teal customized accent was displayed as green
  • +
  • (Fix) Some crashes from the crash report.
  • +
  • (Fix) Lots of bug fixes
  • +
  • There are more stuff are not mentioned, find them out :p
+

What left in FastHub?

+

So far, FastHub has almost all the features from GitHub except for Project Cards next release will hopefully includes that & + then I’ll be only releasing when major bugs is fixed or major feature implemented.

+

How old is FastHub now?

+
+

FastHub is now 5 months & 4 days old, since v1.0.0 it has really grow and became more and more better on each release and this is all + happened because of the community either via reporting bugs, feature requests or even give hand to fix things or implement things in + the app, + I’m really grateful for such community, thank you guys a lot.

+
+
+

Thanks to everyone who contributed either via reporting bugs or via code contribution

+
+

Thank you very much

\ No newline at end of file From a25f4b6c586edec08acdde419756eaad47fd54db Mon Sep 17 00:00:00 2001 From: Astro Date: Sun, 3 Sep 2017 23:57:21 +0900 Subject: [PATCH 34/49] Update strings.xml --- app/src/main/res/values-ko/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 68ba7cef9..17c3cfb4c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -416,7 +416,7 @@ 알림 소리를 선택합니다 알림 소리 선택 GIF 자동 재생 사용을 비활성화합니다 - + GIF 자동 재생 사용 비활성화 요청된 변경 사항 Google Play 서비스를 사용할 수 없음 Gist 수정 From cab9846b4af093b411c2b12897a02f6fe30135d4 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Thu, 7 Sep 2017 19:13:45 +0200 Subject: [PATCH 35/49] this commit fixes #933, fixes #932 , fixes #931 fixes #929 --- .gitignore | 6 +-- .../fastaccess/data/dao/types/FilesType.java | 3 +- .../provider/markdown/MarkDownProvider.java | 2 +- .../NotificationSchedulerJobTask.java | 39 +++++++++++------ .../com/fastaccess/ui/adapter/EmojiAdapter.kt | 1 + .../ui/modules/editor/EditorPresenter.kt | 2 +- .../editor/comment/CommentEditorFragment.kt | 6 +-- .../ui/modules/gists/gist/GistActivity.java | 11 +++-- .../gist/comments/GistCommentsFragment.java | 1 + .../commit/details/CommitPagerActivity.java | 12 ++++-- .../comments/CommitCommentsFragment.java | 1 + .../repos/code/files/RepoFilesPresenter.java | 1 + .../repos/git/EditRepoFilePresenter.kt | 1 - .../issue/details/IssuePagerActivity.java | 4 ++ .../timeline/IssueTimelineFragment.java | 1 + .../details/PullRequestPagerActivity.java | 4 ++ .../timeline/PullRequestTimelineFragment.java | 1 + .../reviews/changes/ReviewChangesActivity.kt | 8 ++++ .../layout/issue_popup_layout.xml | 6 --- app/src/main/res/raw/changelog.html | 43 +++++++++++++------ 20 files changed, 98 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index bc16cd71b..3133b75a7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,10 @@ /local.properties .DS_Store /build -/captures -.externalNativeBuild /gradle.properties /.idea/ /app/google-services.json -/app/db/ /app/build/ /app/src/main/res/values/secrets.xml /app/fastaccess-key -fast-for-github-firebase-crashreporting-7lngx-6b5be91d98.json -/fastScroller/build/ +/jobdispatcher/build/ diff --git a/app/src/main/java/com/fastaccess/data/dao/types/FilesType.java b/app/src/main/java/com/fastaccess/data/dao/types/FilesType.java index 89768701a..b36dd012f 100644 --- a/app/src/main/java/com/fastaccess/data/dao/types/FilesType.java +++ b/app/src/main/java/com/fastaccess/data/dao/types/FilesType.java @@ -12,7 +12,8 @@ public enum FilesType { file(R.drawable.ic_file_document), dir(R.drawable.ic_folder), blob(R.drawable.ic_file_document), - tree(R.drawable.ic_folder); + tree(R.drawable.ic_folder), + symlink(R.drawable.ic_file_document); int icon; diff --git a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java index 7f2088d33..907c5973e 100644 --- a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java +++ b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java @@ -41,7 +41,7 @@ public class MarkDownProvider { }; private static final String[] ARCHIVE_EXTENSIONS = { - ".zip", ".7z", ".rar", ".tar.gz", ".tgz", ".tar.Z", ".tar.bz2", ".tbz2", ".tar.lzma", ".tlz", ".apk", ".jar", ".dmg" + ".zip", ".7z", ".rar", ".tar.gz", ".tgz", ".tar.Z", ".tar.bz2", ".tbz2", ".tar.lzma", ".tlz", ".apk", ".jar", ".dmg", ".pdf" }; private MarkDownProvider() {} diff --git a/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java b/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java index 006afb244..6940d85bf 100644 --- a/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java +++ b/app/src/main/java/com/fastaccess/provider/tasks/notification/NotificationSchedulerJobTask.java @@ -1,12 +1,14 @@ + package com.fastaccess.provider.tasks.notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; @@ -188,12 +190,13 @@ private void finishJob(JobParameters job) { } private void showNotificationWithoutComment(Context context, int accentColor, Notification thread, String iconUrl) { - withoutComments(null, thread, context, accentColor); + withoutComments(thread, context, accentColor); } - private void withoutComments(Bitmap bitmap, Notification thread, Context context, int accentColor) { - android.app.Notification toAdd = getNotification(thread.getSubject().getTitle(), thread.getRepository().getFullName()) - .setLargeIcon(bitmap == null ? BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher) : bitmap) + private void withoutComments(Notification thread, Context context, int accentColor) { + android.app.Notification toAdd = getNotification(thread.getSubject().getTitle(), thread.getRepository().getFullName(), + thread.getRepository() != null ? thread.getRepository().getFullName() : "general") + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentIntent(getPendingIntent(thread.getId(), thread.getSubject().getUrl())) .addAction(R.drawable.ic_github, context.getString(R.string.open), getPendingIntent(thread.getId(), thread .getSubject().getUrl())) @@ -208,12 +211,13 @@ private void withoutComments(Bitmap bitmap, Notification thread, Context context } private void getNotificationWithComment(Context context, int accentColor, Notification thread, Comment comment, String url) { - withComments(null, comment, context, thread, accentColor); + withComments(comment, context, thread, accentColor); } - private void withComments(Bitmap bitmap, Comment comment, Context context, Notification thread, int accentColor) { - android.app.Notification toAdd = getNotification(comment.getUser() != null ? comment.getUser().getLogin() : "", comment.getBody()) - .setLargeIcon(bitmap == null ? BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher) : bitmap) + private void withComments(Comment comment, Context context, Notification thread, int accentColor) { + android.app.Notification toAdd = getNotification(comment.getUser() != null ? comment.getUser().getLogin() : "", comment.getBody(), + thread.getRepository() != null ? thread.getRepository().getFullName() : "general") + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setSmallIcon(R.drawable.ic_notification) .setStyle(new NotificationCompat.BigTextStyle() .setBigContentTitle(comment.getUser() != null ? comment.getUser().getLogin() : "") @@ -234,7 +238,8 @@ private void withComments(Bitmap bitmap, Comment comment, Context context, Notif private android.app.Notification getSummaryGroupNotification(@NonNull Notification thread, int accentColor, boolean toNotificationActivity) { PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(getApplicationContext(), NotificationActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); - return getNotification(thread.getSubject().getTitle(), thread.getRepository().getFullName()) + return getNotification(thread.getSubject().getTitle(), thread.getRepository().getFullName(), + thread.getRepository() != null ? thread.getRepository().getFullName() : "general") .setDefaults(PrefGetter.isNotificationSoundEnabled() ? NotificationCompat.DEFAULT_ALL : 0) .setSound(PrefGetter.getNotificationSound(), AudioManager.STREAM_NOTIFICATION) .setContentIntent(toNotificationActivity ? pendingIntent : getPendingIntent(thread.getId(), thread.getSubject().getUrl())) @@ -251,8 +256,8 @@ private android.app.Notification getSummaryGroupNotification(@NonNull Notificati .build(); } - private NotificationCompat.Builder getNotification(@NonNull String title, @NonNull String message) { - return new NotificationCompat.Builder(this, title) + private NotificationCompat.Builder getNotification(@NonNull String title, @NonNull String message, @NonNull String channelName) { + return new NotificationCompat.Builder(this, channelName) .setContentTitle(title) .setContentText(message) .setAutoCancel(true); @@ -260,7 +265,15 @@ private NotificationCompat.Builder getNotification(@NonNull String title, @NonNu private void showNotification(long id, android.app.Notification notification) { NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(InputHelper.getSafeIntId(id), notification); + if (notificationManager != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = new NotificationChannel(String.valueOf(id), + notification.getChannelId(), NotificationManager.IMPORTANCE_DEFAULT); + notificationChannel.setShowBadge(true); + notificationManager.createNotificationChannel(notificationChannel); + } + notificationManager.notify(InputHelper.getSafeIntId(id), notification); + } } private PendingIntent getReadOnlyPendingIntent(long id, @NonNull String url) { diff --git a/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt b/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt index 4d69efd5c..debe140ef 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/EmojiAdapter.kt @@ -47,6 +47,7 @@ class EmojiAdapter(listener: BaseViewHolder.OnItemClickListener) return results } + @Suppress("UNCHECKED_CAST") override fun publishResults(var1: CharSequence, results: Filter.FilterResults) { results.values?.let { insertItems(it as List) diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorPresenter.kt index 149dd7e25..df35994ba 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorPresenter.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorPresenter.kt @@ -93,7 +93,7 @@ class EditorPresenter : BasePresenter(), EditorMvp.Presenter { } private fun onEditReviewComment(reviewComment: EditReviewCommentModel, savedText: CharSequence, repoId: String, - login: String, issueNumber: Int, id: Long) { + login: String, @Suppress("UNUSED_PARAMETER") issueNumber: Int, id: Long) { if (!InputHelper.isEmpty(savedText)) { val requestModel = CommentRequestModel() requestModel.body = savedText.toString() diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt index 5e3fe89c0..df12f2b96 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt @@ -47,7 +47,6 @@ class CommentEditorFragment : BaseFragment implements Rep .flatMap(response -> { if (response != null && response.getItems() != null) { return Observable.fromIterable(response.getItems()) + .filter(repoFile -> repoFile.getType() != null) .sorted((repoFile, repoFile2) -> repoFile2.getType().compareTo(repoFile.getType())); } return Observable.empty(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt index 2ab4fa76c..f042656f5 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/git/EditRepoFilePresenter.kt @@ -32,7 +32,6 @@ class EditRepoFilePresenter : BasePresenter(), EditRepoFil } } - override fun onSubmit(text: String?, filename: String?, description: String?) { if (model?.login.isNullOrBlank() || model?.repoId.isNullOrBlank()) return diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java index ca6ab1d7d..0e5c7f4b3 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java @@ -376,6 +376,10 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo @Override public void onCreateComment(String text, Bundle bundle) {} + @SuppressWarnings("ConstantConditions") @Override public void onClearEditText() { + if (commentEditorFragment != null && commentEditorFragment.commentText != null) commentEditorFragment.commentText.setText(null); + } + private void hideShowFab() { if (getPresenter().isLocked() && !getPresenter().isOwner()) { getSupportFragmentManager().beginTransaction().hide(commentEditorFragment).commit(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java index 687dc7d8d..5507a98ff 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java @@ -267,6 +267,7 @@ public class IssueTimelineFragment extends BaseFragment @@ -154,8 +152,6 @@ style="@style/TextAppearance.AppCompat.Medium" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingBottom="@dimen/spacing_normal" - android:paddingTop="@dimen/spacing_normal" tools:text="Label 1"/> @@ -206,8 +202,6 @@ style="@style/TextAppearance.AppCompat.Subhead" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingBottom="@dimen/spacing_normal" - android:paddingTop="@dimen/spacing_normal" tools:text="Label 1"/> diff --git a/app/src/main/res/raw/changelog.html b/app/src/main/res/raw/changelog.html index 4ee8ca706..0f87b2a72 100644 --- a/app/src/main/res/raw/changelog.html +++ b/app/src/main/res/raw/changelog.html @@ -6,16 +6,22 @@ -

FastHub changelog

-

Version 4.2.0 (Create, Edit & Delete files (make Commits))

+

FastHub changelog +

+

Version 4.2.0 (Create, Edit & Delete files (make Commits)) +

-

Reporting Issues or Feature Requests in Google Play review section they’ll be ignored or might even gets your account to be blocked to use - FastHub, you are using the app for GitHub, so please do report the issues in FastHub repo instead, a quicker way to report an issue in FastHub - repo is added to the Drawer Menu, PLEASE USE IT.

+

Reporting Issues or Feature Requests in Google Play review section, will be ignored or might even get your account to be blocked from + FastHub. You are using an app for GitHub, which provides a proper way to report issues. + Please report the issues in FastHub repo instead, by opening the Drawer Menu, click about and click "Report an Issue" PLEASE USE IT. +

-

Bugs , Enhancements & new Features (3.2.0)

+

Bugs , Enhancements & new Features (3.2.0) +

    -
  • (New) Make commits on repos from FastHub (this only applies for repo owners so far) PRO
  • +
  • (New) Make commits on repos from FastHub (this only applies for repo owners so far) + PRO +
  • (New) Long pressing files/directories will open up their Commit/Git History.
  • (New) Indicates PR review line has a comment.
  • (New) Showing number of addition, deletion & files in PR files tab.
  • @@ -35,20 +41,29 @@

    Bugs , Enhancements &
  • (Fix) Lots of bug fixes
  • There are more stuff are not mentioned, find them out :p
-

What left in FastHub?

-

So far, FastHub has almost all the features from GitHub except for Project Cards next release will hopefully includes that & - then I’ll be only releasing when major bugs is fixed or major feature implemented.

-

How old is FastHub now?

+

What left in FastHub? +

+

So far, FastHub has almost all the features from GitHub except for + Project Cards + next release will hopefully includes that & + then I’ll be only releasing when major bugs is fixed or major feature implemented. +

+

How old is FastHub now? +

-

FastHub is now 5 months & 4 days old, since v1.0.0 it has really grow and became more and more better on each release and this is all +

FastHub is now 5 months & 4 days old, since v1.0.0 it has really grow and became more and more better on each release and this is + all happened because of the community either via reporting bugs, feature requests or even give hand to fix things or implement things in the app, - I’m really grateful for such community, thank you guys a lot.

+ I’m really grateful for such community, thank you guys a lot. +

Thanks to everyone who contributed either via reporting bugs or via code contribution

-

Thank you very much

+

+ Thank you very much +

\ No newline at end of file From 78698eedeac2141bba26afdab70a861ebf64f61e Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Thu, 7 Sep 2017 19:57:04 +0200 Subject: [PATCH 36/49] this commit fixes images in comments --- .../timeline/handler/DrawableHandler.java | 3 -- .../handler/drawable/DrawableGetter.java | 12 +------ .../handler/drawable/GlideDrawableTarget.java | 15 ++------- .../handler/drawable/UrlDrawable.java | 32 +------------------ 4 files changed, 5 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/DrawableHandler.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/DrawableHandler.java index ad479246b..21fa616c7 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/DrawableHandler.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/DrawableHandler.java @@ -8,7 +8,6 @@ import com.fastaccess.provider.timeline.handler.drawable.DrawableGetter; import net.nightwhistler.htmlspanner.TagNodeHandler; -import net.nightwhistler.htmlspanner.spans.CenterSpan; import org.htmlcleaner.TagNode; @@ -33,9 +32,7 @@ if (!InputHelper.isEmpty(src)) { builder.append(""); if (isNull()) return; - CenterSpan centerSpan = new CenterSpan(); DrawableGetter imageGetter = new DrawableGetter(textView); - builder.setSpan(centerSpan, start, builder.length(), SPAN_EXCLUSIVE_EXCLUSIVE); builder.setSpan(new ImageSpan(imageGetter.getDrawable(src)), start, builder.length(), SPAN_EXCLUSIVE_EXCLUSIVE); appendNewLine(builder); } diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java index 759fe787f..8b8078a58 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java @@ -18,7 +18,7 @@ * Created by Kosh on 22 Apr 2017, 7:44 PM */ -public class DrawableGetter implements Html.ImageGetter, Drawable.Callback { +public class DrawableGetter implements Html.ImageGetter { private WeakReference container; private final Set cachedTargets; @@ -42,16 +42,6 @@ public DrawableGetter(TextView tv) { return urlDrawable; } - @Override public void invalidateDrawable(@NonNull Drawable drawable) { - if (container != null && container.get() != null) { - container.get().invalidate(); - } - } - - @Override public void scheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable, long l) {} - - @Override public void unscheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable) {} - public void clear(@NonNull DrawableGetter drawableGetter) { if (drawableGetter.cachedTargets != null) { for (GlideDrawableTarget target : drawableGetter.cachedTargets) { diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java index 6dc63a6c2..35ff6c95c 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/GlideDrawableTarget.java @@ -24,18 +24,9 @@ class GlideDrawableTarget extends SimpleTarget { @Override public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) { if (container != null && container.get() != null) { TextView textView = container.get(); - float width; - float height; - if (resource.getIntrinsicWidth() >= textView.getWidth()) { - float downScale = (float) resource.getIntrinsicWidth() / textView.getWidth(); - width = (float) resource.getIntrinsicWidth() / downScale; - height = (float) resource.getIntrinsicHeight() / downScale; - } else { - float multiplier = (float) textView.getWidth() / resource.getIntrinsicWidth(); - width = (float) resource.getIntrinsicWidth() * multiplier; - height = (float) resource.getIntrinsicHeight() * multiplier; - } - Rect rect = new Rect(0, 0, (int) Math.round(width / 1.3), (int) Math.round(height / 1.3)); + float width = (float) (resource.getIntrinsicWidth() / 1.3); + float height = (float) (resource.getIntrinsicHeight() / 1.3); + Rect rect = new Rect(0, 0, Math.round(width), Math.round(height)); resource.setBounds(rect); urlDrawable.setBounds(rect); urlDrawable.setDrawable(resource); diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java index 3478859ec..b0bfac97e 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java @@ -3,11 +3,8 @@ import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import com.bumptech.glide.load.resource.gif.GifDrawable; - -class UrlDrawable extends BitmapDrawable implements Drawable.Callback { +class UrlDrawable extends BitmapDrawable { private Drawable drawable; @SuppressWarnings("deprecation") UrlDrawable() {} @@ -15,11 +12,6 @@ class UrlDrawable extends BitmapDrawable implements Drawable.Callback { @Override public void draw(Canvas canvas) { if (drawable != null) { drawable.draw(canvas); - if (drawable instanceof GifDrawable) { - if (!((GifDrawable) drawable).isRunning()) { - ((GifDrawable) drawable).start(); - } - } } } @@ -28,28 +20,6 @@ public Drawable getDrawable() { } public void setDrawable(Drawable drawable) { - if (this.drawable != null) { - this.drawable.setCallback(null); - } - drawable.setCallback(this); this.drawable = drawable; } - - @Override public void invalidateDrawable(@NonNull Drawable who) { - if (getCallback() != null) { - getCallback().invalidateDrawable(who); - } - } - - @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { - if (getCallback() != null) { - getCallback().scheduleDrawable(who, what, when); - } - } - - @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { - if (getCallback() != null) { - getCallback().unscheduleDrawable(who, what); - } - } } \ No newline at end of file From 365e5a2dd73f04e0098474072e32790ead40ba8d Mon Sep 17 00:00:00 2001 From: cozyplanes Date: Fri, 8 Sep 2017 20:25:23 +0900 Subject: [PATCH 37/49] Added unfinished Korean translations Banner translation not done yet. --- app/src/main/res/values-ko/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 17c3cfb4c..f7aeec1e9 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -330,8 +330,8 @@ 개인 토큰으로 로그인 개인 토큰 기본 인증으로 로그인 - If you are actually tied to an organizations and you can\'t see them here please follow link - below.\nhttps://help.github.com/articles/about-third-party-application-restrictions\nPS: 조직에 FastHub 엑세스 권한을 부여하고 액세스 토큰을 사용해서 로그인할 수 있습니다.\n또한 https://github.com/settings/applications에서 FastHub를 찾아 조직 액세스로 스크롤 한 다음 승인 버튼을 클릭하세요. + 실제로 조직에 속해 있고 여기에서 볼 수 없다면 다음 링크를 따르십시오. + \nhttps://help.github.com/articles/about-third-party-application-restrictions\nPS: 조직에 FastHub 엑세스 권한을 부여하고 액세스 토큰을 사용해서 로그인할 수 있습니다.\n또한 https://github.com/settings/applications에서 FastHub를 찾아 조직 액세스로 스크롤 한 다음 승인 버튼을 클릭하세요. 삽입 선택 사진 선택 @@ -401,9 +401,9 @@ Feed 프리미엄 테마 코드 색상 표 - Please do login to your GitHub account in order to access everything from GitHub API - otherwise you\'ll end up being kicked out every time you access anything rather than your Enterprise GitHub due to the token transmitted to - GitHub API is coming from your Enterprise Account. + GitHub API의 모든 항목에 액세스하려면 GitHub 계정에 로그인하십시오. + 그렇지 않으면 GitHub API로 전송 된 토큰은 엔터프라이즈 계정에서 전송되기 때문에 Enterprise GitHub가 아닌 + 다른 항목에 액세스 할 때마다 강제 로그아웃 될 수 있습니다. 계정 추가 계정 선택 일치하지 않음 From 96b8fba39d10f6545b44a378bcd2a0ceac5f824e Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sat, 9 Sep 2017 09:26:08 +0200 Subject: [PATCH 38/49] this commit fixes #920 and fixes #938 --- .../ui/modules/editor/EditorActivity.kt | 6 ++-- .../editor/comment/CommentEditorFragment.kt | 3 ++ .../ui/modules/gists/gist/GistActivity.java | 16 ++++++++-- .../gist/comments/GistCommentsFragment.java | 5 ++++ .../gists/gist/comments/GistCommentsMvp.java | 2 ++ .../commit/details/CommitPagerActivity.java | 30 +++++++++++++------ .../comments/CommitCommentsFragment.java | 5 ++++ .../details/comments/CommitCommentsMvp.java | 2 ++ .../issue/details/IssuePagerActivity.java | 27 ++++++++++++----- .../timeline/IssueTimelineFragment.java | 5 ++++ .../details/timeline/IssueTimelineMvp.java | 2 ++ .../details/PullRequestPagerActivity.java | 26 +++++++++++----- .../timeline/PullRequestTimelineFragment.java | 5 ++++ .../timeline/PullRequestTimelineMvp.java | 2 ++ .../reviews/changes/ReviewChangesActivity.kt | 6 +++- .../ui/widgets/markdown/MarkDownLayout.kt | 1 - 16 files changed, 111 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt index 6122416fa..9b2b0a397 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt @@ -206,12 +206,12 @@ class EditorActivity : BaseActivity(), EditorMv commentId = bundle.getLong(BundleConstant.EXTRA_FOUR) val textToUpdate = bundle.getString(BundleConstant.EXTRA) if (!InputHelper.isEmpty(textToUpdate)) { - editText.setText(String.format("%s ", textToUpdate)) + editText.setText(String.format("%s", textToUpdate)) editText.setSelection(InputHelper.toString(editText).length) } - if (bundle.getString("message", "").isEmpty()) + if (bundle.getString("message", "").isBlank()) { replyQuote.visibility = GONE - else { + } else { MarkDownProvider.setMdText(quote, bundle.getString("message", "")) } participants = bundle.getStringArrayList("participants") diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt index df12f2b96..6d7996ee0 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/comment/CommentEditorFragment.kt @@ -18,6 +18,7 @@ import com.fastaccess.helper.Bundler import com.fastaccess.helper.InputHelper import com.fastaccess.helper.ViewHelper import com.fastaccess.provider.emoji.Emoji +import com.fastaccess.provider.timeline.CommentsHelper import com.fastaccess.ui.base.BaseFragment import com.fastaccess.ui.base.mvp.BaseMvp import com.fastaccess.ui.base.mvp.presenter.BasePresenter @@ -57,6 +58,7 @@ class CommentEditorFragment : BaseFragment? } companion object { diff --git a/app/src/main/java/com/fastaccess/ui/modules/gists/gist/GistActivity.java b/app/src/main/java/com/fastaccess/ui/modules/gists/gist/GistActivity.java index 2a1ddcd87..a0223b5d0 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/gists/gist/GistActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/gists/gist/GistActivity.java @@ -40,6 +40,8 @@ import com.fastaccess.ui.widgets.ViewPagerView; import com.fastaccess.ui.widgets.dialog.MessageDialogView; +import java.util.ArrayList; + import butterknife.BindView; import butterknife.OnClick; @@ -267,8 +269,7 @@ public static Intent createIntent(@NonNull Context context, @NonNull String gist } @Override public void onSendActionClicked(@NonNull String text, Bundle bundle) { - if (pager == null || pager.getAdapter() == null) return; - GistCommentsFragment view = (GistCommentsFragment) pager.getAdapter().instantiateItem(pager, 1); + GistCommentsFragment view = getGistCommentsFragment(); if (view != null) { view.onHandleComment(text, bundle); } @@ -286,6 +287,17 @@ public static Intent createIntent(@NonNull Context context, @NonNull String gist if (commentEditorFragment != null && commentEditorFragment.commentText != null) commentEditorFragment.commentText.setText(null); } + @NonNull @Override public ArrayList getNamesToTag() { + GistCommentsFragment view = getGistCommentsFragment(); + if (view != null) return view.getNamesToTag(); + return new ArrayList<>(); + } + + @Nullable private GistCommentsFragment getGistCommentsFragment() { + if (pager == null || pager.getAdapter() == null) return null; + return (GistCommentsFragment) pager.getAdapter().instantiateItem(pager, 1); + } + private void hideShowFab() { if (pager.getCurrentItem() == 1) { getSupportFragmentManager().beginTransaction().show(commentEditorFragment).commit(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsFragment.java b/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsFragment.java index bbdcef307..a520df8b8 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsFragment.java @@ -29,6 +29,7 @@ import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView; import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller; +import java.util.ArrayList; import java.util.List; import butterknife.BindView; @@ -202,6 +203,10 @@ public static GistCommentsFragment newInstance(@NonNull String gistId) { if (commentsCallback != null) commentsCallback.onClearEditText(); } + @NonNull @Override public ArrayList getNamesToTag() { + return CommentsHelper.getUsers(adapter.getData()); + } + @Override public void onDestroyView() { recycler.removeOnScrollListener(getLoadMore()); super.onDestroyView(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsMvp.java b/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsMvp.java index c3cee5043..281f23997 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/gists/gist/comments/GistCommentsMvp.java @@ -40,6 +40,8 @@ interface View extends BaseMvp.FAView, SwipeRefreshLayout.OnRefreshListener, void onHandleComment(@NonNull String text, @Nullable Bundle bundle); void onAddNewComment(@NonNull Comment comment); + + @NonNull ArrayList getNamesToTag(); } interface Presenter extends BaseMvp.FAPresenter, diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java index 1fd3ecf7a..699d4c481 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/CommitPagerActivity.java @@ -40,6 +40,7 @@ import com.fastaccess.ui.widgets.ViewPagerView; import com.fastaccess.ui.widgets.dialog.MessageDialogView; +import java.util.ArrayList; import java.util.Date; import butterknife.BindView; @@ -224,11 +225,9 @@ public static void createIntentForOffline(@NonNull Context context, @NonNull Com } @Override public void onAddComment(@NonNull Comment newComment) { - if (pager != null && pager.getAdapter() != null) { - CommitCommentsFragment fragment = (CommitCommentsFragment) pager.getAdapter().instantiateItem(pager, 1); - if (fragment != null) { - fragment.addComment(newComment); - } + CommitCommentsFragment fragment = getCommitCommentsFragment(); + if (fragment != null) { + fragment.addComment(newComment); } } @@ -246,10 +245,9 @@ public static void createIntentForOffline(@NonNull Context context, @NonNull Com } @Override public void onSendActionClicked(@NonNull String text, Bundle bundle) { - if (pager == null || pager.getAdapter() == null) return; - CommitCommentsFragment view = (CommitCommentsFragment) pager.getAdapter().instantiateItem(pager, 1); - if (view != null) { - view.onHandleComment(text, bundle); + CommitCommentsFragment fragment = getCommitCommentsFragment(); + if (fragment != null) { + fragment.onHandleComment(text, bundle); } } @@ -265,6 +263,14 @@ public static void createIntentForOffline(@NonNull Context context, @NonNull Com if (commentEditorFragment != null && commentEditorFragment.commentText != null) commentEditorFragment.commentText.setText(null); } + @NonNull @Override public ArrayList getNamesToTag() { + CommitCommentsFragment fragment = getCommitCommentsFragment(); + if (fragment != null) { + return fragment.getNamesToTags(); + } + return new ArrayList<>(); + } + private void hideShowFab() { if (pager.getCurrentItem() == 1) { getSupportFragmentManager().beginTransaction().show(commentEditorFragment).commit(); @@ -272,4 +278,10 @@ private void hideShowFab() { getSupportFragmentManager().beginTransaction().hide(commentEditorFragment).commit(); } } + + private CommitCommentsFragment getCommitCommentsFragment() { + if (pager != null & pager.getAdapter() != null) + return (CommitCommentsFragment) pager.getAdapter().instantiateItem(pager, 1); + return null; + } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsFragment.java index 3ffb5e3db..305e831a9 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsFragment.java @@ -32,6 +32,7 @@ import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView; import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -292,4 +293,8 @@ public static CommitCommentsFragment newInstance(@NonNull String login, @NonNull @Override public void onHandleComment(@NonNull String text, @Nullable Bundle bundle) { getPresenter().onHandleComment(text, bundle); } + + @NonNull @Override public ArrayList getNamesToTags() { + return CommentsHelper.getUsersByTimeline(adapter.getData()); + } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsMvp.java index 029d6f8f6..36351979e 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsMvp.java @@ -48,6 +48,8 @@ interface View extends BaseMvp.FAView, SwipeRefreshLayout.OnRefreshListener, void showReload(); void onHandleComment(@NonNull String text, @Nullable Bundle bundle); + + @NonNull List getNamesToTags(); } interface Presenter extends BaseMvp.FAPresenter, diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java index 0e5c7f4b3..4fa91883a 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/IssuePagerActivity.java @@ -46,6 +46,7 @@ import com.fastaccess.ui.widgets.dialog.MessageDialogView; import java.util.ArrayList; +import java.util.List; import butterknife.BindView; import butterknife.OnClick; @@ -256,7 +257,7 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } updateViews(issueModel); if (isUpdate) { - IssueTimelineFragment issueDetailsView = (IssueTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); + IssueTimelineFragment issueDetailsView = getIssueTimelineFragment(); if (issueDetailsView != null && getPresenter().getIssue() != null) { issueDetailsView.onUpdateHeader(); } @@ -301,8 +302,7 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } @Override public void onUpdateTimeline() { - if (pager == null || pager.getAdapter() == null) return; - IssueTimelineFragment issueDetailsView = (IssueTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); + IssueTimelineFragment issueDetailsView = getIssueTimelineFragment(); if (issueDetailsView != null && getPresenter().getIssue() != null) { issueDetailsView.onRefresh(); } @@ -362,14 +362,17 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } @Override public void onSendActionClicked(@NonNull String text, @Nullable Bundle bundle) { - if (pager != null && pager.getAdapter() != null) { - IssueTimelineFragment fragment = (IssueTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); - if (fragment != null) { - fragment.onHandleComment(text, bundle); - } + IssueTimelineFragment fragment = getIssueTimelineFragment(); + if (fragment != null) { + fragment.onHandleComment(text, bundle); } } + private IssueTimelineFragment getIssueTimelineFragment() { + if (pager == null || pager.getAdapter() == null) return null; + return (IssueTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); + } + @Override public void onTagUser(@NonNull String username) { commentEditorFragment.onAddUserName(username); } @@ -380,6 +383,14 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo if (commentEditorFragment != null && commentEditorFragment.commentText != null) commentEditorFragment.commentText.setText(null); } + @NonNull @Override public ArrayList getNamesToTag() { + IssueTimelineFragment fragment = getIssueTimelineFragment(); + if (fragment != null) { + return fragment.getNamesToTag(); + } + return new ArrayList<>(); + } + private void hideShowFab() { if (getPresenter().isLocked() && !getPresenter().isOwner()) { getSupportFragmentManager().beginTransaction().hide(commentEditorFragment).commit(); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java index 5507a98ff..864dcd593 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineFragment.java @@ -36,6 +36,7 @@ import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView; import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -270,6 +271,10 @@ public class IssueTimelineFragment extends BaseFragment getNamesToTag() { + return CommentsHelper.getUsersByTimeline(adapter.getData()); + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == Activity.RESULT_OK) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineMvp.java index ed964702e..30b5901ca 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelineMvp.java @@ -57,6 +57,8 @@ interface View extends BaseMvp.FAView, SwipeRefreshLayout.OnRefreshListener, and void onHandleComment(String text, @Nullable Bundle bundle); void addNewComment(@NonNull TimelineModel timelineModel); + + @NonNull ArrayList getNamesToTag(); } interface Presenter extends BaseMvp.FAPresenter, BaseViewHolder.OnItemClickListener, diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java index 9881fa484..eff5221ee 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/PullRequestPagerActivity.java @@ -290,7 +290,7 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo setTaskName(pullRequest.getRepoId() + " - " + pullRequest.getTitle()); updateViews(pullRequest); if (update) { - PullRequestTimelineFragment issueDetailsView = (PullRequestTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); + PullRequestTimelineFragment issueDetailsView = getPullRequestTimelineFragment(); if (issueDetailsView != null && getPresenter().getPullRequest() != null) { issueDetailsView.onUpdateHeader(); } @@ -371,8 +371,7 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo @Override public void onUpdateTimeline() { supportInvalidateOptionsMenu(); - if (pager == null || pager.getAdapter() == null) return; - PullRequestTimelineFragment pullRequestDetailsView = (PullRequestTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); + PullRequestTimelineFragment pullRequestDetailsView = getPullRequestTimelineFragment(); if (pullRequestDetailsView != null && getPresenter().getPullRequest() != null) { pullRequestDetailsView.onRefresh(); } @@ -425,14 +424,17 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } @Override public void onSendActionClicked(@NonNull String text, Bundle bundle) { - if (pager != null && pager.getAdapter() != null) { - PullRequestTimelineFragment fragment = (PullRequestTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); - if (fragment != null) { - fragment.onHandleComment(text, bundle); - } + PullRequestTimelineFragment fragment = getPullRequestTimelineFragment(); + if (fragment != null) { + fragment.onHandleComment(text, bundle); } } + private PullRequestTimelineFragment getPullRequestTimelineFragment() { + if (pager == null || pager.getAdapter() == null) return null; + return (PullRequestTimelineFragment) pager.getAdapter().instantiateItem(pager, 0); + } + @Override public void onTagUser(@NonNull String username) { commentEditorFragment.onAddUserName(username); } @@ -450,6 +452,14 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo if (commentEditorFragment != null && commentEditorFragment.commentText != null) commentEditorFragment.commentText.setText(null); } + @Override public ArrayList getNamesToTag() { + PullRequestTimelineFragment fragment = getPullRequestTimelineFragment(); + if (fragment != null) { + return fragment.getNamesToTag(); + } + return new ArrayList<>(); + } + protected void hideAndClearReviews() { getPresenter().getCommitComment().clear(); AnimHelper.mimicFabVisibility(false, prReviewHolder, null); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineFragment.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineFragment.java index 3b159078d..fe9d2135b 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineFragment.java @@ -36,6 +36,7 @@ import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView; import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -284,6 +285,10 @@ public class PullRequestTimelineFragment extends BaseFragment getNamesToTag() { + return CommentsHelper.getUsersByTimeline(adapter.getData()); + } + @Override public void showReactionsPopup(@NonNull ReactionTypes type, @NonNull String login, @NonNull String repoId, long idOrNumber, int reactionType) { ReactionsDialogFragment.newInstance(login, repoId, type, idOrNumber, reactionType).show(getChildFragmentManager(), "ReactionsDialogFragment"); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineMvp.java index 58901ad9c..21e20cb00 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelineMvp.java @@ -75,6 +75,8 @@ void onReplyOrCreateReview(@Nullable User user, @Nullable String message, int gr @NonNull EditReviewCommentModel model); void addComment(@NonNull TimelineModel timelineModel); + + @NonNull ArrayList getNamesToTag(); } interface Presenter extends BaseMvp.FAPresenter, BaseViewHolder.OnItemClickListener, diff --git a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt index 7416fd8c2..1a023ca63 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/reviews/changes/ReviewChangesActivity.kt @@ -124,11 +124,15 @@ class ReviewChangesActivity : BaseDialogFragment? { + return arrayListOf() + } + companion object { fun startForResult(reviewChanges: ReviewRequestModel, repoId: String, owner: String, number: Long, isAuthor: Boolean, isEnterprise: Boolean, isClosed: Boolean): ReviewChangesActivity { diff --git a/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt b/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt index c4f09ca18..b37156529 100644 --- a/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt +++ b/app/src/main/java/com/fastaccess/ui/widgets/markdown/MarkDownLayout.kt @@ -158,7 +158,6 @@ class MarkDownLayout : LinearLayout { if (isLink) { MarkDownProvider.addLink(it.getEditText(), InputHelper.toString(title), InputHelper.toString(link)) } else { - it.getEditText().setText(String.format("%s\n", it.getEditText().text)) MarkDownProvider.addPhoto(it.getEditText(), InputHelper.toString(title), InputHelper.toString(link)) } } From b617b80a8c712b4014d4ef7d914a9ba9b8eceeb0 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sat, 9 Sep 2017 12:36:50 +0200 Subject: [PATCH 39/49] released 4.2.0 --- .github/check_translations.cs | 60 ------------------ README.md | 6 +- app/build.gradle | 1 + app/proguard-rules.pro | 3 +- .../handler/drawable/DrawableGetter.java | 12 +++- .../handler/drawable/UrlDrawable.java | 32 +++++++++- .../ui/modules/repos/RepoPagerActivity.java | 3 +- app/src/main/res/raw/changelog.html | 48 +++++++------- appveyor.yml | 63 ------------------- build.gradle | 2 +- 10 files changed, 73 insertions(+), 157 deletions(-) delete mode 100644 .github/check_translations.cs delete mode 100644 appveyor.yml diff --git a/.github/check_translations.cs b/.github/check_translations.cs deleted file mode 100644 index 6a0130ef9..000000000 --- a/.github/check_translations.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Xml; - -void Main(string[] args) -{ - string RootDir = System.Reflection.Assembly.GetExecutingAssembly().Location; - string ResDir = Path.Combine(RootDir, @"app\src\main\res"); - - int TotalFiles = 0, - TotalIssues = 0; - - foreach (string dir in Directory.GetDirectories(ResDir, "values-*").Where(d => File.Exists(Path.Combine(d, @"strings.xml")))) { - Console.WriteLine(@"Checking ""{0}""...", dir); - - XmlDocument xmlFile = new XmlDocument(); - xmlFile.Load(Path.Combine(dir, @"strings.xml")); - XmlElement xRoot = xmlFile.DocumentElement; - - bool wasAdded = false; - - foreach (XmlNode xmlNode in xRoot) { - if (xmlNode.Attributes == null) { - continue; - } - if (xmlNode.Attributes.Count > 0) { - foreach (XmlNode attr in xmlNode.Attributes) { - if (attr == null) { - continue; - } - if (attr.Name == "translatable") { - TotalIssues++; - PowerShell ps = PowerShell.Create(); - ps.AddCommand("Add-AppveyorMessage"); - ps.AddArgument(String.Format(@"Found **{0}=""{1}""** {4} in **""{2}""**. {4}File: **""{3}""**", attr.Name, attr.Value, xmlNode.OuterXml, dir, Environment.NewLine)); - ps.Invoke(); - Console.WriteLine(@" {0}=""{1}"" in {2}", attr.Name, attr.Value, xmlNode.OuterXml); - if (wasAdded) { - continue; - } - TotalFiles++; - wasAdded = true; - } - } - } - } - } - - Console.WriteLine("Found {0} issue(s) in {1} file(s).", TotalIssues, TotalFiles); - if (TotalIssues != 0) { - PowerShell ps = PowerShell.Create(); - ps.AddCommand("Add-AppveyorMessage"); - ps.AddArgument(@"Please, remove the string(s) and commit again."); - ps.Invoke(); - Environment.Exit(101); - } -} \ No newline at end of file diff --git a/README.md b/README.md index a9dd95120..c93c55698 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/k0shk0sh/FastHub.svg?branch=master)](https://travis-ci.org/k0shk0sh/FastHub) [![Build status](https://ci.appveyor.com/api/projects/status/2yhxx7hu6hju24bk?svg=true)](https://ci.appveyor.com/project/k0shk0sh/fasthub) +[![Build Status](https://travis-ci.org/k0shk0sh/FastHub.svg?branch=master)](https://travis-ci.org/k0shk0sh/FastHub) [![Releases](https://img.shields.io/github/release/k0shk0sh/FastHub.svg)](https://github.com/k0shk0sh/FastHub/releases/latest) [![Slack](https://img.shields.io/badge/slack-join-e01563.svg)](http://rebrand.ly/fasthub) ![Logo](/.github/assets/feature_graphic.png?raw=true "Logo") @@ -18,10 +18,6 @@ Yet another **open-source** GitHub client app but unlike any other app, FastHub alt="Direct apk download" height="80">](https://github.com/k0shk0sh/FastHub/releases/latest) -#### Snapshots / Test builds - -We have configured snapshots of FastHub, which can be downloaded from [AppVeyor CI](https://ci.appveyor.com/project/k0shk0sh/fasthub/build/artifacts). - # Features - **App** - Three login types (Basic Auth), (Access Token) or via (OAuth) diff --git a/app/build.gradle b/app/build.gradle index 2dbbecd87..60dc4579b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,7 @@ android { dexOptions { jumboMode true + javaMaxHeapSize "4g" } testOptions { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 661073967..af293c907 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -130,4 +130,5 @@ -dontwarn sun.misc.Unsafe -dontwarn com.octo.android.robospice.retrofit.RetrofitJackson** -dontwarn retrofit.appengine.UrlFetchClient --dontwarn icepick.** \ No newline at end of file +-dontwarn icepick.** +-dontwarn com.fastaccess.ui.modules.repos.** \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java index 8b8078a58..759fe787f 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/DrawableGetter.java @@ -18,7 +18,7 @@ * Created by Kosh on 22 Apr 2017, 7:44 PM */ -public class DrawableGetter implements Html.ImageGetter { +public class DrawableGetter implements Html.ImageGetter, Drawable.Callback { private WeakReference container; private final Set cachedTargets; @@ -42,6 +42,16 @@ public DrawableGetter(TextView tv) { return urlDrawable; } + @Override public void invalidateDrawable(@NonNull Drawable drawable) { + if (container != null && container.get() != null) { + container.get().invalidate(); + } + } + + @Override public void scheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable, long l) {} + + @Override public void unscheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable) {} + public void clear(@NonNull DrawableGetter drawableGetter) { if (drawableGetter.cachedTargets != null) { for (GlideDrawableTarget target : drawableGetter.cachedTargets) { diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java index b0bfac97e..3478859ec 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/drawable/UrlDrawable.java @@ -3,8 +3,11 @@ import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; -class UrlDrawable extends BitmapDrawable { +import com.bumptech.glide.load.resource.gif.GifDrawable; + +class UrlDrawable extends BitmapDrawable implements Drawable.Callback { private Drawable drawable; @SuppressWarnings("deprecation") UrlDrawable() {} @@ -12,6 +15,11 @@ class UrlDrawable extends BitmapDrawable { @Override public void draw(Canvas canvas) { if (drawable != null) { drawable.draw(canvas); + if (drawable instanceof GifDrawable) { + if (!((GifDrawable) drawable).isRunning()) { + ((GifDrawable) drawable).start(); + } + } } } @@ -20,6 +28,28 @@ public Drawable getDrawable() { } public void setDrawable(Drawable drawable) { + if (this.drawable != null) { + this.drawable.setCallback(null); + } + drawable.setCallback(this); this.drawable = drawable; } + + @Override public void invalidateDrawable(@NonNull Drawable who) { + if (getCallback() != null) { + getCallback().invalidateDrawable(who); + } + } + + @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + if (getCallback() != null) { + getCallback().scheduleDrawable(who, what, when); + } + } + + @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + if (getCallback() != null) { + getCallback().unscheduleDrawable(who, what); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java index 2d2a30acc..912acb68a 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java @@ -39,6 +39,7 @@ import com.fastaccess.helper.ViewHelper; import com.fastaccess.provider.colors.ColorsProvider; import com.fastaccess.provider.scheme.LinkParserHelper; +import com.fastaccess.provider.scheme.SchemeParser; import com.fastaccess.provider.tasks.git.GithubActionService; import com.fastaccess.ui.adapter.TopicsAdapter; import com.fastaccess.ui.base.BaseActivity; @@ -529,7 +530,7 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } else if (item.getItemId() == R.id.originalRepo) { if (getPresenter().getRepo() != null && getPresenter().getRepo().getParent() != null) { Repo parent = getPresenter().getRepo().getParent(); - RepoPagerActivity.startRepoPager(this, new NameParser(parent.getHtmlUrl())); + SchemeParser.launchUri(this, parent.getHtmlUrl()); } return true; } else if (item.getItemId() == R.id.deleteRepo) { diff --git a/app/src/main/res/raw/changelog.html b/app/src/main/res/raw/changelog.html index 0f87b2a72..f0bc53064 100644 --- a/app/src/main/res/raw/changelog.html +++ b/app/src/main/res/raw/changelog.html @@ -13,13 +13,15 @@

Version 4.

Reporting Issues or Feature Requests in Google Play review section, will be ignored or might even get your account to be blocked from FastHub. You are using an app for GitHub, which provides a proper way to report issues. - Please report the issues in FastHub repo instead, by opening the Drawer Menu, click about and click "Report an Issue" PLEASE USE IT. +
+ Please report the issues in FastHub repo instead, by opening the Drawer Menu, click about and click “Report an Issue”PLEASE + USE IT.

-

Bugs , Enhancements & new Features (3.2.0) +

Bugs , Enhancements & new Features (3.2.0)

    -
  • (New) Make commits on repos from FastHub (this only applies for repo owners so far) +
  • (New) Make commits on repos from FastHub (this only applies for repo owners so far). PRO
  • (New) Long pressing files/directories will open up their Commit/Git History.
  • @@ -33,37 +35,35 @@

    Bugs , Enhancements &
  • (Enhancement) Hide fab while scrolling in Issues & PR tab.
  • (Fix) PR Status where it got somehow broken in previous release.
  • (Fix) A text under PR status will indicates if the PR is mergable or not.
  • +
  • (Fix) Tagging users in full screen editor.
  • (Fix) Comments text selection where sometime they aren’t selectable.
  • -
  • (Fix) PR Review footer is invisible in (4.1.0)
  • +
  • (Fix) PR Review footer is invisible in (4.1.0).
  • (Fix) Cursor position after inserting emoji, links & images.
  • -
  • (Fix) Teal customized accent was displayed as green
  • +
  • (Fix) Teal customized accent was displayed as green.
  • (Fix) Some crashes from the crash report.
  • -
  • (Fix) Lots of bug fixes
  • -
  • There are more stuff are not mentioned, find them out :p
  • +
  • (Fix) Lots of bug fixes.
  • +
  • There are more stuff are not mentioned, find them out :stuck_out_tongue:
-

What left in FastHub? -

-

So far, FastHub has almost all the features from GitHub except for - Project Cards - next release will hopefully includes that & - then I’ll be only releasing when major bugs is fixed or major feature implemented. -

-

How old is FastHub now? +

What left in FastHub?

-

FastHub is now 5 months & 4 days old, since v1.0.0 it has really grow and became more and more better on each release and this is - all - happened because of the community either via reporting bugs, feature requests or even give hand to fix things or implement things in - the app, - I’m really grateful for such community, thank you guys a lot. +

+ So far, FastHub has implemented almost all the features of GitHub, besides forProject Cards. Hopefully in the + next release FastHub will include that. The following releases will mainly be bug fixes or if a major feature is implemented. +

+

How old is FastHub now? +

-

Thanks to everyone who contributed either via reporting bugs or via code contribution

+

+ FastHub is now 5 months & a week old. Since v1.0.0 it has really grow with each and every release. The only way this was + possible, was due to the community helping out either via reporting bugs, feature requests or even give hand to fix things or + implement things in the app. I’m really grateful for having such a great community. Thank you guys! + +

-

- Thank you very much -

+

Thank you to all who have contributed either via reporting bugs or via code contribution.

\ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 238f4a76f..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,63 +0,0 @@ -image: Visual Studio 2017 -clone_folder: 'C:\FastHub' - -# skip branch build if there is an active pull request -skip_branch_with_pr: true -skip_commits: - files: - - '**/*.md' - message: \[(skip app veyor|app veyor skip|skip appveyor|appveyor skip)\] - -environment: - ANDROID_HOME: 'A:\' - GRADLE_USER_HOME: 'G:\' - -init: - - ps: | - subst F: C:\FastHub - mkdir C:\gradle.home - subst G: C:\gradle.home - mkdir C:\Android\android-sdk - subst A: C:\Android\android-sdk - appveyor DownloadFile "https://dl.google.com/android/repository/sdk-tools-windows-3859397.zip" -FileName "C:\android-tools.zip" - Write-Host "Extracting SDK tools..." - 7z x "C:\android-tools.zip" -o"$env:ANDROID_HOME" | Out-Null - -install: - - ps: | - Write-Output "" > ~\.android\repositories.cfg - if ($env:APPVEYOR_PULL_REQUEST_NUMBER){ - if (($env:APPVEYOR_REPO_COMMIT_AUTHOR -ne 'Yakov') -and ($env:APPVEYOR_REPO_COMMIT_AUTHOR -ne 'Kosh Sergani')) { - Write-Host "PR detected. Installing C# Script Engine and doing translations check:" - cinst cs-script --version 3.26.2.0 - cscs - cscs -ac:2 -nl $env:APPVEYOR_BUILD_FOLDER\.github\check_translations.cs - } - } - Write-Host "Installing Android packages:" - $pkgs = '"platform-tools"', '"extras;android;m2repository"', '"extras;google;m2repository"', '"build-tools;26.0.1"', '"platforms;android-26"' - foreach ($pkg in $pkgs) { - Write-Host "Installing ${pkg}:" - echo "y" | & $env:ANDROID_HOME\tools\bin\sdkmanager.bat ${pkg} - } -build_script: - - cmd: | - CD /D F: - F:\gradlew clean assembleDebug --stacktrace - -after_build: - - ps: Rename-Item -Path "$env:APPVEYOR_BUILD_FOLDER\app\build\outputs\apk\debug\app-debug.apk" -NewName "fasthub-debug-$env:APPVEYOR_BUILD_VERSION.apk" - -test: off - -artifacts: -- path: \app\build\outputs\apk\debug\fasthub-debug-%APPVEYOR_BUILD_VERSION%.apk - -deploy: off - -notifications: -- provider: GitHubPullRequest - template: ':x: [Build {{&projectName}} {{buildVersion}} {{status}}]({{buildUrl}}) (commit {{commitUrl}} by @{{&commitAuthorUsername}})

**Message(s):**
{{#jobs}}{{#messages}}
{{message}}
{{/messages}}{{/jobs}}' - on_build_success: false - on_build_failure: true - on_build_status_changed: false \ No newline at end of file diff --git a/build.gradle b/build.gradle index e03a897db..03b6eeacc 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-beta2' + classpath 'com.android.tools.build:gradle:3.0.0-beta5' classpath 'com.google.gms:google-services:3.0.0' classpath 'com.novoda:gradle-build-properties-plugin:0.3' classpath 'com.dicedmelon.gradle:jacoco-android:0.1.2' From e406010e39d362b51864f93acb061518801a63de Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sat, 9 Sep 2017 20:04:44 +0200 Subject: [PATCH 40/49] This commit fixes #945 --- .../java/com/fastaccess/provider/colors/ColorsProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/fastaccess/provider/colors/ColorsProvider.java b/app/src/main/java/com/fastaccess/provider/colors/ColorsProvider.java index a6a418ba6..bccfb2aef 100644 --- a/app/src/main/java/com/fastaccess/provider/colors/ColorsProvider.java +++ b/app/src/main/java/com/fastaccess/provider/colors/ColorsProvider.java @@ -68,7 +68,7 @@ public static void load() { .filter(value -> value != null && !InputHelper.isEmpty(value.getKey())) .map(Map.Entry::getKey) .collect(Collectors.toCollection(ArrayList::new))); - lang.add(0, "All Language"); + lang.add(0, "All Languages"); lang.addAll(1, POPULAR_LANG); return lang; } From bfc2fc4952bb74caf54df5bef3eb14512393108e Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sat, 9 Sep 2017 20:49:55 +0200 Subject: [PATCH 41/49] This commit impl project cards list for repos. WIP for cards --- .../data/dao/FragmentPagerAdapterModel.java | 12 ++ .../fastaccess/data/dao/ProjectsModel.java | 165 ++++++++++++++++++ .../data/service/ProjectsService.kt | 27 +++ .../provider/rest/RestProvider.java | 5 + .../timeline/handler/TableHandler.java | 13 +- .../fastaccess/ui/adapter/ProjectsAdapter.kt | 20 +++ .../adapter/viewholder/ProjectViewHolder.kt | 42 +++++ .../ui/modules/repos/RepoPagerMvp.java | 4 +- .../ui/modules/repos/RepoPagerPresenter.java | 10 ++ .../projects/RepoProjectsFragmentPager.kt | 46 +++++ .../projects/list/RepoProjectFragment.kt | 127 ++++++++++++++ .../repos/projects/list/RepoProjectMvp.kt | 28 +++ .../projects/list/RepoProjectPresenter.kt | 72 ++++++++ app/src/main/res/drawable/ic_project.xml | 9 + .../res/menu-land/repo_bottom_nav_menu.xml | 8 + .../res/menu-sw600dp/repo_bottom_nav_menu.xml | 8 + .../main/res/menu/repo_bottom_nav_menu.xml | 7 + app/src/main/res/values/strings.xml | 2 + .../jobdispatcher/GooglePlayDriver.java | 21 ++- 19 files changed, 611 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java create mode 100644 app/src/main/java/com/fastaccess/data/service/ProjectsService.kt create mode 100644 app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt create mode 100644 app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt create mode 100644 app/src/main/res/drawable/ic_project.xml diff --git a/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java b/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java index 09eb11ff1..36e3ca3e2 100644 --- a/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/FragmentPagerAdapterModel.java @@ -44,6 +44,7 @@ import com.fastaccess.ui.modules.repos.issues.issue.RepoClosedIssuesFragment; import com.fastaccess.ui.modules.repos.issues.issue.RepoOpenedIssuesFragment; import com.fastaccess.ui.modules.repos.issues.issue.details.timeline.IssueTimelineFragment; +import com.fastaccess.ui.modules.repos.projects.list.RepoProjectFragment; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.RepoPullRequestFragment; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.details.commits.PullRequestCommitsFragment; import com.fastaccess.ui.modules.repos.pull_requests.pull_request.details.files.PullRequestFilesFragment; @@ -230,4 +231,15 @@ private FragmentPagerAdapterModel(String title, Fragment fragment) { BranchesFragment.Companion.newInstance(login, repoId, false))) .toList(); } + + + @NonNull public static List buildForRepoProjects(@NonNull Context context, @NonNull String repoId, + @NonNull String login) { + return Stream.of(new FragmentPagerAdapterModel(context.getString(R.string.open), + RepoProjectFragment.Companion.newInstance(login, repoId, IssueState.open)), + new FragmentPagerAdapterModel(context.getString(R.string.closed), + RepoProjectFragment.Companion.newInstance(login, repoId, IssueState.closed))) + .toList(); + } + } diff --git a/app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java b/app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java new file mode 100644 index 000000000..4072614f9 --- /dev/null +++ b/app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java @@ -0,0 +1,165 @@ +package com.fastaccess.data.dao; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.fastaccess.data.dao.model.User; + +import java.util.Date; + +/** + * Created by kosh on 09/09/2017. + */ + +public class ProjectsModel implements Parcelable { + private String ownerUrl; + private String url; + private String htmlUrl; + private String columnsUrl; + private int id; + private String name; + private String body; + private int number; + private String state; + private User creator; + private Date createdAt; + private Date updatedAt; + + public String getOwnerUrl() { + return ownerUrl; + } + + public void setOwnerUrl(String ownerUrl) { + this.ownerUrl = ownerUrl; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public void setHtmlUrl(String htmlUrl) { + this.htmlUrl = htmlUrl; + } + + public String getColumnsUrl() { + return columnsUrl; + } + + public void setColumnsUrl(String columnsUrl) { + this.columnsUrl = columnsUrl; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public User getCreator() { + return creator; + } + + public void setCreator(User creator) { + this.creator = creator; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + @Override public int describeContents() { return 0; } + + @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(this.ownerUrl); + dest.writeString(this.url); + dest.writeString(this.htmlUrl); + dest.writeString(this.columnsUrl); + dest.writeInt(this.id); + dest.writeString(this.name); + dest.writeString(this.body); + dest.writeInt(this.number); + dest.writeString(this.state); + dest.writeParcelable(this.creator, flags); + dest.writeLong(this.createdAt != null ? this.createdAt.getTime() : -1); + dest.writeLong(this.updatedAt != null ? this.updatedAt.getTime() : -1); + } + + public ProjectsModel() {} + + protected ProjectsModel(Parcel in) { + this.ownerUrl = in.readString(); + this.url = in.readString(); + this.htmlUrl = in.readString(); + this.columnsUrl = in.readString(); + this.id = in.readInt(); + this.name = in.readString(); + this.body = in.readString(); + this.number = in.readInt(); + this.state = in.readString(); + this.creator = in.readParcelable(User.class.getClassLoader()); + long tmpCreatedAt = in.readLong(); + this.createdAt = tmpCreatedAt == -1 ? null : new Date(tmpCreatedAt); + long tmpUpdatedAt = in.readLong(); + this.updatedAt = tmpUpdatedAt == -1 ? null : new Date(tmpUpdatedAt); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public ProjectsModel createFromParcel(Parcel source) {return new ProjectsModel(source);} + + @Override public ProjectsModel[] newArray(int size) {return new ProjectsModel[size];} + }; +} diff --git a/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt b/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt new file mode 100644 index 000000000..1f465d510 --- /dev/null +++ b/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt @@ -0,0 +1,27 @@ +package com.fastaccess.data.service + +import com.fastaccess.data.dao.Pageable +import com.fastaccess.data.dao.ProjectsModel +import io.reactivex.Observable +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.Path +import retrofit2.http.Query + +/** + * Created by kosh on 09/09/2017. + */ + +interface ProjectsService { + + @GET("repos/{owner}/{repo}/projects") + @Headers("Accept: application/vnd.github.inertia-preview+json") + fun getRepoProjects(@Path("owner") owner: String, @Path("repo") repo: String, + @Query("state") state: String?, @Query("page") page: Int): Observable> + + @GET("orgs/{org}/projects") + @Headers("Accept: application/vnd.github.inertia-preview+json") + fun getOrgsProjects(@Path("org") org: String, + @Query("page") page: Int): Observable> + +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java b/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java index 7ce117057..a21f77ad7 100644 --- a/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java +++ b/app/src/main/java/com/fastaccess/provider/rest/RestProvider.java @@ -18,6 +18,7 @@ import com.fastaccess.data.service.IssueService; import com.fastaccess.data.service.NotificationService; import com.fastaccess.data.service.OrganizationService; +import com.fastaccess.data.service.ProjectsService; import com.fastaccess.data.service.PullRequestService; import com.fastaccess.data.service.ReactionsService; import com.fastaccess.data.service.RepoService; @@ -192,6 +193,10 @@ public static int getErrorCode(Throwable throwable) { return provideRetrofit(enterprise).create(ContentService.class); } + @NonNull public static ProjectsService getProjectsService(boolean enterprise) { + return provideRetrofit(enterprise).create(ProjectsService.class); + } + @Nullable public static GitHubErrorResponse getErrorResponse(@NonNull Throwable throwable) { ResponseBody body = null; if (throwable instanceof HttpException) { diff --git a/app/src/main/java/com/fastaccess/provider/timeline/handler/TableHandler.java b/app/src/main/java/com/fastaccess/provider/timeline/handler/TableHandler.java index ff1bab2da..a3d1a5371 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/handler/TableHandler.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/handler/TableHandler.java @@ -144,12 +144,13 @@ private int calculateRowHeight(List row) { int rowHeight = 0; - for (Spanned cell : row) { - StaticLayout layout = new StaticLayout(cell, textPaint, columnWidth - - 2 * PADDING, Alignment.ALIGN_NORMAL, 1f, 0f, true); - - if (layout.getHeight() > rowHeight) { - rowHeight = layout.getHeight(); + if (columnWidth > 0) { + for (Spanned cell : row) { + StaticLayout layout = new StaticLayout(cell, textPaint, columnWidth + - 2 * PADDING, Alignment.ALIGN_NORMAL, 1f, 0f, true); + if (layout.getHeight() > rowHeight) { + rowHeight = layout.getHeight(); + } } } diff --git a/app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt b/app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt new file mode 100644 index 000000000..2adc506c1 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt @@ -0,0 +1,20 @@ +package com.fastaccess.ui.adapter + +import android.view.ViewGroup +import com.fastaccess.data.dao.ProjectsModel +import com.fastaccess.ui.adapter.viewholder.ProjectViewHolder +import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter +import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder + +/** + * Created by kosh on 09/09/2017. + */ +class ProjectsAdapter(data: ArrayList) : + BaseRecyclerAdapter>(data) { + + override fun viewHolder(parent: ViewGroup, viewType: Int): ProjectViewHolder = ProjectViewHolder.newInstance(parent, this) + + override fun onBindView(holder: ProjectViewHolder?, position: Int) { + holder?.bind(data[position]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt new file mode 100644 index 000000000..a954bb20c --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt @@ -0,0 +1,42 @@ +package com.fastaccess.ui.adapter.viewholder + +import android.view.View +import android.view.ViewGroup +import butterknife.BindView +import com.fastaccess.R +import com.fastaccess.data.dao.ProjectsModel +import com.fastaccess.helper.ParseDateFormat +import com.fastaccess.ui.widgets.FontTextView +import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter +import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder + +/** + * Created by kosh on 09/09/2017. + */ +class ProjectViewHolder(view: View, adapter: BaseRecyclerAdapter<*, *, *>) : BaseViewHolder(view, adapter) { + + @BindView(R.id.description) lateinit var description: FontTextView + @BindView(R.id.title) lateinit var title: FontTextView + @BindView(R.id.date) lateinit var date: FontTextView + + override fun bind(t: ProjectsModel) { + title.text = t.name + if (t.body.isNullOrBlank()) { + description.visibility = View.GONE + } else { + description.visibility = View.VISIBLE + description.text = t.body + } + if (t.updatedAt == null) { + date.text = ParseDateFormat.getTimeAgo(t.createdAt) + } else { + date.text = ParseDateFormat.getTimeAgo(t.updatedAt) + } + } + + companion object { + fun newInstance(parent: ViewGroup, adapter: BaseRecyclerAdapter<*, *, *>): ProjectViewHolder { + return ProjectViewHolder(getView(parent, R.layout.feeds_row_no_image_item), adapter) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerMvp.java b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerMvp.java index 25ed188e2..3477aa692 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerMvp.java @@ -24,12 +24,14 @@ public interface RepoPagerMvp { int CODE = 0; int ISSUES = 1; int PULL_REQUEST = 2; - int PROFILE = 3; + int PROJECTS = 3; + int PROFILE = 4; @IntDef({ CODE, ISSUES, PULL_REQUEST, + PROJECTS, PROFILE }) @Retention(RetentionPolicy.SOURCE) @interface RepoNavigationType {} diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerPresenter.java index f19612272..756689fc2 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerPresenter.java @@ -18,6 +18,7 @@ import com.fastaccess.ui.base.mvp.presenter.BasePresenter; import com.fastaccess.ui.modules.repos.code.RepoCodePagerFragment; import com.fastaccess.ui.modules.repos.issues.RepoIssuesPagerFragment; +import com.fastaccess.ui.modules.repos.projects.RepoProjectsFragmentPager; import com.fastaccess.ui.modules.repos.pull_requests.RepoPullRequestPagerFragment; import static com.fastaccess.helper.ActivityHelper.getVisibleFragment; @@ -185,6 +186,8 @@ private void callApi(int navTyp) { AppHelper.getFragmentByTag(fragmentManager, RepoIssuesPagerFragment.TAG); RepoPullRequestPagerFragment pullRequestPagerView = (RepoPullRequestPagerFragment) AppHelper.getFragmentByTag(fragmentManager, RepoPullRequestPagerFragment.TAG); + RepoProjectsFragmentPager projectsFragmentPager = (RepoProjectsFragmentPager) AppHelper.getFragmentByTag(fragmentManager, + RepoProjectsFragmentPager.Companion.getTAG()); if (getRepo() == null) { sendToView(RepoPagerMvp.View::onFinishActivity); return; @@ -219,6 +222,13 @@ private void callApi(int navTyp) { onShowHideFragment(fragmentManager, pullRequestPagerView, currentVisible); } break; + case RepoPagerMvp.PROJECTS: + if (projectsFragmentPager == null) { + onAddAndHide(fragmentManager, RepoProjectsFragmentPager.Companion.newInstance(repoId(), login()), currentVisible); + } else { + onShowHideFragment(fragmentManager, projectsFragmentPager, currentVisible); + } + break; } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt new file mode 100644 index 000000000..d55c5ee5f --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt @@ -0,0 +1,46 @@ +package com.fastaccess.ui.modules.repos.projects + +import android.os.Bundle +import android.support.design.widget.TabLayout +import android.view.View +import butterknife.BindView +import com.fastaccess.R +import com.fastaccess.data.dao.FragmentPagerAdapterModel +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Bundler +import com.fastaccess.ui.adapter.FragmentsPagerAdapter +import com.fastaccess.ui.base.BaseFragment +import com.fastaccess.ui.base.mvp.BaseMvp +import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import com.fastaccess.ui.widgets.ViewPagerView + +/** + * Created by kosh on 09/09/2017. + */ +class RepoProjectsFragmentPager : BaseFragment>() { + + @BindView(R.id.tabs) lateinit var tabs: TabLayout + @BindView(R.id.pager) lateinit var pager: ViewPagerView + + override fun fragmentLayout(): Int = R.layout.centered_tabbed_viewpager + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + pager.adapter = FragmentsPagerAdapter(childFragmentManager, FragmentPagerAdapterModel.buildForRepoProjects(context, + arguments.getString(BundleConstant.EXTRA), arguments.getString(BundleConstant.ID))) + tabs.setupWithViewPager(pager) + } + + override fun providePresenter(): BasePresenter = BasePresenter() + + companion object { + val TAG = RepoProjectsFragmentPager::class.java.simpleName + fun newInstance(login: String, repoId: String): RepoProjectsFragmentPager { + val fragment = RepoProjectsFragmentPager() + fragment.arguments = Bundler.start() + .put(BundleConstant.ID, repoId) + .put(BundleConstant.EXTRA, login) + .end() + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt new file mode 100644 index 000000000..812d97922 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt @@ -0,0 +1,127 @@ +package com.fastaccess.ui.modules.repos.projects.list + +import android.os.Bundle +import android.support.annotation.StringRes +import android.support.v4.widget.SwipeRefreshLayout +import android.view.View +import butterknife.BindView +import com.fastaccess.R +import com.fastaccess.data.dao.ProjectsModel +import com.fastaccess.data.dao.types.IssueState +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Bundler +import com.fastaccess.provider.rest.loadmore.OnLoadMore +import com.fastaccess.ui.adapter.ProjectsAdapter +import com.fastaccess.ui.base.BaseFragment +import com.fastaccess.ui.widgets.StateLayout +import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView +import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller + +/** + * Created by kosh on 09/09/2017. + */ + +class RepoProjectFragment : BaseFragment(), RepoProjectMvp.View { + + @BindView(R.id.recycler) lateinit var recycler: DynamicRecyclerView + @BindView(R.id.refresh) lateinit var refresh: SwipeRefreshLayout + @BindView(R.id.stateLayout) lateinit var stateLayout: StateLayout + @BindView(R.id.fastScroller) lateinit var fastScroller: RecyclerViewFastScroller + private var onLoadMore: OnLoadMore? = null + private val adapter by lazy { ProjectsAdapter(presenter.getProjects()) } + + + override fun providePresenter(): RepoProjectPresenter = RepoProjectPresenter() + + override fun onNotifyAdapter(items: List?, page: Int) { + hideProgress() + if (items == null || items.isEmpty()) { + adapter.clear() + return + } + if (page <= 1) { + adapter.insertItems(items) + } else { + adapter.addItems(items) + } + } + + override fun getLoadMore(): OnLoadMore { + if (onLoadMore == null) { + onLoadMore = OnLoadMore(presenter) + } + onLoadMore!!.parameter = getState() + return onLoadMore!! + } + + override fun fragmentLayout(): Int = R.layout.micro_grid_refresh_list + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + stateLayout.setEmptyText(R.string.no_projects) + stateLayout.setOnReloadListener({ presenter.onCallApi(1, getState()) }) + refresh.setOnRefreshListener({ presenter.onCallApi(1, getState()) }) + recycler.setEmptyView(stateLayout, refresh) + getLoadMore().initialize(presenter.currentPage, presenter + .previousTotal) + adapter.listener = presenter + recycler.adapter = adapter + recycler.addDivider() + recycler.addOnScrollListener(getLoadMore()) + fastScroller.attachRecyclerView(recycler) + if (presenter.getProjects().isEmpty() && !presenter.isApiCalled) { + presenter.onFragmentCreate(arguments) + presenter.onCallApi(1, getState()) + } + } + + override fun showProgress(@StringRes resId: Int) { + refresh.isRefreshing = true + stateLayout.showProgress() + } + + override fun hideProgress() { + refresh.isRefreshing = false + stateLayout.hideProgress() + } + + override fun showErrorMessage(message: String) { + showReload() + super.showErrorMessage(message) + } + + override fun showMessage(titleRes: Int, msgRes: Int) { + showReload() + super.showMessage(titleRes, msgRes) + } + + override fun onScrollTop(index: Int) { + super.onScrollTop(index) + if (recycler != null) { + recycler.scrollToPosition(0) + } + } + + override fun onDestroyView() { + recycler.removeOnScrollListener(getLoadMore()) + super.onDestroyView() + } + + private fun showReload() { + hideProgress() + stateLayout.showReload(adapter.itemCount) + } + + private fun getState(): IssueState = arguments.getSerializable(BundleConstant.EXTRA_TYPE) as IssueState + + companion object { + fun newInstance(login: String, repoId: String, state: IssueState): RepoProjectFragment { + val fragment = RepoProjectFragment() + fragment.arguments = Bundler.start() + .put(BundleConstant.ID, repoId) + .put(BundleConstant.EXTRA, login) + .put(BundleConstant.EXTRA_TYPE, state) + .end() + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt new file mode 100644 index 000000000..91a5062a8 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt @@ -0,0 +1,28 @@ +package com.fastaccess.ui.modules.repos.projects.list + +import android.os.Bundle +import com.fastaccess.data.dao.ProjectsModel +import com.fastaccess.data.dao.types.IssueState +import com.fastaccess.provider.rest.loadmore.OnLoadMore +import com.fastaccess.ui.base.mvp.BaseMvp +import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder +import java.util.* + +/** + * Created by kosh on 09/09/2017. + */ +interface RepoProjectMvp { + + interface View : BaseMvp.FAView { + fun onNotifyAdapter(items: List?, page: Int) + fun getLoadMore(): OnLoadMore + } + + interface Presenter : BaseViewHolder.OnItemClickListener, + BaseMvp.PaginationListener { + + fun onFragmentCreate(bundle: Bundle?) + + fun getProjects(): ArrayList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt new file mode 100644 index 000000000..fec58940c --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt @@ -0,0 +1,72 @@ +package com.fastaccess.ui.modules.repos.projects.list + +import android.os.Bundle +import android.view.View +import com.fastaccess.data.dao.ProjectsModel +import com.fastaccess.data.dao.types.IssueState +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Logger +import com.fastaccess.provider.rest.RestProvider +import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import java.util.* + +/** + * Created by kosh on 09/09/2017. + */ +class RepoProjectPresenter : BasePresenter(), RepoProjectMvp.Presenter { + + private val projects = ArrayList() + private var page: Int = 0 + private var previousTotal: Int = 0 + private var lastPage = Integer.MAX_VALUE + @com.evernote.android.state.State var login: String = "" + @com.evernote.android.state.State var repoId: String = "" + + override fun onItemClick(position: Int, v: View?, item: ProjectsModel?) { + + } + + override fun onItemLongClick(position: Int, v: View?, item: ProjectsModel?) { + + } + + override fun onFragmentCreate(bundle: Bundle?) { + bundle?.let { + repoId = it.getString(BundleConstant.ID) + login = it.getString(BundleConstant.EXTRA) + } + } + + override fun getProjects(): ArrayList = projects + + override fun getCurrentPage(): Int = page + + override fun getPreviousTotal(): Int = previousTotal + + override fun setCurrentPage(page: Int) { + this.page = page + } + + override fun setPreviousTotal(previousTotal: Int) { + this.previousTotal = previousTotal + } + + override fun onCallApi(page: Int, parameter: IssueState?): Boolean { + if (page == 1) { + lastPage = Integer.MAX_VALUE + sendToView { view -> view.getLoadMore().reset() } + } + if (page > lastPage || lastPage == 0) { + sendToView({ it.hideProgress() }) + return false + } + currentPage = page + makeRestCall(RestProvider.getProjectsService(isEnterprise) + .getRepoProjects(login, repoId, parameter?.name, page), { response -> + lastPage = response.last + Logger.e(response.items as List?) + sendToView({ it.onNotifyAdapter(response.items, page) }) + }) + return true + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_project.xml b/app/src/main/res/drawable/ic_project.xml new file mode 100644 index 000000000..75d0dde7b --- /dev/null +++ b/app/src/main/res/drawable/ic_project.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu-land/repo_bottom_nav_menu.xml b/app/src/main/res/menu-land/repo_bottom_nav_menu.xml index 2136f9ede..424d09d52 100644 --- a/app/src/main/res/menu-land/repo_bottom_nav_menu.xml +++ b/app/src/main/res/menu-land/repo_bottom_nav_menu.xml @@ -24,6 +24,14 @@ android:icon="@drawable/ic_pull_requests" android:title="@string/pull_requests"/> + + + + + + + + + + +
\ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2750fc1ae..a7da4708b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -561,4 +561,6 @@ In App Animations Disable in App animations everywhere. This PR can\'t be merged now. + Projects + No Projects diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java index ae76808cd..58314821a 100644 --- a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/GooglePlayDriver.java @@ -20,7 +20,10 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.support.annotation.NonNull; + import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleResult; /** @@ -28,7 +31,8 @@ * services installed. This backend does not do any availability checks and any uses should be * guarded with a call to {@code GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)} * - * @see GoogleApiAvailability + * @see + * GoogleApiAvailability */ public final class GooglePlayDriver implements Driver { static final String BACKEND_PACKAGE = "com.google.android.gms"; @@ -62,12 +66,6 @@ public final class GooglePlayDriver implements Driver { * Turns Jobs into Bundles. */ private final GooglePlayJobWriter mWriter; - /** - * This is hardcoded to true to avoid putting an unnecessary dependency on the Google Play - * services library. - */ - //TODO: this is an unsatisfying solution - private final boolean mAvailable = true; /** * Instantiates a new GooglePlayDriver. @@ -81,9 +79,16 @@ public GooglePlayDriver(Context context) { @Override public boolean isAvailable() { - return mAvailable; + ApplicationInfo applicationInfo = null; + try { + applicationInfo = mContext.getPackageManager().getApplicationInfo(BACKEND_PACKAGE, 0); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return applicationInfo != null && applicationInfo.enabled; } + /** * Schedules the provided Job. */ From 11b22a0914c91ad10be631864f52048f31e71674 Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sun, 10 Sep 2017 11:18:37 +0200 Subject: [PATCH 42/49] this commit fixes #944 fixes #950 and fixes #947 --- .../provider/timeline/HtmlHelper.java | 8 ++- .../ui/modules/editor/EditorActivity.kt | 10 +++ .../issues/issue/RepoIssuesPresenter.java | 72 ++++++++++++++----- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/fastaccess/provider/timeline/HtmlHelper.java b/app/src/main/java/com/fastaccess/provider/timeline/HtmlHelper.java index 7de7c038f..88f97f1ea 100644 --- a/app/src/main/java/com/fastaccess/provider/timeline/HtmlHelper.java +++ b/app/src/main/java/com/fastaccess/provider/timeline/HtmlHelper.java @@ -47,13 +47,15 @@ public class HtmlHelper { public static void htmlIntoTextView(@NonNull TextView textView, @NonNull String html, int width) { registerClickEvent(textView); - if (textView.getMeasuredWidth() > 0) { + if (textView.getWidth() > 100) { textView.setText(initHtml(textView, getActualWidth(textView)).fromHtml(format(html).toString())); } else { textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { textView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - textView.setText(initHtml(textView, getActualWidth(textView)).fromHtml(format(html).toString())); + textView.setText(initHtml(textView, textView.getWidth() > 100 ? getActualWidth(textView) : (width + convertDpToPx(textView + .getContext(), 16))) + .fromHtml(format(html).toString())); } }); } @@ -92,7 +94,7 @@ private static void registerClickEvent(@NonNull TextView textView) { } private static int getActualWidth(TextView textView) { - return textView.getMeasuredWidth() - (convertDpToPx(textView.getContext(), 16)); + return textView.getWidth() - (convertDpToPx(textView.getContext(), 16)); } private static HtmlSpanner initHtml(@NonNull TextView textView, int width) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt index 9b2b0a397..d3cec953c 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/editor/EditorActivity.kt @@ -26,6 +26,7 @@ import com.fastaccess.provider.emoji.Emoji import com.fastaccess.provider.markdown.MarkDownProvider import com.fastaccess.ui.base.BaseActivity import com.fastaccess.ui.widgets.FontTextView +import com.fastaccess.ui.widgets.dialog.MessageDialogView import com.fastaccess.ui.widgets.markdown.MarkDownLayout import com.fastaccess.ui.widgets.markdown.MarkdownEditText import java.util.* @@ -163,6 +164,15 @@ class EditorActivity : BaseActivity(), EditorMv override fun onBackPressed() { if (!InputHelper.isEmpty(editText)) { ViewHelper.hideKeyboard(editText) + MessageDialogView.newInstance(getString(R.string.close), getString(R.string.unsaved_data_warning), + Bundler.start() + .put("primary_extra", getString(R.string.discard)) + .put("secondary_extra", getString(R.string.cancel)) + .put(BundleConstant.EXTRA, true) + .end()) + .show(supportFragmentManager, MessageDialogView.TAG) + return + } super.onBackPressed() } diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoIssuesPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoIssuesPresenter.java index b45dac4e4..1809861d1 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoIssuesPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/RepoIssuesPresenter.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; +import io.reactivex.Observable; + /** * Created by Kosh on 03 Dec 2016, 3:48 PM */ @@ -76,27 +78,32 @@ class RepoIssuesPresenter extends BasePresenter implements R sortBy = "updated"; } setCurrentPage(page); - makeRestCall(RestProvider.getIssueService(isEnterprise()).getRepositoryIssues(login, repoId, parameter.name(), sortBy, page), - issues -> { - lastPage = issues.getLast(); - List filtered = Stream.of(issues.getItems()) - .filter(issue -> issue.getPullRequest() == null) - .toList(); - if (getCurrentPage() == 1) { - manageDisposable(Issue.save(filtered, repoId, login)); - } - sendToView(view -> view.onNotifyAdapter(filtered, page)); - }); + String finalSortBy = sortBy; + makeRestCall(RestProvider.getIssueService(isEnterprise()) + .getRepositoryIssues(login, repoId, parameter.name(), sortBy, page) + .flatMap(issues -> { + lastPage = issues.getLast(); + List filtered = Stream.of(issues.getItems()) + .filter(issue -> issue.getPullRequest() == null) + .toList(); + if (filtered != null) { + if (filtered.size() < 10 && issues.getNext() > 1) { + setCurrentPage(getCurrentPage() + 1); + return grabMoreIssues(filtered, parameter.name(), finalSortBy, getCurrentPage()); + } + return Observable.just(filtered); + } + return Observable.just(new ArrayList()); + }) + .doOnNext(filtered -> { + if (getCurrentPage() == 1) { + Issue.save(filtered, repoId, login); + } + }), + issues -> sendToView(view -> view.onNotifyAdapter(issues, page))); return true; } - private void onCallCountApi(@NonNull IssueState issueState) { - manageDisposable(RxHelper.getObservable(RestProvider.getIssueService(isEnterprise()) - .getIssuesWithCount(RepoQueryProvider.getIssuesPullRequestQuery(login, repoId, issueState, false), 1)) - .subscribe(pullRequestPageable -> sendToView(view -> view.onUpdateCount(pullRequestPageable.getTotalCount())), - Throwable::printStackTrace)); - } - @Override public void onFragmentCreated(@NonNull Bundle bundle, @NonNull IssueState issueState) { repoId = bundle.getString(BundleConstant.ID); login = bundle.getString(BundleConstant.EXTRA); @@ -144,4 +151,33 @@ private void onCallCountApi(@NonNull IssueState issueState) { @Override public void onItemLongClick(int position, View v, Issue item) { if (getView() != null) getView().onShowIssuePopup(item); } + + private void onCallCountApi(@NonNull IssueState issueState) { + manageDisposable(RxHelper.getObservable(RestProvider.getIssueService(isEnterprise()) + .getIssuesWithCount(RepoQueryProvider.getIssuesPullRequestQuery(login, repoId, issueState, false), 1)) + .subscribe(pullRequestPageable -> sendToView(view -> view.onUpdateCount(pullRequestPageable.getTotalCount())), + Throwable::printStackTrace)); + } + + private Observable> grabMoreIssues(@NonNull List issues, @NonNull String state, @NonNull String sortBy, int page) { + return RestProvider.getIssueService(isEnterprise()).getRepositoryIssues(login, repoId, state, sortBy, page) + .flatMap(issuePageable -> { + if (issuePageable != null) { + lastPage = issuePageable.getLast(); + List filtered = Stream.of(issuePageable.getItems()) + .filter(issue -> issue.getPullRequest() == null) + .toList(); + if (filtered != null) { + issues.addAll(filtered); + if (issues.size() < 10 && issuePageable.getNext() > 1) { + setCurrentPage(getCurrentPage() + 1); + return grabMoreIssues(issues, state, sortBy, getCurrentPage()); + } + return Observable.just(filtered); + } + } + return Observable.just(new ArrayList()); + }); + } + } From 5dcf325bd3544fefe35e1e73262609174266a1fd Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Sun, 10 Sep 2017 16:59:16 +0200 Subject: [PATCH 43/49] only show projects in bottomNav if the repo has projects enabled. --- app/src/main/java/com/fastaccess/App.java | 2 +- .../data/dao/model/AbstractRepo.java | 3 ++ .../ui/modules/repos/RepoPagerActivity.java | 5 ++- .../layout/edit_repo_file_layout.xml | 1 + .../res/menu-sw600dp/repo_bottom_nav_menu.xml | 8 ----- .../repo_with_project_bottom_nav_menu.xml} | 0 .../main/res/menu/repo_bottom_nav_menu.xml | 6 ---- .../repo_with_project_bottom_nav_menu.xml | 34 +++++++++++++++++++ build.gradle | 2 +- 9 files changed, 44 insertions(+), 17 deletions(-) rename app/src/main/res/{menu-land/repo_bottom_nav_menu.xml => menu-sw600dp/repo_with_project_bottom_nav_menu.xml} (100%) create mode 100644 app/src/main/res/menu/repo_with_project_bottom_nav_menu.xml diff --git a/app/src/main/java/com/fastaccess/App.java b/app/src/main/java/com/fastaccess/App.java index 6182df764..e7a1d601f 100644 --- a/app/src/main/java/com/fastaccess/App.java +++ b/app/src/main/java/com/fastaccess/App.java @@ -72,7 +72,7 @@ private void setupPreference() { public ReactiveEntityStore getDataStore() { if (dataStore == null) { EntityModel model = Models.DEFAULT; - DatabaseSource source = new DatabaseSource(this, model, "FastHub-DB", 12); + DatabaseSource source = new DatabaseSource(this, model, "FastHub-DB", 13); Configuration configuration = source.getConfiguration(); if (BuildConfig.DEBUG) { source.setTableCreationMode(TableCreationMode.CREATE_NOT_EXISTS); diff --git a/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java b/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java index 2993bfd5f..8a8c63062 100644 --- a/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java +++ b/app/src/main/java/com/fastaccess/data/dao/model/AbstractRepo.java @@ -120,6 +120,7 @@ int networkCount; String starredUser; String reposOwner; + @Nullable boolean hasProjects; public Disposable save(Repo entity) { return Single.create(e -> { @@ -322,6 +323,7 @@ public static Single> getMyRepos(@NonNull String reposOwner) { dest.writeInt(this.networkCount); dest.writeString(this.starredUser); dest.writeString(this.reposOwner); + dest.writeByte(this.hasProjects ? (byte) 1 : (byte) 0); } protected AbstractRepo(Parcel in) { @@ -406,6 +408,7 @@ protected AbstractRepo(Parcel in) { this.networkCount = in.readInt(); this.starredUser = in.readString(); this.reposOwner = in.readString(); + this.hasProjects = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java index 912acb68a..2e20a815b 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/RepoPagerActivity.java @@ -368,8 +368,11 @@ public static Intent createIntent(@NonNull Context context, @NonNull String repo } showWhich = -1; setTaskName(getPresenter().getRepo().getFullName()); - bottomNavigation.setOnMenuItemClickListener(getPresenter()); Repo repoModel = getPresenter().getRepo(); + if (repoModel.isHasProjects()) { + bottomNavigation.inflateMenu(R.menu.repo_with_project_bottom_nav_menu); + } + bottomNavigation.setOnMenuItemClickListener(getPresenter()); if (repoModel.getTopics() != null && !repoModel.getTopics().isEmpty()) { tagsIcon.setVisibility(View.VISIBLE); topicsList.setAdapter(new TopicsAdapter(repoModel.getTopics())); diff --git a/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml index afd0ec03a..7b8e8bcdc 100644 --- a/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/edit_repo_file_layout.xml @@ -92,6 +92,7 @@ android:minLines="5" android:padding="@dimen/spacing_xs_large" android:scrollbars="vertical" + android:textAppearance="@style/TextAppearance.AppCompat.Small" tools:ignore="ScrollViewSize"/> diff --git a/app/src/main/res/menu-sw600dp/repo_bottom_nav_menu.xml b/app/src/main/res/menu-sw600dp/repo_bottom_nav_menu.xml index 424d09d52..2136f9ede 100644 --- a/app/src/main/res/menu-sw600dp/repo_bottom_nav_menu.xml +++ b/app/src/main/res/menu-sw600dp/repo_bottom_nav_menu.xml @@ -24,14 +24,6 @@ android:icon="@drawable/ic_pull_requests" android:title="@string/pull_requests"/> - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/repo_with_project_bottom_nav_menu.xml b/app/src/main/res/menu/repo_with_project_bottom_nav_menu.xml new file mode 100644 index 000000000..7f9e45e42 --- /dev/null +++ b/app/src/main/res/menu/repo_with_project_bottom_nav_menu.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 03b6eeacc..b3832d3da 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { assertjVersion = '2.5.0' espresseVersion = '2.2.2' requery = '1.3.2' - kotlin_version = '1.1.4-2' + kotlin_version = '1.1.4-3' commonmark = '0.9.0' } repositories { From 38e88e6ac373dc687acfd3156f49582a2160636e Mon Sep 17 00:00:00 2001 From: Yakov Date: Mon, 11 Sep 2017 10:10:21 -0400 Subject: [PATCH 44/49] Fixed #956 and a small issue template correction --- app/src/main/java/com/fastaccess/helper/AppHelper.java | 2 +- app/src/main/res/values-de/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/fastaccess/helper/AppHelper.java b/app/src/main/java/com/fastaccess/helper/AppHelper.java index 4137ecd4a..93b388a89 100644 --- a/app/src/main/java/com/fastaccess/helper/AppHelper.java +++ b/app/src/main/java/com/fastaccess/helper/AppHelper.java @@ -86,7 +86,7 @@ public static String getFastHubIssueTemplate(boolean enterprise) { builder.append("- **Model:** ").append(model).append(" \n") .append("---").append("\n\n"); if (!Locale.getDefault().getLanguage().equals(new Locale("en").getLanguage())) { - builder.append("<--") + builder.append("") .append("\n"); diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d24120f8e..6dacd3b78 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -191,7 +191,7 @@ Wähle wie regelmäßig FastHub nach neuen Benachrichtigungen sucht. Benachrichtigunssynchronisierungsintervall Alle - Verhalten :\& Aussehen + Verhalten & Aussehen Listenanimationen aktivieren Listenanimationen Dialog deaktivieren, der verhindert, dass du FastHub ausversehen verlässt From e236ef24bec50aad1908761d21eef3326553a8bc Mon Sep 17 00:00:00 2001 From: Kosh Sergani Date: Mon, 11 Sep 2017 18:36:57 +0200 Subject: [PATCH 45/49] another firebase crash catching. --- .../java/com/fastaccess/helper/AppHelper.java | 19 +++++++++++++----- .../provider/markdown/MarkDownProvider.java | 20 ++++++++++--------- .../firebase/jobdispatcher/JobService.java | 14 +++++++------ 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/fastaccess/helper/AppHelper.java b/app/src/main/java/com/fastaccess/helper/AppHelper.java index 4137ecd4a..52496c84c 100644 --- a/app/src/main/java/com/fastaccess/helper/AppHelper.java +++ b/app/src/main/java/com/fastaccess/helper/AppHelper.java @@ -34,7 +34,9 @@ public class AppHelper { public static void hideKeyboard(@NonNull View view) { InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + if (inputManager != null) { + inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } } @Nullable public static Fragment getFragmentByTag(@NonNull FragmentManager fragmentManager, @NonNull String tag) { @@ -47,18 +49,25 @@ public static void cancelNotification(@NonNull Context context) { public static void cancelNotification(@NonNull Context context, int id) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(id); + if (notificationManager != null) { + notificationManager.cancel(id); + } } public static void cancelAllNotifications(@NonNull Context context) { - ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll(); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.cancelAll(); + } } public static void copyToClipboard(@NonNull Context context, @NonNull String uri) { ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText(context.getString(R.string.app_name), uri); - clipboard.setPrimaryClip(clip); - Toasty.success(App.getInstance(), context.getString(R.string.success_copied)).show(); + if (clipboard != null) { + clipboard.setPrimaryClip(clip); + Toasty.success(App.getInstance(), context.getString(R.string.success_copied)).show(); + } } public static boolean isNightMode(@NonNull Resources resources) { diff --git a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java index 907c5973e..6394fe671 100644 --- a/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java +++ b/app/src/main/java/com/fastaccess/provider/markdown/MarkDownProvider.java @@ -10,7 +10,6 @@ import com.annimon.stream.IntStream; import com.fastaccess.helper.InputHelper; -import com.fastaccess.helper.Logger; import com.fastaccess.provider.markdown.extension.emoji.EmojiExtension; import com.fastaccess.provider.markdown.extension.mention.MentionExtension; import com.fastaccess.provider.timeline.HtmlHelper; @@ -74,14 +73,17 @@ protected static void render(@NonNull TextView textView, String markdown, int wi Parser parser = Parser.builder() .extensions(extensions) .build(); - Node node = parser.parse(markdown); - String rendered = HtmlRenderer - .builder() - .extensions(extensions) - .build() - .render(node); - Logger.e(rendered); - HtmlHelper.htmlIntoTextView(textView, rendered, (width - (textView.getPaddingStart() + textView.getPaddingEnd()))); + try { + Node node = parser.parse(markdown); + String rendered = HtmlRenderer + .builder() + .extensions(extensions) + .build() + .render(node); + HtmlHelper.htmlIntoTextView(textView, rendered, (width - (textView.getPaddingStart() + textView.getPaddingEnd()))); + } catch (Exception ignored) { + HtmlHelper.htmlIntoTextView(textView, markdown, (width - (textView.getPaddingStart() + textView.getPaddingEnd()))); + } } public static void stripMdText(@NonNull TextView textView, String markdown) { diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java index 0c85d5994..41076e767 100644 --- a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java +++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobService.java @@ -114,7 +114,7 @@ void start(JobParameters job, Message msg) { synchronized (runningJobs) { if (runningJobs.containsKey(job.getTag())) { Log.w(TAG, String - .format(Locale.US, "Job with tag = %s was already running.", job.getTag())); + .format(Locale.US, "Job with tag = %s was already running.", job.getTag())); return; } runningJobs.put(job.getTag(), new JobCallback(msg)); @@ -188,7 +188,7 @@ public final boolean onUnbind(Intent intent) { for (int i = runningJobs.size() - 1; i >= 0; i--) { JobCallback callback = runningJobs.get(runningJobs.keyAt(i)); if (callback != null && callback.message != null) { - if(callback.message.obj instanceof JobParameters) { + if (callback.message.obj instanceof JobParameters) { callback.sendResult(onStopJob((JobParameters) callback.message.obj) // returned true, would like to be rescheduled ? RESULT_FAIL_RETRY @@ -242,10 +242,12 @@ private JobCallback(Message message) { } void sendResult(@JobResult int result) { - if (message != null) { - message.arg1 = result; - message.sendToTarget(); - } + try { + if (message != null) { + message.arg1 = result; + message.sendToTarget(); + } + } catch (Exception ignored) {}//catch this freaking crash!!!! } } From 0c489a56edd3f0b82876242bc83ecd1f4684e823 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Mon, 11 Sep 2017 20:40:19 +0200 Subject: [PATCH 46/49] display project columns WIP --- app/src/main/AndroidManifest.xml | 9 ++ .../main/assets/lottie/bounching_ball.json | 1 + .../data/dao/FragmentPagerAdapterModel.java | 7 +- .../fastaccess/data/dao/ProjectCardModel.java | 121 +++++++++++++++++ .../data/dao/ProjectColumnModel.java | 109 +++++++++++++++ .../fastaccess/data/dao/ProjectsModel.java | 12 +- .../data/service/ProjectsService.kt | 9 ++ .../projects/list/RepoProjectPresenter.kt | 9 +- .../list/columns/PorjectColumnsFragment.kt | 19 +++ .../list/details/ProjectPagerActivity.kt | 126 ++++++++++++++++++ .../projects/list/details/ProjectPagerMvp.kt | 23 ++++ .../list/details/ProjectPagerPresenter.kt | 55 ++++++++ .../layout/project_columns_layout.xml | 105 +++++++++++++++ .../layout/projects_activity_layout.xml | 40 ++++++ build.gradle | 4 +- 15 files changed, 635 insertions(+), 14 deletions(-) create mode 100644 app/src/main/assets/lottie/bounching_ball.json create mode 100644 app/src/main/java/com/fastaccess/data/dao/ProjectCardModel.java create mode 100644 app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerActivity.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerMvp.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerPresenter.kt create mode 100644 app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml create mode 100644 app/src/main/res/layouts/main_layouts/layout/projects_activity_layout.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8cd02dd1d..20bf78103 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -249,6 +249,15 @@ android:value=".ui.modules.repos.RepoPagerActivity"/>
+ + + + buildForRepoProjects(@NonNull Context context, @NonNull String repoId, @NonNull String login) { return Stream.of(new FragmentPagerAdapterModel(context.getString(R.string.open), @@ -242,4 +242,9 @@ private FragmentPagerAdapterModel(String title, Fragment fragment) { .toList(); } + @NonNull public static List buildForProjectColumns(@NonNull List models) { + return Stream.of(models) + .map(projectColumnModel -> new FragmentPagerAdapterModel("", new PorjectColumnsFragment())) + .toList(); + } } diff --git a/app/src/main/java/com/fastaccess/data/dao/ProjectCardModel.java b/app/src/main/java/com/fastaccess/data/dao/ProjectCardModel.java new file mode 100644 index 000000000..4f0fba75f --- /dev/null +++ b/app/src/main/java/com/fastaccess/data/dao/ProjectCardModel.java @@ -0,0 +1,121 @@ +package com.fastaccess.data.dao; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.fastaccess.data.dao.model.User; + +import java.util.Date; + +/** + * Created by Hashemsergani on 11.09.17. + */ + +public class ProjectCardModel implements Parcelable { + private String url; + private String columnUrl; + private String contentUrl; + private int id; + private String note; + private User creator; + private Date createdAt; + private Date updatedAt; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getColumnUrl() { + return columnUrl; + } + + public void setColumnUrl(String columnUrl) { + this.columnUrl = columnUrl; + } + + public String getContentUrl() { + return contentUrl; + } + + public void setContentUrl(String contentUrl) { + this.contentUrl = contentUrl; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getNote() { + return note; + } + + public void setNote(String note) { + this.note = note; + } + + public User getCreator() { + return creator; + } + + public void setCreator(User creator) { + this.creator = creator; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + @Override public int describeContents() { return 0; } + + @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(this.url); + dest.writeString(this.columnUrl); + dest.writeString(this.contentUrl); + dest.writeInt(this.id); + dest.writeString(this.note); + dest.writeParcelable(this.creator, flags); + dest.writeLong(this.createdAt != null ? this.createdAt.getTime() : -1); + dest.writeLong(this.updatedAt != null ? this.updatedAt.getTime() : -1); + } + + public ProjectCardModel() {} + + protected ProjectCardModel(Parcel in) { + this.url = in.readString(); + this.columnUrl = in.readString(); + this.contentUrl = in.readString(); + this.id = in.readInt(); + this.note = in.readString(); + this.creator = in.readParcelable(User.class.getClassLoader()); + long tmpCreatedAt = in.readLong(); + this.createdAt = tmpCreatedAt == -1 ? null : new Date(tmpCreatedAt); + long tmpUpdatedAt = in.readLong(); + this.updatedAt = tmpUpdatedAt == -1 ? null : new Date(tmpUpdatedAt); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public ProjectCardModel createFromParcel(Parcel source) {return new ProjectCardModel(source);} + + @Override public ProjectCardModel[] newArray(int size) {return new ProjectCardModel[size];} + }; +} diff --git a/app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java b/app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java new file mode 100644 index 000000000..19e56f90c --- /dev/null +++ b/app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java @@ -0,0 +1,109 @@ +package com.fastaccess.data.dao; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Date; + +/** + * Created by Hashemsergani on 11.09.17. + */ + +public class ProjectColumnModel implements Parcelable { + + private int id; + private String name; + private String url; + private String projectUrl; + private String cardsUrl; + private Date createdAt; + private Date updatedAt; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getProjectUrl() { + return projectUrl; + } + + public void setProjectUrl(String projectUrl) { + this.projectUrl = projectUrl; + } + + public String getCardsUrl() { + return cardsUrl; + } + + public void setCardsUrl(String cardsUrl) { + this.cardsUrl = cardsUrl; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + @Override public int describeContents() { return 0; } + + @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(this.id); + dest.writeString(this.name); + dest.writeString(this.url); + dest.writeString(this.projectUrl); + dest.writeString(this.cardsUrl); + dest.writeLong(this.createdAt != null ? this.createdAt.getTime() : -1); + dest.writeLong(this.updatedAt != null ? this.updatedAt.getTime() : -1); + } + + public ProjectColumnModel() {} + + protected ProjectColumnModel(Parcel in) { + this.id = in.readInt(); + this.name = in.readString(); + this.url = in.readString(); + this.projectUrl = in.readString(); + this.cardsUrl = in.readString(); + long tmpCreatedAt = in.readLong(); + this.createdAt = tmpCreatedAt == -1 ? null : new Date(tmpCreatedAt); + long tmpUpdatedAt = in.readLong(); + this.updatedAt = tmpUpdatedAt == -1 ? null : new Date(tmpUpdatedAt); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public ProjectColumnModel createFromParcel(Parcel source) {return new ProjectColumnModel(source);} + + @Override public ProjectColumnModel[] newArray(int size) {return new ProjectColumnModel[size];} + }; +} diff --git a/app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java b/app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java index 4072614f9..92244fb61 100644 --- a/app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/ProjectsModel.java @@ -16,7 +16,7 @@ public class ProjectsModel implements Parcelable { private String url; private String htmlUrl; private String columnsUrl; - private int id; + private long id; private String name; private String body; private int number; @@ -57,11 +57,11 @@ public void setColumnsUrl(String columnsUrl) { this.columnsUrl = columnsUrl; } - public int getId() { + public long getId() { return id; } - public void setId(int id) { + public void setId(long id) { this.id = id; } @@ -128,7 +128,7 @@ public void setUpdatedAt(Date updatedAt) { dest.writeString(this.url); dest.writeString(this.htmlUrl); dest.writeString(this.columnsUrl); - dest.writeInt(this.id); + dest.writeLong(this.id); dest.writeString(this.name); dest.writeString(this.body); dest.writeInt(this.number); @@ -145,7 +145,7 @@ protected ProjectsModel(Parcel in) { this.url = in.readString(); this.htmlUrl = in.readString(); this.columnsUrl = in.readString(); - this.id = in.readInt(); + this.id = in.readLong(); this.name = in.readString(); this.body = in.readString(); this.number = in.readInt(); @@ -157,7 +157,7 @@ protected ProjectsModel(Parcel in) { this.updatedAt = tmpUpdatedAt == -1 ? null : new Date(tmpUpdatedAt); } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Creator CREATOR = new Creator() { @Override public ProjectsModel createFromParcel(Parcel source) {return new ProjectsModel(source);} @Override public ProjectsModel[] newArray(int size) {return new ProjectsModel[size];} diff --git a/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt b/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt index 1f465d510..1525c2e23 100644 --- a/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt +++ b/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt @@ -1,6 +1,8 @@ package com.fastaccess.data.service import com.fastaccess.data.dao.Pageable +import com.fastaccess.data.dao.ProjectCardModel +import com.fastaccess.data.dao.ProjectColumnModel import com.fastaccess.data.dao.ProjectsModel import io.reactivex.Observable import retrofit2.http.GET @@ -24,4 +26,11 @@ interface ProjectsService { fun getOrgsProjects(@Path("org") org: String, @Query("page") page: Int): Observable> + @GET("projects/{projectId}/columns?per_page=100") + @Headers("Accept: application/vnd.github.inertia-preview+json") + fun getProjectColumns(@Path("projectId") projectId: Long): Observable> + + @GET("projects/columns/{columnId}/cards") + @Headers("Accept: application/vnd.github.inertia-preview+json") + fun getProjectCards(@Path("columnId") columnId: String): Observable> } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt index fec58940c..35f89fd71 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt @@ -8,6 +8,7 @@ import com.fastaccess.helper.BundleConstant import com.fastaccess.helper.Logger import com.fastaccess.provider.rest.RestProvider import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import com.fastaccess.ui.modules.repos.projects.list.details.ProjectPagerActivity import java.util.* /** @@ -22,13 +23,11 @@ class RepoProjectPresenter : BasePresenter(), RepoProjectMv @com.evernote.android.state.State var login: String = "" @com.evernote.android.state.State var repoId: String = "" - override fun onItemClick(position: Int, v: View?, item: ProjectsModel?) { - + override fun onItemClick(position: Int, v: View, item: ProjectsModel) { + ProjectPagerActivity.startActivity(v.context, login, repoId, item.id) } - override fun onItemLongClick(position: Int, v: View?, item: ProjectsModel?) { - - } + override fun onItemLongClick(position: Int, v: View?, item: ProjectsModel?) {} override fun onFragmentCreate(bundle: Bundle?) { bundle?.let { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt new file mode 100644 index 000000000..0e0113f65 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt @@ -0,0 +1,19 @@ +package com.fastaccess.ui.modules.repos.projects.list.columns + +import android.os.Bundle +import android.view.View +import com.fastaccess.R +import com.fastaccess.ui.base.BaseFragment +import com.fastaccess.ui.base.mvp.BaseMvp +import com.fastaccess.ui.base.mvp.presenter.BasePresenter + +/** + * Created by Hashemsergani on 11.09.17. + */ +class PorjectColumnsFragment : BaseFragment>() { + override fun providePresenter(): BasePresenter = BasePresenter() + + override fun fragmentLayout(): Int = R.layout.project_columns_layout + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerActivity.kt new file mode 100644 index 000000000..11dadb5c2 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerActivity.kt @@ -0,0 +1,126 @@ +package com.fastaccess.ui.modules.repos.projects.list.details + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v4.view.ViewPager +import android.view.MenuItem +import android.view.View +import butterknife.BindView +import com.airbnb.lottie.LottieAnimationView +import com.fastaccess.R +import com.fastaccess.data.dao.FragmentPagerAdapterModel +import com.fastaccess.data.dao.NameParser +import com.fastaccess.data.dao.ProjectColumnModel +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Bundler +import com.fastaccess.ui.adapter.FragmentsPagerAdapter +import com.fastaccess.ui.base.BaseActivity +import com.fastaccess.ui.modules.repos.RepoPagerActivity +import com.fastaccess.ui.widgets.CardsPagerTransformerBasic + +/** + * Created by Hashemsergani on 11.09.17. + */ + +class ProjectPagerActivity : BaseActivity(), ProjectPagerMvp.View { + + + @BindView(R.id.pager) lateinit var pager: ViewPager + @BindView(R.id.loading) lateinit var loading: LottieAnimationView + + override fun canBack(): Boolean = true + + override fun isSecured(): Boolean = false + + override fun providePresenter(): ProjectPagerPresenter = ProjectPagerPresenter() + + override fun onInitPager(list: List) { + hideProgress() + pager.adapter = FragmentsPagerAdapter(supportFragmentManager, FragmentPagerAdapterModel.buildForProjectColumns(list)) + } + + override fun showMessage(titleRes: Int, msgRes: Int) { + hideProgress() + super.showMessage(titleRes, msgRes) + } + + override fun showMessage(titleRes: String, msgRes: String) { + hideProgress() + super.showMessage(titleRes, msgRes) + } + + override fun showErrorMessage(msgRes: String) { + hideProgress() + super.showErrorMessage(msgRes) + } + + override fun showProgress(resId: Int) { + loading.visibility = View.VISIBLE + loading.playAnimation() + } + + override fun hideProgress() { + loading.cancelAnimation() + loading.visibility = View.GONE + } + + override fun layout(): Int = R.layout.projects_activity_layout + + override fun isTransparent(): Boolean = true + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + return when (item?.itemId) { + android.R.id.home -> { + if (!presenter.login.isBlank() && !presenter.repoId.isBlank()) { + val nameParse = NameParser("") + nameParse.name = presenter.repoId + nameParse.username = presenter.login + nameParse.isEnterprise = isEnterprise + RepoPagerActivity.startRepoPager(this, nameParse) + } + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + pager.clipToPadding = false + val pageMargin = resources.getDimensionPixelSize(R.dimen.spacing_normal) + pager.pageMargin = pageMargin + pager.setPageTransformer(true, CardsPagerTransformerBasic(4, 10)) + pager.setPadding(pageMargin, pageMargin, pageMargin, pageMargin) + + if (savedInstanceState == null) { + presenter.onActivityCreated(intent) + } else if (presenter.getColumns().isEmpty() && !presenter.isApiCalled) { + presenter.onRetrieveColumns() + } else { + onInitPager(presenter.getColumns()) + } + } + + companion object { + fun startActivity(context: Context, login: String, repoId: String, projectId: Long) { + context.startActivity(getIntent(context, login, repoId, projectId)) + } + + fun getIntent(context: Context, login: String, repoId: String, projectId: Long): Intent { + val intent = Intent(context, ProjectPagerActivity::class.java) + intent.putExtras(Bundler.start() + .put(BundleConstant.ID, projectId) + .put(BundleConstant.ITEM, repoId) + .put(BundleConstant.EXTRA, login) + .end()) + return intent + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerMvp.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerMvp.kt new file mode 100644 index 000000000..7e4c0e430 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerMvp.kt @@ -0,0 +1,23 @@ +package com.fastaccess.ui.modules.repos.projects.list.details + +import android.content.Intent +import com.fastaccess.data.dao.ProjectColumnModel +import com.fastaccess.ui.base.mvp.BaseMvp + +/** + * Created by Hashemsergani on 11.09.17. + */ +interface ProjectPagerMvp { + + interface View : BaseMvp.FAView { + fun onInitPager(list: List) + } + + interface Presenter { + fun onActivityCreated(intent: Intent?) + + fun onRetrieveColumns() + + fun getColumns(): ArrayList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerPresenter.kt new file mode 100644 index 000000000..edfc7ae1b --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerPresenter.kt @@ -0,0 +1,55 @@ +package com.fastaccess.ui.modules.repos.projects.list.details + +import android.content.Intent +import com.fastaccess.R +import com.fastaccess.data.dao.ProjectColumnModel +import com.fastaccess.helper.BundleConstant +import com.fastaccess.provider.rest.RestProvider +import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import io.reactivex.Observable + +/** + * Created by Hashemsergani on 11.09.17. + */ +class ProjectPagerPresenter : BasePresenter(), ProjectPagerMvp.Presenter { + + private val columns = arrayListOf() + @com.evernote.android.state.State var projectId: Long = -1 + @com.evernote.android.state.State var repoId: String = "" + @com.evernote.android.state.State var login: String = "" + + override fun getColumns(): ArrayList = columns + + + override fun onRetrieveColumns() { + makeRestCall(RestProvider.getProjectsService(isEnterprise).getProjectColumns(projectId) + .flatMap { + if (it.items != null) { + return@flatMap Observable.just(it.items) + } + return@flatMap Observable.just(listOf()) + }, { t -> + columns.clear() + columns.addAll(t) + sendToView { it.onInitPager(columns) } + }) + } + + override fun onActivityCreated(intent: Intent?) { + intent?.let { + it.extras?.let { + projectId = it.getLong(BundleConstant.ID) + repoId = it.getString(BundleConstant.ITEM) + login = it.getString(BundleConstant.EXTRA) + } + } + if (columns.isEmpty()) { + if (projectId > 0) + onRetrieveColumns() + else + sendToView { it.showMessage(R.string.error, R.string.unexpected_error) } + } else { + sendToView { it.onInitPager(columns) } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml b/app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml new file mode 100644 index 000000000..691c766c7 --- /dev/null +++ b/app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/projects_activity_layout.xml b/app/src/main/res/layouts/main_layouts/layout/projects_activity_layout.xml new file mode 100644 index 000000000..848b58318 --- /dev/null +++ b/app/src/main/res/layouts/main_layouts/layout/projects_activity_layout.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index b3832d3da..0b0e929fe 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { assertjVersion = '2.5.0' espresseVersion = '2.2.2' requery = '1.3.2' - kotlin_version = '1.1.4-3' + kotlin_version = '1.1.4-2' commonmark = '0.9.0' } repositories { @@ -26,7 +26,7 @@ buildscript { classpath 'com.google.gms:google-services:3.0.0' classpath 'com.novoda:gradle-build-properties-plugin:0.3' classpath 'com.dicedmelon.gradle:jacoco-android:0.1.2' - classpath 'io.fabric.tools:gradle:1.22.2' + classpath 'io.fabric.tools:gradle:1.24.1' classpath 'com.apollographql.apollo:gradle-plugin:0.4.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.github.viswaramamoorthy:gradle-util-plugins:0.1.0-RELEASE" From 91b5c048c8b01bd912f8c6e1118b84ac5d900cd9 Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Mon, 11 Sep 2017 21:48:02 +0200 Subject: [PATCH 47/49] showing project columns & cards --- app/src/main/AndroidManifest.xml | 2 +- .../data/dao/FragmentPagerAdapterModel.java | 7 +- .../data/dao/ProjectColumnModel.java | 12 +- .../data/service/ProjectsService.kt | 2 +- .../ui/adapter/ColumnCardAdapter.kt | 21 +++ .../viewholder/ColumnCardViewHolder.kt | 37 +++++ .../projects/columns/ProjectColumnFragment.kt | 133 ++++++++++++++++++ .../projects/columns/ProjectColumnMvp.kt | 21 +++ .../columns/ProjectColumnPresenter.kt | 59 ++++++++ .../details/ProjectPagerActivity.kt | 6 +- .../{list => }/details/ProjectPagerMvp.kt | 2 +- .../details/ProjectPagerPresenter.kt | 25 +++- .../projects/list/RepoProjectPresenter.kt | 4 +- .../list/columns/PorjectColumnsFragment.kt | 19 --- .../scroll/RecyclerViewFastScroller.java | 2 + .../layout/project_columns_layout.xml | 11 +- .../layout/column_card_row_layout.xml | 61 ++++++++ app/src/main/res/values/strings.xml | 2 + 18 files changed, 377 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/com/fastaccess/ui/adapter/ColumnCardAdapter.kt create mode 100644 app/src/main/java/com/fastaccess/ui/adapter/viewholder/ColumnCardViewHolder.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnFragment.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnMvp.kt create mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnPresenter.kt rename app/src/main/java/com/fastaccess/ui/modules/repos/projects/{list => }/details/ProjectPagerActivity.kt (96%) rename app/src/main/java/com/fastaccess/ui/modules/repos/projects/{list => }/details/ProjectPagerMvp.kt (88%) rename app/src/main/java/com/fastaccess/ui/modules/repos/projects/{list => }/details/ProjectPagerPresenter.kt (64%) delete mode 100644 app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt create mode 100644 app/src/main/res/layouts/row_layouts/layout/column_card_row_layout.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 20bf78103..a8dc09333 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -250,7 +250,7 @@ buildForProjectColumns(@NonNull List models) { + @NonNull public static List buildForProjectColumns(@NonNull List models, boolean isCollaborator) { return Stream.of(models) - .map(projectColumnModel -> new FragmentPagerAdapterModel("", new PorjectColumnsFragment())) + .map(projectColumnModel -> new FragmentPagerAdapterModel("", ProjectColumnFragment.Companion + .newInstance(projectColumnModel, isCollaborator))) .toList(); } } diff --git a/app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java b/app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java index 19e56f90c..e24920249 100644 --- a/app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/ProjectColumnModel.java @@ -11,7 +11,7 @@ public class ProjectColumnModel implements Parcelable { - private int id; + private long id; private String name; private String url; private String projectUrl; @@ -19,11 +19,11 @@ public class ProjectColumnModel implements Parcelable { private Date createdAt; private Date updatedAt; - public int getId() { + public long getId() { return id; } - public void setId(int id) { + public void setId(long id) { this.id = id; } @@ -78,7 +78,7 @@ public void setUpdatedAt(Date updatedAt) { @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(this.id); + dest.writeLong(this.id); dest.writeString(this.name); dest.writeString(this.url); dest.writeString(this.projectUrl); @@ -90,7 +90,7 @@ public void setUpdatedAt(Date updatedAt) { public ProjectColumnModel() {} protected ProjectColumnModel(Parcel in) { - this.id = in.readInt(); + this.id = in.readLong(); this.name = in.readString(); this.url = in.readString(); this.projectUrl = in.readString(); @@ -101,7 +101,7 @@ protected ProjectColumnModel(Parcel in) { this.updatedAt = tmpUpdatedAt == -1 ? null : new Date(tmpUpdatedAt); } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Creator CREATOR = new Creator() { @Override public ProjectColumnModel createFromParcel(Parcel source) {return new ProjectColumnModel(source);} @Override public ProjectColumnModel[] newArray(int size) {return new ProjectColumnModel[size];} diff --git a/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt b/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt index 1525c2e23..53231ea50 100644 --- a/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt +++ b/app/src/main/java/com/fastaccess/data/service/ProjectsService.kt @@ -32,5 +32,5 @@ interface ProjectsService { @GET("projects/columns/{columnId}/cards") @Headers("Accept: application/vnd.github.inertia-preview+json") - fun getProjectCards(@Path("columnId") columnId: String): Observable> + fun getProjectCards(@Path("columnId") columnId: Long, @Query("page") page: Int): Observable> } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/adapter/ColumnCardAdapter.kt b/app/src/main/java/com/fastaccess/ui/adapter/ColumnCardAdapter.kt new file mode 100644 index 000000000..25d939472 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/adapter/ColumnCardAdapter.kt @@ -0,0 +1,21 @@ +package com.fastaccess.ui.adapter + +import android.view.ViewGroup +import com.fastaccess.data.dao.ProjectCardModel +import com.fastaccess.ui.adapter.viewholder.ColumnCardViewHolder +import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter +import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder + +/** + * Created by Hashemsergani on 11.09.17. + */ +class ColumnCardAdapter(date: ArrayList, val isOwner: Boolean) : BaseRecyclerAdapter>(date) { + + override fun viewHolder(parent: ViewGroup, viewType: Int): ColumnCardViewHolder = ColumnCardViewHolder.newInstance(parent, this, isOwner) + + override fun onBindView(holder: ColumnCardViewHolder?, position: Int) { + holder?.bind(data[position]) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ColumnCardViewHolder.kt b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ColumnCardViewHolder.kt new file mode 100644 index 000000000..d568f2376 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ColumnCardViewHolder.kt @@ -0,0 +1,37 @@ +package com.fastaccess.ui.adapter.viewholder + +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import butterknife.BindView +import com.fastaccess.R +import com.fastaccess.data.dao.ProjectCardModel +import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter +import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder + +/** + * Created by Hashemsergani on 11.09.17. + */ +class ColumnCardViewHolder private constructor(item: View, adapter: BaseRecyclerAdapter<*, *, *>, val isOwner: Boolean) + : BaseViewHolder(item, adapter) { + + @BindView(R.id.title) lateinit var title: TextView + @BindView(R.id.addedBy) lateinit var addedBy: TextView + @BindView(R.id.editCard) lateinit var editCard: View + + init { + editCard.setOnClickListener(this) + } + + override fun bind(t: ProjectCardModel) { + title.text = t.note + addedBy.text = itemView.context.getString(R.string.card_added_by, t.creator?.login) + editCard.visibility = if (isOwner) View.VISIBLE else View.GONE + } + + companion object { + fun newInstance(parent: ViewGroup, adapter: BaseRecyclerAdapter<*, *, *>, isOwner: Boolean): ColumnCardViewHolder { + return ColumnCardViewHolder(getView(parent, R.layout.column_card_row_layout), adapter, isOwner) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnFragment.kt new file mode 100644 index 000000000..bda5cd4d8 --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnFragment.kt @@ -0,0 +1,133 @@ +package com.fastaccess.ui.modules.repos.projects.columns + +import android.os.Bundle +import android.support.annotation.StringRes +import android.support.v4.widget.SwipeRefreshLayout +import android.view.View +import butterknife.BindView +import butterknife.OnClick +import com.fastaccess.R +import com.fastaccess.data.dao.ProjectCardModel +import com.fastaccess.data.dao.ProjectColumnModel +import com.fastaccess.helper.BundleConstant +import com.fastaccess.helper.Bundler +import com.fastaccess.provider.rest.loadmore.OnLoadMore +import com.fastaccess.ui.adapter.ColumnCardAdapter +import com.fastaccess.ui.base.BaseFragment +import com.fastaccess.ui.widgets.FontTextView +import com.fastaccess.ui.widgets.StateLayout +import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView +import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller + +/** + * Created by Hashemsergani on 11.09.17. + */ +class ProjectColumnFragment : BaseFragment(), ProjectColumnMvp.View { + + @BindView(R.id.recycler) lateinit var recycler: DynamicRecyclerView + @BindView(R.id.refresh) lateinit var refresh: SwipeRefreshLayout + @BindView(R.id.stateLayout) lateinit var stateLayout: StateLayout + @BindView(R.id.fastScroller) lateinit var fastScroller: RecyclerViewFastScroller + @BindView(R.id.columnName) lateinit var columnName: FontTextView + @BindView(R.id.editColumnHolder) lateinit var editColumnHolder: View + + private var onLoadMore: OnLoadMore? = null + private val adapter by lazy { ColumnCardAdapter(presenter.getCards(), isOwner()) } + + @OnClick(R.id.editColumn) fun onEditColumn() {} + @OnClick(R.id.deleteColumn) fun onDeleteColumn() {} + @OnClick(R.id.addCard) fun onAddCard() {} + + + override fun onNotifyAdapter(items: List?, page: Int) { + hideProgress() + if (items == null || items.isEmpty()) { + adapter.clear() + return + } + if (page <= 1) { + adapter.insertItems(items) + } else { + adapter.addItems(items) + } + } + + override fun getLoadMore(): OnLoadMore { + if (onLoadMore == null) { + onLoadMore = OnLoadMore(presenter) + } + onLoadMore!!.parameter = getColumn().id + return onLoadMore!! + } + + override fun providePresenter(): ProjectColumnPresenter = ProjectColumnPresenter() + + override fun fragmentLayout(): Int = R.layout.project_columns_layout + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val column = getColumn() + columnName.text = column.name + refresh.setOnRefreshListener { presenter.onCallApi(1, column.id) } + stateLayout.setOnReloadListener { presenter.onCallApi(1, column.id) } + stateLayout.setEmptyText(R.string.no_cards) + recycler.setEmptyView(stateLayout, refresh) + getLoadMore().initialize(presenter.currentPage, presenter.previousTotal) + adapter.listener = presenter + recycler.adapter = adapter + recycler.addOnScrollListener(getLoadMore()) + fastScroller.attachRecyclerView(recycler) + if (presenter.getCards().isEmpty() && !presenter.isApiCalled) { + presenter.onCallApi(1, column.id) + } + } + + override fun showProgress(@StringRes resId: Int) { + refresh.isRefreshing = true + stateLayout.showProgress() + } + + override fun hideProgress() { + refresh.isRefreshing = false + stateLayout.hideProgress() + } + + override fun showErrorMessage(message: String) { + showReload() + super.showErrorMessage(message) + } + + override fun showMessage(titleRes: Int, msgRes: Int) { + showReload() + super.showMessage(titleRes, msgRes) + } + + override fun onScrollTop(index: Int) { + super.onScrollTop(index) + recycler?.scrollToPosition(0) + } + + override fun onDestroyView() { + recycler.removeOnScrollListener(getLoadMore()) + super.onDestroyView() + } + + private fun showReload() { + hideProgress() + stateLayout.showReload(adapter.itemCount) + } + + private fun getColumn(): ProjectColumnModel = arguments.getParcelable(BundleConstant.ITEM) + + private fun isOwner(): Boolean = arguments.getBoolean(BundleConstant.EXTRA) + + companion object { + fun newInstance(column: ProjectColumnModel, isCollaborator: Boolean): ProjectColumnFragment { + val fragment = ProjectColumnFragment() + fragment.arguments = Bundler.start() + .put(BundleConstant.ITEM, column) + .put(BundleConstant.EXTRA, isCollaborator) + .end() + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnMvp.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnMvp.kt new file mode 100644 index 000000000..ef244b28d --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnMvp.kt @@ -0,0 +1,21 @@ +package com.fastaccess.ui.modules.repos.projects.columns + +import com.fastaccess.data.dao.ProjectCardModel +import com.fastaccess.provider.rest.loadmore.OnLoadMore +import com.fastaccess.ui.base.mvp.BaseMvp +import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder + +/** + * Created by Hashemsergani on 11.09.17. + */ + +interface ProjectColumnMvp { + interface View : BaseMvp.FAView { + fun onNotifyAdapter(items: List?, page: Int) + fun getLoadMore(): OnLoadMore + } + + interface Presenter : BaseViewHolder.OnItemClickListener, BaseMvp.PaginationListener { + fun getCards(): ArrayList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnPresenter.kt new file mode 100644 index 000000000..9fca8046b --- /dev/null +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/columns/ProjectColumnPresenter.kt @@ -0,0 +1,59 @@ +package com.fastaccess.ui.modules.repos.projects.columns + +import android.view.View +import com.fastaccess.data.dao.ProjectCardModel +import com.fastaccess.helper.Logger +import com.fastaccess.provider.rest.RestProvider +import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import java.util.* + +/** + * Created by Hashemsergani on 11.09.17. + */ + +class ProjectColumnPresenter : BasePresenter(), ProjectColumnMvp.Presenter { + + + private val projects = ArrayList() + private var page: Int = 0 + private var previousTotal: Int = 0 + private var lastPage = Integer.MAX_VALUE + + override fun onItemClick(position: Int, v: View?, item: ProjectCardModel?) {} + + override fun onItemLongClick(position: Int, v: View?, item: ProjectCardModel?) {} + + override fun getCards(): ArrayList = projects + + override fun getCurrentPage(): Int = page + + override fun getPreviousTotal(): Int = previousTotal + + override fun setCurrentPage(page: Int) { + this.page = page + } + + override fun setPreviousTotal(previousTotal: Int) { + this.previousTotal = previousTotal + } + + override fun onCallApi(page: Int, parameter: Long?): Boolean { + if (page == 1) { + lastPage = Integer.MAX_VALUE + sendToView { view -> view.getLoadMore().reset() } + } + if (page > lastPage || lastPage == 0) { + sendToView({ it.hideProgress() }) + return false + } + currentPage = page + makeRestCall(RestProvider.getProjectsService(isEnterprise).getProjectCards(parameter!!, page), + { response -> + lastPage = response.last + Logger.e(response.items as List?) + sendToView({ it.onNotifyAdapter(response.items, page) }) + }) + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerActivity.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/details/ProjectPagerActivity.kt similarity index 96% rename from app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerActivity.kt rename to app/src/main/java/com/fastaccess/ui/modules/repos/projects/details/ProjectPagerActivity.kt index 11dadb5c2..a756458d7 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/details/ProjectPagerActivity.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/details/ProjectPagerActivity.kt @@ -1,4 +1,4 @@ -package com.fastaccess.ui.modules.repos.projects.list.details +package com.fastaccess.ui.modules.repos.projects.details import android.content.Context import android.content.Intent @@ -37,7 +37,8 @@ class ProjectPagerActivity : BaseActivity) { hideProgress() - pager.adapter = FragmentsPagerAdapter(supportFragmentManager, FragmentPagerAdapterModel.buildForProjectColumns(list)) + pager.adapter = FragmentsPagerAdapter(supportFragmentManager, FragmentPagerAdapterModel + .buildForProjectColumns(list, presenter.isCollaborator)) } override fun showMessage(titleRes: Int, msgRes: Int) { @@ -89,7 +90,6 @@ class ProjectPagerActivity : BaseActivity(), ProjectPage @com.evernote.android.state.State var projectId: Long = -1 @com.evernote.android.state.State var repoId: String = "" @com.evernote.android.state.State var login: String = "" + @com.evernote.android.state.State var isCollaborator: Boolean = false override fun getColumns(): ArrayList = columns override fun onRetrieveColumns() { - makeRestCall(RestProvider.getProjectsService(isEnterprise).getProjectColumns(projectId) + makeRestCall(Observable.zip(RestProvider.getProjectsService(isEnterprise).getProjectColumns(projectId), + RestProvider.getRepoService(isEnterprise).isCollaborator(login, repoId, Login.getUser().login), + BiFunction { items: Pageable, response: Response -> + isCollaborator = response.code() == 204 + return@BiFunction items + }) .flatMap { if (it.items != null) { return@flatMap Observable.just(it.items) } return@flatMap Observable.just(listOf()) - }, { t -> - columns.clear() - columns.addAll(t) - sendToView { it.onInitPager(columns) } - }) + }, + { t -> + columns.clear() + columns.addAll(t) + sendToView { it.onInitPager(columns) } + }) } override fun onActivityCreated(intent: Intent?) { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt index 35f89fd71..f9089135b 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt @@ -5,10 +5,9 @@ import android.view.View import com.fastaccess.data.dao.ProjectsModel import com.fastaccess.data.dao.types.IssueState import com.fastaccess.helper.BundleConstant -import com.fastaccess.helper.Logger import com.fastaccess.provider.rest.RestProvider import com.fastaccess.ui.base.mvp.presenter.BasePresenter -import com.fastaccess.ui.modules.repos.projects.list.details.ProjectPagerActivity +import com.fastaccess.ui.modules.repos.projects.details.ProjectPagerActivity import java.util.* /** @@ -63,7 +62,6 @@ class RepoProjectPresenter : BasePresenter(), RepoProjectMv makeRestCall(RestProvider.getProjectsService(isEnterprise) .getRepoProjects(login, repoId, parameter?.name, page), { response -> lastPage = response.last - Logger.e(response.items as List?) sendToView({ it.onNotifyAdapter(response.items, page) }) }) return true diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt deleted file mode 100644 index 0e0113f65..000000000 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/columns/PorjectColumnsFragment.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.fastaccess.ui.modules.repos.projects.list.columns - -import android.os.Bundle -import android.view.View -import com.fastaccess.R -import com.fastaccess.ui.base.BaseFragment -import com.fastaccess.ui.base.mvp.BaseMvp -import com.fastaccess.ui.base.mvp.presenter.BasePresenter - -/** - * Created by Hashemsergani on 11.09.17. - */ -class PorjectColumnsFragment : BaseFragment>() { - override fun providePresenter(): BasePresenter = BasePresenter() - - override fun fragmentLayout(): Int = R.layout.project_columns_layout - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {} -} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/scroll/RecyclerViewFastScroller.java b/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/scroll/RecyclerViewFastScroller.java index e7c246d82..ca41c0865 100755 --- a/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/scroll/RecyclerViewFastScroller.java +++ b/app/src/main/java/com/fastaccess/ui/widgets/recyclerview/scroll/RecyclerViewFastScroller.java @@ -229,6 +229,8 @@ private void setScrollerHeight(float y) { protected void hideShow() { if (recyclerView != null && recyclerView.getAdapter() != null) { setVisibility(recyclerView.getAdapter().getItemCount() > 10 ? VISIBLE : GONE); + } else { + setVisibility(GONE); } } } \ No newline at end of file diff --git a/app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml b/app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml index 691c766c7..22c30db4a 100644 --- a/app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml +++ b/app/src/main/res/layouts/main_layouts/layout/project_columns_layout.xml @@ -41,15 +41,16 @@ android:paddingTop="@dimen/spacing_normal"> + android:textColor="@color/white" + tools:text="One must need the visitor in order to study the lord of great awareness."/> - + \ No newline at end of file diff --git a/app/src/main/res/layouts/row_layouts/layout/column_card_row_layout.xml b/app/src/main/res/layouts/row_layouts/layout/column_card_row_layout.xml new file mode 100644 index 000000000..41b07da44 --- /dev/null +++ b/app/src/main/res/layouts/row_layouts/layout/column_card_row_layout.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7da4708b..668129825 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -563,4 +563,6 @@ This PR can\'t be merged now. Projects No Projects + No Cards + Added by %s From 5fb0e76bbc2c0729be1e5c80a3308edc494fca8e Mon Sep 17 00:00:00 2001 From: Jobin Johnson Date: Tue, 12 Sep 2017 06:52:07 +0000 Subject: [PATCH 48/49] show commit message with hash on commit chooser dialog. --- .../main/java/com/fastaccess/data/dao/GitCommitModel.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/fastaccess/data/dao/GitCommitModel.java b/app/src/main/java/com/fastaccess/data/dao/GitCommitModel.java index 415b1a632..61dd15767 100644 --- a/app/src/main/java/com/fastaccess/data/dao/GitCommitModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/GitCommitModel.java @@ -61,6 +61,11 @@ protected GitCommitModel(Parcel in) { }; @Override public String toString() { - return sha != null && sha.length() > 10 ? sha.substring(0, 10) : "N/A"; + if (message != null) { + return (sha != null && sha.length() > 7 ? sha.substring(0, 7) + " - " : "") + message.split(System.lineSeparator())[0]; + } else if (sha != null && sha.length() > 10) { + return sha.substring(0, 10); + } + return "N/A"; } } From 444803297b7571da63511b76264e0e1fb3344e5a Mon Sep 17 00:00:00 2001 From: k0shk0sh Date: Wed, 13 Sep 2017 20:44:27 +0200 Subject: [PATCH 49/49] this commit fixes #937 --- .../{pr => github}/PinnedRepos.graphql | 0 .../PullRequestTimeline.graphql | 0 .../main/graphql/github/RepoProject.graphql | 90 +++++++++++++++++ .../main/graphql/{pr => github}/schema.json | 0 .../fastaccess/data/dao/ReactionsModel.java | 2 +- .../data/dao/TabsCountStateModel.java | 29 +++++- .../dao/timeline/PullRequestReviewModel.java | 4 +- .../timeline/PullRequestTimelineModel.java | 2 +- .../provider/rest/ApolloProdivder.kt | 23 +++++ .../ui/adapter/ProfilePinnedReposAdapter.kt | 2 +- .../fastaccess/ui/adapter/ProjectsAdapter.kt | 6 +- .../ProfilePinnedReposViewHolder.kt | 2 +- .../adapter/viewholder/ProjectViewHolder.kt | 18 ++-- .../viewholder/PullRequestEventViewHolder.kt | 4 +- ...PullRequestTimelineCommentsViewHolder.java | 4 +- .../ui/modules/feeds/FeedsPresenter.java | 6 +- .../overview/ProfileOverviewFragment.java | 2 +- .../profile/overview/ProfileOverviewMvp.java | 2 +- .../overview/ProfileOverviewPresenter.java | 6 +- .../comments/CommitCommentsPresenter.java | 8 +- .../timeline/IssueTimelinePresenter.java | 11 ++- .../projects/RepoProjectsFragmentPager.kt | 39 +++++++- .../projects/list/RepoProjectFragment.kt | 24 ++++- .../repos/projects/list/RepoProjectMvp.kt | 9 +- .../projects/list/RepoProjectPresenter.kt | 96 ++++++++++++++++--- .../PullRequestTimelinePresenter.java | 14 ++- 26 files changed, 341 insertions(+), 62 deletions(-) rename app/src/main/graphql/{pr => github}/PinnedRepos.graphql (100%) rename app/src/main/graphql/{pr => github}/PullRequestTimeline.graphql (100%) create mode 100644 app/src/main/graphql/github/RepoProject.graphql rename app/src/main/graphql/{pr => github}/schema.json (100%) create mode 100644 app/src/main/java/com/fastaccess/provider/rest/ApolloProdivder.kt diff --git a/app/src/main/graphql/pr/PinnedRepos.graphql b/app/src/main/graphql/github/PinnedRepos.graphql similarity index 100% rename from app/src/main/graphql/pr/PinnedRepos.graphql rename to app/src/main/graphql/github/PinnedRepos.graphql diff --git a/app/src/main/graphql/pr/PullRequestTimeline.graphql b/app/src/main/graphql/github/PullRequestTimeline.graphql similarity index 100% rename from app/src/main/graphql/pr/PullRequestTimeline.graphql rename to app/src/main/graphql/github/PullRequestTimeline.graphql diff --git a/app/src/main/graphql/github/RepoProject.graphql b/app/src/main/graphql/github/RepoProject.graphql new file mode 100644 index 000000000..15b895db6 --- /dev/null +++ b/app/src/main/graphql/github/RepoProject.graphql @@ -0,0 +1,90 @@ +query repoProjectsOpen($owner: String!, $name: String!, $page: String) { +repository(owner: $owner, name: $name) { + projects(first: 30, states: OPEN, after: $page, orderBy: {field: CREATED_AT, direction: DESC}) { + totalCount + edges { + cursor + } + nodes { + name + number + body + createdAt + id + viewerCanUpdate + columns(first: 1) { + totalCount + } + } + } + } +} +query repoProjectsClosed($owner: String!, $name: String!, $page: String) { +repository(owner: $owner, name: $name) { + projects(first: 30, states: CLOSED, after: $page, orderBy: {field: CREATED_AT, direction: DESC}) { + totalCount + edges { + cursor + } + nodes { + name + number + body + createdAt + id + viewerCanUpdate + columns(first: 1) { + totalCount + } + } + } + } +} + +query getColumns($owner: String!, $name:String!,$number:Int!) { +repository(owner: $owner, name: $name) { + project(number: $number) { + name + viewerCanUpdate + columns(first: 100) { + nodes { + name + createdAt + id + cards(first: 100) { + nodes { + note + createdAt + url + content { + ... on Node { + __typename + } + ... on Issue { + title + url + number + issueState: state + } + ... on PullRequest { + title + url + number + PrState: state + } + ... on Comment { + body + author { + login + avatarUrl + url + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/graphql/pr/schema.json b/app/src/main/graphql/github/schema.json similarity index 100% rename from app/src/main/graphql/pr/schema.json rename to app/src/main/graphql/github/schema.json diff --git a/app/src/main/java/com/fastaccess/data/dao/ReactionsModel.java b/app/src/main/java/com/fastaccess/data/dao/ReactionsModel.java index 26793c228..ee84a9074 100644 --- a/app/src/main/java/com/fastaccess/data/dao/ReactionsModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/ReactionsModel.java @@ -13,7 +13,7 @@ import lombok.Getter; import lombok.NonNull; import lombok.Setter; -import pr.PullRequestTimelineQuery; +import github.PullRequestTimelineQuery; /** * Created by Kosh on 28 Mar 2017, 9:15 PM diff --git a/app/src/main/java/com/fastaccess/data/dao/TabsCountStateModel.java b/app/src/main/java/com/fastaccess/data/dao/TabsCountStateModel.java index f607bf309..dcb45ede0 100644 --- a/app/src/main/java/com/fastaccess/data/dao/TabsCountStateModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/TabsCountStateModel.java @@ -6,18 +6,39 @@ import java.io.Serializable; -import lombok.Getter; -import lombok.Setter; - /** * Created by Kosh on 27 Apr 2017, 6:10 PM */ -@Getter @Setter public class TabsCountStateModel implements Parcelable, Serializable { +public class TabsCountStateModel implements Parcelable, Serializable { private int count; private int tabIndex; @DrawableRes private int drawableId; + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public int getTabIndex() { + return tabIndex; + } + + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + } + + public int getDrawableId() { + return drawableId; + } + + public void setDrawableId(int drawableId) { + this.drawableId = drawableId; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; diff --git a/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestReviewModel.java b/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestReviewModel.java index 638893764..f260ea429 100644 --- a/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestReviewModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestReviewModel.java @@ -10,8 +10,8 @@ import java.util.ArrayList; import java.util.List; -import pr.PullRequestTimelineQuery; -import pr.type.PullRequestReviewState; +import github.PullRequestTimelineQuery; +import github.type.PullRequestReviewState; /** * Created by kosh on 20/08/2017. diff --git a/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestTimelineModel.java b/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestTimelineModel.java index fcddb8f91..0cd8bd06b 100644 --- a/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestTimelineModel.java +++ b/app/src/main/java/com/fastaccess/data/dao/timeline/PullRequestTimelineModel.java @@ -7,7 +7,7 @@ import lombok.Getter; import lombok.Setter; -import pr.PullRequestTimelineQuery; +import github.PullRequestTimelineQuery; /** * Created by kosh on 02/08/2017. diff --git a/app/src/main/java/com/fastaccess/provider/rest/ApolloProdivder.kt b/app/src/main/java/com/fastaccess/provider/rest/ApolloProdivder.kt new file mode 100644 index 000000000..230843348 --- /dev/null +++ b/app/src/main/java/com/fastaccess/provider/rest/ApolloProdivder.kt @@ -0,0 +1,23 @@ +package com.fastaccess.provider.rest + +import com.apollographql.apollo.ApolloClient +import com.fastaccess.BuildConfig +import com.fastaccess.helper.PrefGetter +import com.fastaccess.provider.scheme.LinkParserHelper + +/** + * Created by Hashemsergani on 12.09.17. + */ + +object ApolloProdivder { + + fun getApollo(enterprise: Boolean) = ApolloClient.builder() + .serverUrl("${if (enterprise && PrefGetter.isEnterprise()) { + "${LinkParserHelper.getEndpoint(PrefGetter.getEnterpriseUrl())}/" + } else { + BuildConfig.REST_URL + }}graphql") + .okHttpClient(RestProvider.provideOkHttpClient()) + .build() + +} \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/adapter/ProfilePinnedReposAdapter.kt b/app/src/main/java/com/fastaccess/ui/adapter/ProfilePinnedReposAdapter.kt index eb14b6996..de64981ff 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/ProfilePinnedReposAdapter.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/ProfilePinnedReposAdapter.kt @@ -4,7 +4,7 @@ import android.view.ViewGroup import com.fastaccess.ui.adapter.viewholder.ProfilePinnedReposViewHolder import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder -import pr.GetPinnedReposQuery +import github.GetPinnedReposQuery import java.text.NumberFormat /** diff --git a/app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt b/app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt index 2adc506c1..b56807ddf 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/ProjectsAdapter.kt @@ -1,16 +1,16 @@ package com.fastaccess.ui.adapter import android.view.ViewGroup -import com.fastaccess.data.dao.ProjectsModel import com.fastaccess.ui.adapter.viewholder.ProjectViewHolder import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder +import github.RepoProjectsOpenQuery /** * Created by kosh on 09/09/2017. */ -class ProjectsAdapter(data: ArrayList) : - BaseRecyclerAdapter>(data) { +class ProjectsAdapter(data: ArrayList) : + BaseRecyclerAdapter>(data) { override fun viewHolder(parent: ViewGroup, viewType: Int): ProjectViewHolder = ProjectViewHolder.newInstance(parent, this) diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProfilePinnedReposViewHolder.kt b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProfilePinnedReposViewHolder.kt index f26e124eb..e050efcb3 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProfilePinnedReposViewHolder.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProfilePinnedReposViewHolder.kt @@ -8,7 +8,7 @@ import com.fastaccess.R import com.fastaccess.ui.widgets.FontTextView import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder -import pr.GetPinnedReposQuery +import github.GetPinnedReposQuery import java.text.NumberFormat /** diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt index a954bb20c..5a3a8f090 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/ProjectViewHolder.kt @@ -4,34 +4,30 @@ import android.view.View import android.view.ViewGroup import butterknife.BindView import com.fastaccess.R -import com.fastaccess.data.dao.ProjectsModel import com.fastaccess.helper.ParseDateFormat import com.fastaccess.ui.widgets.FontTextView import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder +import github.RepoProjectsOpenQuery /** * Created by kosh on 09/09/2017. */ -class ProjectViewHolder(view: View, adapter: BaseRecyclerAdapter<*, *, *>) : BaseViewHolder(view, adapter) { +class ProjectViewHolder(view: View, adapter: BaseRecyclerAdapter<*, *, *>) : BaseViewHolder(view, adapter) { @BindView(R.id.description) lateinit var description: FontTextView @BindView(R.id.title) lateinit var title: FontTextView @BindView(R.id.date) lateinit var date: FontTextView - override fun bind(t: ProjectsModel) { - title.text = t.name - if (t.body.isNullOrBlank()) { + override fun bind(t: RepoProjectsOpenQuery.Node) { + title.text = t.name() + if (t.body().isNullOrBlank()) { description.visibility = View.GONE } else { description.visibility = View.VISIBLE - description.text = t.body - } - if (t.updatedAt == null) { - date.text = ParseDateFormat.getTimeAgo(t.createdAt) - } else { - date.text = ParseDateFormat.getTimeAgo(t.updatedAt) + description.text = t.body() } + date.text = ParseDateFormat.getTimeAgo(t.createdAt().toString()) } companion object { diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestEventViewHolder.kt b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestEventViewHolder.kt index c6f1d8eae..6b29ed9e6 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestEventViewHolder.kt +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestEventViewHolder.kt @@ -21,8 +21,8 @@ import com.fastaccess.ui.widgets.SpannableBuilder import com.fastaccess.ui.widgets.recyclerview.BaseRecyclerAdapter import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder import com.zzhoujay.markdown.style.CodeSpan -import pr.PullRequestTimelineQuery -import pr.type.StatusState +import github.PullRequestTimelineQuery +import github.type.StatusState /** * Created by kosh on 03/08/2017. diff --git a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestTimelineCommentsViewHolder.java b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestTimelineCommentsViewHolder.java index 36512e878..081339911 100644 --- a/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestTimelineCommentsViewHolder.java +++ b/app/src/main/java/com/fastaccess/ui/adapter/viewholder/PullRequestTimelineCommentsViewHolder.java @@ -30,8 +30,8 @@ import java.util.List; import butterknife.BindView; -import pr.PullRequestTimelineQuery; -import pr.type.ReactionContent; +import github.PullRequestTimelineQuery; +import github.type.ReactionContent; /** * Created by Kosh on 11 Nov 2016, 2:08 PM diff --git a/app/src/main/java/com/fastaccess/ui/modules/feeds/FeedsPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/feeds/FeedsPresenter.java index 6578a7f3c..085990aea 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/feeds/FeedsPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/feeds/FeedsPresenter.java @@ -185,7 +185,11 @@ public class FeedsPresenter extends BasePresenter implements Feed .collect(Collectors.toCollection(ArrayList::new))); } } else { - onItemClick(position, v, item); + Repo repo = item.getRepo(); + if (repo != null) { + NameParser parser = new NameParser(repo.getUrl()); + RepoPagerActivity.startRepoPager(v.getContext(), parser); + } } } } diff --git a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java index 0432f365c..15a531f6e 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java +++ b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewFragment.java @@ -52,7 +52,7 @@ import butterknife.BindView; import butterknife.OnClick; -import pr.GetPinnedReposQuery; +import github.GetPinnedReposQuery; import static android.view.Gravity.TOP; import static android.view.View.GONE; diff --git a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewMvp.java b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewMvp.java index 0e271ff53..a31172448 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewMvp.java +++ b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewMvp.java @@ -13,7 +13,7 @@ import java.util.ArrayList; import java.util.List; -import pr.GetPinnedReposQuery; +import github.GetPinnedReposQuery; /** * Created by Kosh on 03 Dec 2016, 9:15 AM diff --git a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java index 76302e9a2..55f91001a 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/profile/overview/ProfileOverviewPresenter.java @@ -8,12 +8,12 @@ import com.apollographql.apollo.ApolloCall; import com.apollographql.apollo.rx2.Rx2Apollo; -import com.fastaccess.App; import com.fastaccess.data.dao.model.Login; import com.fastaccess.data.dao.model.User; import com.fastaccess.helper.BundleConstant; import com.fastaccess.helper.InputHelper; import com.fastaccess.helper.RxHelper; +import com.fastaccess.provider.rest.ApolloProdivder; import com.fastaccess.provider.rest.RestProvider; import com.fastaccess.ui.base.mvp.presenter.BasePresenter; import com.fastaccess.ui.widgets.contributions.ContributionsDay; @@ -23,8 +23,8 @@ import java.util.ArrayList; import java.util.List; +import github.GetPinnedReposQuery; import io.reactivex.Observable; -import pr.GetPinnedReposQuery; /** * Created by Kosh on 03 Dec 2016, 9:16 AM @@ -106,7 +106,7 @@ class ProfileOverviewPresenter extends BasePresenter im } @SuppressWarnings("ConstantConditions") private void loadPinnedRepos(@NonNull String login) { - ApolloCall apolloCall = App.getInstance().getApolloClient() + ApolloCall apolloCall = ApolloProdivder.INSTANCE.getApollo(isEnterprise()) .query(GetPinnedReposQuery.builder() .login(login) .build()); diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsPresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsPresenter.java index 5d2f9edbd..404cd03e4 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsPresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/code/commit/details/comments/CommitCommentsPresenter.java @@ -37,6 +37,7 @@ class CommitCommentsPresenter extends BasePresenter impl @com.evernote.android.state.State String repoId; @com.evernote.android.state.State String login; @com.evernote.android.state.State String sha; + @com.evernote.android.state.State boolean isCollaborator; @Override public int getCurrentPage() { @@ -64,6 +65,11 @@ class CommitCommentsPresenter extends BasePresenter impl sendToView(CommitCommentsMvp.View::hideProgress); return false; } + if (page == 1) { + manageObservable(RestProvider.getRepoService(isEnterprise()).isCollaborator(login, repoId, + Login.getUser().getLogin()) + .doOnNext(booleanResponse -> isCollaborator = booleanResponse.code() == 204)); + } setCurrentPage(page); makeRestCall(RestProvider.getRepoService(isEnterprise()).getCommitComments(login, repoId, sha, page) .flatMap(listResponse -> { @@ -152,7 +158,7 @@ class CommitCommentsPresenter extends BasePresenter impl PopupMenu popupMenu = new PopupMenu(v.getContext(), v); popupMenu.inflate(R.menu.comments_menu); String username = Login.getUser().getLogin(); - boolean isOwner = CommentsHelper.isOwner(username, login, item.getUser().getLogin()); + boolean isOwner = CommentsHelper.isOwner(username, login, item.getUser().getLogin()) || isCollaborator; popupMenu.getMenu().findItem(R.id.delete).setVisible(isOwner); popupMenu.getMenu().findItem(R.id.edit).setVisible(isOwner); popupMenu.setOnMenuItemClickListener(item1 -> { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java index c2f38c8df..bc0721575 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/issues/issue/details/timeline/IssueTimelinePresenter.java @@ -46,6 +46,7 @@ private int page; private int previousTotal; private int lastPage = Integer.MAX_VALUE; + @com.evernote.android.state.State boolean isCollaborator; @Override public boolean isPreviouslyReacted(long commentId, int vId) { return getReactionsProvider().isPreviouslyReacted(commentId, vId); @@ -60,7 +61,7 @@ PopupMenu popupMenu = new PopupMenu(v.getContext(), v); popupMenu.inflate(R.menu.comments_menu); String username = Login.getUser().getLogin(); - boolean isOwner = CommentsHelper.isOwner(username, issue.getLogin(), item.getComment().getUser().getLogin()); + boolean isOwner = CommentsHelper.isOwner(username, issue.getLogin(), item.getComment().getUser().getLogin()) || isCollaborator; popupMenu.getMenu().findItem(R.id.delete).setVisible(isOwner); popupMenu.getMenu().findItem(R.id.edit).setVisible(isOwner); popupMenu.setOnMenuItemClickListener(item1 -> { @@ -112,7 +113,8 @@ PopupMenu popupMenu = new PopupMenu(v.getContext(), v); popupMenu.inflate(R.menu.comments_menu); String username = Login.getUser().getLogin(); - boolean isOwner = CommentsHelper.isOwner(username, item.getIssue().getLogin(), item.getIssue().getUser().getLogin()); + boolean isOwner = CommentsHelper.isOwner(username, item.getIssue().getLogin(), + item.getIssue().getUser().getLogin()) || isCollaborator; popupMenu.getMenu().findItem(R.id.edit).setVisible(isOwner); popupMenu.setOnMenuItemClickListener(item1 -> { if (getView() == null) return false; @@ -259,6 +261,11 @@ setCurrentPage(page); String login = parameter.getLogin(); String repoId = parameter.getRepoId(); + if (page == 1) { + manageObservable(RestProvider.getRepoService(isEnterprise()).isCollaborator(login, repoId, + Login.getUser().getLogin()) + .doOnNext(booleanResponse -> isCollaborator = booleanResponse.code() == 204)); + } int number = parameter.getNumber(); Observable> observable = RestProvider.getIssueService(isEnterprise()) .getTimeline(login, repoId, number, page) diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt index d55c5ee5f..61467b61f 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/RepoProjectsFragmentPager.kt @@ -6,32 +6,69 @@ import android.view.View import butterknife.BindView import com.fastaccess.R import com.fastaccess.data.dao.FragmentPagerAdapterModel +import com.fastaccess.data.dao.TabsCountStateModel import com.fastaccess.helper.BundleConstant import com.fastaccess.helper.Bundler +import com.fastaccess.helper.Logger +import com.fastaccess.helper.ViewHelper import com.fastaccess.ui.adapter.FragmentsPagerAdapter import com.fastaccess.ui.base.BaseFragment import com.fastaccess.ui.base.mvp.BaseMvp import com.fastaccess.ui.base.mvp.presenter.BasePresenter +import com.fastaccess.ui.modules.repos.RepoPagerMvp +import com.fastaccess.ui.widgets.SpannableBuilder import com.fastaccess.ui.widgets.ViewPagerView /** * Created by kosh on 09/09/2017. */ -class RepoProjectsFragmentPager : BaseFragment>() { +class RepoProjectsFragmentPager : BaseFragment>(), RepoPagerMvp.TabsBadgeListener { @BindView(R.id.tabs) lateinit var tabs: TabLayout @BindView(R.id.pager) lateinit var pager: ViewPagerView + private var counts = hashSetOf() override fun fragmentLayout(): Int = R.layout.centered_tabbed_viewpager + override fun onSaveInstanceState(outState: Bundle?) { + super.onSaveInstanceState(outState) + if (counts.isNotEmpty()) { + outState?.putSerializable("counts", counts) + } + } + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { pager.adapter = FragmentsPagerAdapter(childFragmentManager, FragmentPagerAdapterModel.buildForRepoProjects(context, arguments.getString(BundleConstant.EXTRA), arguments.getString(BundleConstant.ID))) tabs.setupWithViewPager(pager) + if (savedInstanceState != null) { + @Suppress("UNCHECKED_CAST") + counts = savedInstanceState.getSerializable("counts") as HashSet + Logger.e(counts) + if (!counts.isEmpty()) counts.onEach { updateCount(it) } + } } override fun providePresenter(): BasePresenter = BasePresenter() + override fun onSetBadge(tabIndex: Int, count: Int) { + val model = TabsCountStateModel() + model.tabIndex = tabIndex + model.count = count + counts.add(model) + tabs?.let { updateCount(model) } + } + + private fun updateCount(model: TabsCountStateModel) { + val tv = ViewHelper.getTabTextView(tabs, model.tabIndex) + tv.text = SpannableBuilder.builder() + .append(if (model.tabIndex == 0) getString(R.string.opened) else getString(R.string.closed)) + .append(" ") + .append("(") + .bold(model.count.toString()) + .append(")") + } + companion object { val TAG = RepoProjectsFragmentPager::class.java.simpleName fun newInstance(login: String, repoId: String): RepoProjectsFragmentPager { diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt index 812d97922..057e5a381 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectFragment.kt @@ -1,21 +1,23 @@ package com.fastaccess.ui.modules.repos.projects.list +import android.content.Context import android.os.Bundle import android.support.annotation.StringRes import android.support.v4.widget.SwipeRefreshLayout import android.view.View import butterknife.BindView import com.fastaccess.R -import com.fastaccess.data.dao.ProjectsModel import com.fastaccess.data.dao.types.IssueState import com.fastaccess.helper.BundleConstant import com.fastaccess.helper.Bundler import com.fastaccess.provider.rest.loadmore.OnLoadMore import com.fastaccess.ui.adapter.ProjectsAdapter import com.fastaccess.ui.base.BaseFragment +import com.fastaccess.ui.modules.repos.RepoPagerMvp import com.fastaccess.ui.widgets.StateLayout import com.fastaccess.ui.widgets.recyclerview.DynamicRecyclerView import com.fastaccess.ui.widgets.recyclerview.scroll.RecyclerViewFastScroller +import github.RepoProjectsOpenQuery /** * Created by kosh on 09/09/2017. @@ -29,11 +31,25 @@ class RepoProjectFragment : BaseFragment? = null private val adapter by lazy { ProjectsAdapter(presenter.getProjects()) } + private var badgeListener: RepoPagerMvp.TabsBadgeListener? = null + + override fun onAttach(context: Context?) { + super.onAttach(context) + if (parentFragment is RepoPagerMvp.TabsBadgeListener) { + badgeListener = parentFragment as RepoPagerMvp.TabsBadgeListener + } else if (context is RepoPagerMvp.TabsBadgeListener) { + badgeListener = context + } + } + override fun onDetach() { + badgeListener = null + super.onDetach() + } override fun providePresenter(): RepoProjectPresenter = RepoProjectPresenter() - override fun onNotifyAdapter(items: List?, page: Int) { + override fun onNotifyAdapter(items: List?, page: Int) { hideProgress() if (items == null || items.isEmpty()) { adapter.clear() @@ -46,6 +62,10 @@ class RepoProjectFragment : BaseFragment { if (onLoadMore == null) { onLoadMore = OnLoadMore(presenter) diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt index 91a5062a8..59db03396 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectMvp.kt @@ -1,11 +1,11 @@ package com.fastaccess.ui.modules.repos.projects.list import android.os.Bundle -import com.fastaccess.data.dao.ProjectsModel import com.fastaccess.data.dao.types.IssueState import com.fastaccess.provider.rest.loadmore.OnLoadMore import com.fastaccess.ui.base.mvp.BaseMvp import com.fastaccess.ui.widgets.recyclerview.BaseViewHolder +import github.RepoProjectsOpenQuery import java.util.* /** @@ -14,15 +14,16 @@ import java.util.* interface RepoProjectMvp { interface View : BaseMvp.FAView { - fun onNotifyAdapter(items: List?, page: Int) + fun onNotifyAdapter(items: List?, page: Int) fun getLoadMore(): OnLoadMore + fun onChangeTotalCount(count: Int) } - interface Presenter : BaseViewHolder.OnItemClickListener, + interface Presenter : BaseViewHolder.OnItemClickListener, BaseMvp.PaginationListener { fun onFragmentCreate(bundle: Bundle?) - fun getProjects(): ArrayList + fun getProjects(): ArrayList } } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt index f9089135b..782596aac 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/projects/list/RepoProjectPresenter.kt @@ -2,31 +2,35 @@ package com.fastaccess.ui.modules.repos.projects.list import android.os.Bundle import android.view.View -import com.fastaccess.data.dao.ProjectsModel +import com.apollographql.apollo.rx2.Rx2Apollo import com.fastaccess.data.dao.types.IssueState import com.fastaccess.helper.BundleConstant -import com.fastaccess.provider.rest.RestProvider +import com.fastaccess.provider.rest.ApolloProdivder import com.fastaccess.ui.base.mvp.presenter.BasePresenter import com.fastaccess.ui.modules.repos.projects.details.ProjectPagerActivity -import java.util.* +import github.RepoProjectsClosedQuery +import github.RepoProjectsOpenQuery +import io.reactivex.Observable /** * Created by kosh on 09/09/2017. */ class RepoProjectPresenter : BasePresenter(), RepoProjectMvp.Presenter { - private val projects = ArrayList() + private val projects = arrayListOf() private var page: Int = 0 private var previousTotal: Int = 0 private var lastPage = Integer.MAX_VALUE @com.evernote.android.state.State var login: String = "" @com.evernote.android.state.State var repoId: String = "" + var count: Int = 0 + val pages = arrayListOf() - override fun onItemClick(position: Int, v: View, item: ProjectsModel) { - ProjectPagerActivity.startActivity(v.context, login, repoId, item.id) + override fun onItemClick(position: Int, v: View, item: RepoProjectsOpenQuery.Node) { + ProjectPagerActivity.startActivity(v.context, login, repoId, item.number().toLong()) } - override fun onItemLongClick(position: Int, v: View?, item: ProjectsModel?) {} + override fun onItemLongClick(position: Int, v: View?, item: RepoProjectsOpenQuery.Node?) {} override fun onFragmentCreate(bundle: Bundle?) { bundle?.let { @@ -35,7 +39,7 @@ class RepoProjectPresenter : BasePresenter(), RepoProjectMv } } - override fun getProjects(): ArrayList = projects + override fun getProjects(): ArrayList = projects override fun getCurrentPage(): Int = page @@ -54,16 +58,80 @@ class RepoProjectPresenter : BasePresenter(), RepoProjectMv lastPage = Integer.MAX_VALUE sendToView { view -> view.getLoadMore().reset() } } - if (page > lastPage || lastPage == 0) { + if (page > lastPage || lastPage == 0 || parameter == null) { sendToView({ it.hideProgress() }) return false } currentPage = page - makeRestCall(RestProvider.getProjectsService(isEnterprise) - .getRepoProjects(login, repoId, parameter?.name, page), { response -> - lastPage = response.last - sendToView({ it.onNotifyAdapter(response.items, page) }) - }) + val apollo = ApolloProdivder.getApollo(isEnterprise) + if (parameter == IssueState.open) { + val query = RepoProjectsOpenQuery.builder() + .name(repoId) + .owner(login) + .page(getPage()) + .build() + makeRestCall(Rx2Apollo.from(apollo.query(query)) + .flatMap { + val list = arrayListOf() + it.data()?.repository()?.let { + it.projects().let { + pages.clear() + count = it.totalCount() + it.edges()?.let { + pages.addAll(it.map { it.cursor() }) + } + it.nodes()?.let { + list.addAll(it) + } + } + } + return@flatMap Observable.just(list) + }, + { + sendToView({ v -> + v.onNotifyAdapter(it, page) + if (page == 1) v.onChangeTotalCount(count) + }) + }) + } else { + val query = RepoProjectsClosedQuery.builder() + .name(repoId) + .owner(login) + .page(getPage()) + .build() + makeRestCall(Rx2Apollo.from(apollo.query(query)) + .flatMap { + val list = arrayListOf() + it.data()?.repository()?.let { + it.projects().let { + pages.clear() + count = it.totalCount() + it.edges()?.let { + pages.addAll(it.map { it.cursor() }) + } + it.nodes()?.let { + val toConvert = arrayListOf() + it.onEach { + val columns = RepoProjectsOpenQuery.Columns(it.columns().__typename(), it.columns().totalCount()) + val node = RepoProjectsOpenQuery.Node(it.__typename(), it.name(), it.number(), it.body(), + it.createdAt(), it.id(), it.viewerCanUpdate(), columns) + toConvert.add(node) + } + list.addAll(toConvert) + } + } + } + return@flatMap Observable.just(list) + }, + { + sendToView({ v -> + v.onNotifyAdapter(it, page) + if (page == 1) v.onChangeTotalCount(count) + }) + }) + } return true } + + private fun getPage(): String? = if (pages.isNotEmpty()) pages.last() else null } \ No newline at end of file diff --git a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java index 8e17d13f1..570dafb72 100644 --- a/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java +++ b/app/src/main/java/com/fastaccess/ui/modules/repos/pull_requests/pull_request/details/timeline/timeline/PullRequestTimelinePresenter.java @@ -26,7 +26,6 @@ import com.fastaccess.helper.ActivityHelper; import com.fastaccess.helper.BundleConstant; import com.fastaccess.helper.InputHelper; -import com.fastaccess.helper.Logger; import com.fastaccess.provider.rest.RestProvider; import com.fastaccess.provider.scheme.SchemeParser; import com.fastaccess.provider.timeline.CommentsHelper; @@ -54,6 +53,7 @@ public class PullRequestTimelinePresenter extends BasePresenter { @@ -119,7 +120,7 @@ public class PullRequestTimelinePresenter extends BasePresenter { if (getView() == null) return false; @@ -275,7 +276,7 @@ public class PullRequestTimelinePresenter extends BasePresenter { @@ -349,6 +350,11 @@ public class PullRequestTimelinePresenter extends BasePresenter isCollaborator = booleanResponse.code() == 204)); + } setCurrentPage(page); if (parameter.getHead() != null) { Observable> observable = Observable.zip(