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 caf42eed..d066b22f 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 @@ import lombok.Setter; @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 0ba3815f..1be01dbe 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 class PullStatusViewHolder extends BaseViewHolder 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 5d794f69..0effcdc2 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 b3c91c78..c5fa8d9c 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.R; 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.timeline.SourceModel; 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 26b716a2..06e52ea8 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 16174b7a..1805f931 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 a1be537c..6525676a 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 8e2839ee..0d0549ad 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 class EditorLinkImageDialogFragment extends BaseDialogFragment 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 3147699c..8dee36c2 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 00000000..d326ddf2 --- /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 00000000..204f2dc2 --- /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 00000000..c87f27fc --- /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 00000000..48b1048b --- /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 00000000..71205bd8 --- /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 00000000..774a8340 --- /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 00000000..ff413fb2 --- /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 00000000..4804804e --- /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 00000000..fe832721 --- /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 00000000..1dbacb3c --- /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 00000000..1a76093f --- /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 00000000..03fca073 --- /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 00000000..ae76808c --- /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 00000000..7f47fe59 --- /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 00000000..0389f90f --- /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 00000000..664805f6 --- /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 00000000..06fb153f --- /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 00000000..e8dc6060 --- /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 00000000..692404fe --- /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 00000000..adeb82ea --- /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 00000000..e5523587 --- /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 00000000..08263e84 --- /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 00000000..24f0844e --- /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 00000000..0c85d599 --- /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 00000000..c96e9f68 --- /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 00000000..b1c510c7 --- /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 00000000..c97198f4 --- /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 00000000..456bc221 --- /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 00000000..b22c15e0 --- /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 00000000..7d9b3190 --- /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 00000000..8cfbe5f4 --- /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 00000000..3e01ae9c --- /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 00000000..00fc053b --- /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 00000000..d9cfbe02 --- /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 00000000..0720a56f --- /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 00000000..808d7a3c --- /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 00000000..5ea200af --- /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 00000000..2d526417 --- /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 00000000..a523c465 --- /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 00000000..6dfe8577 --- /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 00000000..e5ecf104 --- /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 00000000..dd0b22ee --- /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 00000000..f6591850 --- /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 00000000..822a8f1d --- /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 00000000..c56ab0d0 --- /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 00000000..31969a16 --- /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 00000000..b71cf003 --- /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 00000000..47d7da57 --- /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 00000000..1cf57ee6 --- /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 00000000..ffb4026e --- /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 00000000..bd3ebfb9 --- /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 00000000..769dd19d --- /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 00000000..8a8be89f --- /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 00000000..83c7134f --- /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 00000000..2e4e939a --- /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 00000000..079a6eed --- /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 00000000..77b3604a --- /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 00000000..85f9fadc --- /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 00000000..a12317c8 --- /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 e7b4def4..57087241 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 | 53 ++++++++++--------- .../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, 124 insertions(+), 81 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 61657946..573f44dd 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 @@ import lombok.NoArgsConstructor; .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 b5fba597..73cbcb1f 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 com.fastaccess.R; 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 2c0e6310..dedc2b6b 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.net.Uri; 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()); - } - urlBuilder.appendQueryParameter("ref", branch); + if (isSvg) { + Uri svgBlob = Uri.parse(uri.toString().replace("blob/", "")); + return svgBlob.buildUpon().authority(RAW_AUTHORITY).build(); } - return urlBuilder != null ? urlBuilder.build() : uri; + 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)); + } + } + 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 5e60502f..30c5c68d 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 class SchemeParser { 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 class SchemeParser { 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 class SchemeParser { */ @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 class SchemeParser { } 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 428bab2d..6dc63a6c 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.load.resource.drawable.GlideDrawable; 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 bb7ca244..01e94d5e 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 class IssueTimelineViewHolder extends BaseViewHolder { 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 db12f7c2..e6da5aec 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 31a86374..7f7d7b0c 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 b9995484..121aea97 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 @@ public class ViewerFragment extends BaseFragment 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 bb79ebaf..94049894 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.Login; 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 a37bd8eb..62cabc42 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 c8c131f2..c982486c 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 24b0fd93..dc553a76 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 4d0773a9..75cb6b0d 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 f3425700..777e1fb0 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 77dcb5d0..4d37c0a3 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 b77fdf12..00000000 --- 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 8dee36c2..e03a897d 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 879673fe..1303a8bd 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 84ccc46f..a70280ad 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 9b93c6cc..b3e5cae5 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 79d18243..8afc043b 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 e6da5aec..e3b7f20e 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.design.widget.FloatingActionButton; 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 f9be7274..50b04eea 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 @@ public interface MainMvp { 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 7f7d7b0c..5dd36dc4 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 android.support.v4.widget.DrawerLayout; 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.base.mvp.presenter.BasePresenter; 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 @@ import static com.fastaccess.helper.AppHelper.getFragmentByTag; 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 f907237b..c2f38c8d 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 @@ import lombok.Getter; 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 94049894..4cee0cf7 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 ccd9e760..6b84a01d 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.adapter.ProfilePinnedReposAdapter; 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 5f8779ae..5ccaf819 100644 --- a/app/src/main/res/raw/changelog.html +++ b/app/src/main/res/raw/changelog.html @@ -13,34 +13,40 @@

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 11afebe6..2b827acf 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 661d44b3..15ff17a8 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 0465ccc8..8b8f51fa 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 1de10fde..9b4f7fdf 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 @@ public class FastHubAboutActivity extends MaterialAboutActivity { .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 91cf9aed..e999d0ca 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 class SlackBottomSheetDialog extends BaseBottomSheetDialog { @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 70aa6b0b..3e13f97e 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 aea0a71c..006afb24 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.app.NotificationCompat; 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 @@ public class NotificationSchedulerJobTask extends JobService { } 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 @@ public class NotificationSchedulerJobTask extends JobService { } 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 4f28bd14..a40a753b 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 d1041c51..4d69efd5 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 532f13f9..303ef6ee 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 3881656e..dbbac513 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 android.view.View; 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 0110f162..79ccfc1c 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 class CommitPagerActivity extends BaseActivity + + + \ 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 777e1fb0..873974bb 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 c7b96073..218a1741 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 class CodeViewerActivity extends BaseActivity { @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 121aea97..96f78085 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 @@ public class ViewerFragment extends BaseFragment 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 cd5622b0..a864e2d1 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.CommentRequestModel; 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.provider.rest.loadmore.OnLoadMore; 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 class PullRequestFilesFragment extends BaseFragment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 f5224de1..1c6b5db7 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 d222417b..ed531fce 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 4b4c3470..03cb104e 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 dd300dc2..37d0e14d 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 c3bdc61f..6a1d1306 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.view.View; 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.base.BaseFragment; 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 00000000..b8652491 --- /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 00000000..7a2d2246 --- /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 a864e2d1..f95095b5 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 @@ public class PullRequestFilesFragment extends BaseFragment + + + + + + + + + + + + + + + + + + \ 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 ae73d7f7..e25e1747 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 465ea2ac..5544661e 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 73cbcb1f..054a907d 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 com.fastaccess.R; 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 class PrefGetter { 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 59c28dd2..f728a6fc 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.evernote.android.state.StateSaver; 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 7f776c80..aade5618 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 1e2a70ed..012fcb9a 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 android.support.v4.app.DialogFragment; 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 a8afbc6e..1d5cefac 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 c580aeb4..5cd0ef79 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 5efd0847..48c34194 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 f59b1cb1..352051a3 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 class ActivityHelper { } 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 f728a6fc..68f841e3 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 012fcb9a..8a1222d6 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 68f841e3..ed7db43e 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 4413be49..2993bfd5 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 @@ import static com.fastaccess.data.dao.model.Repo.UPDATED_AT; 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 b3e5cae5..708d6360 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 0effcdc2..44e4d631 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 6b84a01d..0432f365 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 class ProfileOverviewFragment extends BaseFragment 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 fcc43eba..76302e9a 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 659abc19..d51e3199 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 1e5e88a8..b8cb6ef5 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 a97ccd85..0282ace2 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 class MarkDownProvider { 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 class MarkDownProvider { 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 class MarkDownProvider { 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 81477840..3fbeb4c5 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 37d0e14d..6122416f 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 44e4d631..5e3fe89c 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 class RepoIssuesPagerFragment extends BaseFragment 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 9de1e247..c27db2da 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 class RepoOpenedIssuesFragment extends BaseFragment 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 17a26fdb..11bb369f 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.view.View; 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.Bundler; 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 class RepoPullRequestPagerFragment 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 class RepoPullRequestFragment extends BaseFragment 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 4b9b6200..8e428ebd 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 abstract class InfiniteScroll extends RecyclerView.OnScrollListener { 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 c982486c..ee8c2667 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 ed531fce..65ec7fe7 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 ee63cad4..d5258b84 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 @@ import lombok.Setter; } @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 @@ import lombok.Setter; 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 6a8835db..5c78fc79 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 @@ public interface IssueService { 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 fff86780..4137ecd4 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 class AppHelper { 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 054a907d..f24bdf38 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 3fbeb4c5..362efc7f 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 dfe09b87..39a3325d 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 00000000..35d1ad0d --- /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 1dddbdd0..00000000 --- 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 9624342f..dd08c90e 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 a70280ad..6937184c 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 00000000..d5882bd9 --- /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 00000000..adc115f7 --- /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 00000000..5e837e85 --- /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 d7500a5e..a9c26cf5 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 68633490..7ce11705 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.BuildConfig; 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 class RestProvider { 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 class RestProvider { 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 1320e6f1..86cee4f2 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 df1af1dc..0b1325dc 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.view.View; 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 12abc715..ee63afae 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 6b9baad5..c615a020 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 40730cad..740d3709 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 d35bf4bc..303eda91 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 | 19 ++-- .../repo_file_header_layout.xml | 19 ++-- .../layout/delete_repo_file_layout.xml | 94 +++++++++++++++++++ .../layout/edit_repo_file_layout.xml | 4 +- .../layout/repo_file_header_layout.xml | 21 +++-- app/src/main/res/menu/download_share_menu.xml | 8 ++ app/src/main/res/values/strings.xml | 1 + 16 files changed, 283 insertions(+), 64 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 a9c26cf5..ee9328ff 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 0b1325dc..e82d2292 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.BundleConstant; 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 d6ce3858..8b7e7418 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 @@ package com.fastaccess.ui.modules.repos.code.files; 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 0b243a23..4a66270c 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.ActivityHelper; 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 class RepoFilePathFragment 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?) { 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 dd4a3835..2ab4fa76 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 00000000..9713a647 --- /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 00000000..f8208a56 --- /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 ee63afae..d624a675 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,15 +83,6 @@ android:padding="@dimen/spacing_micro" android:src="@drawable/ic_download"/> - + + \ 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 c615a020..0b087710 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,15 +87,6 @@ android:padding="@dimen/spacing_micro" android:src="@drawable/ic_download"/> - + + \ 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 00000000..fd278f40 --- /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 740d3709..afd0ec03 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 303eda91..e8b6ba28 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 @@ -80,16 +80,6 @@ android:padding="@dimen/spacing_micro" android:src="@drawable/ic_download"/> - - + + + \ 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 5544661e..882e13a4 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 30c5c68d..9d07961a 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 @@ public class SchemeParser { @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 cf5f89ee..2d2a30ac 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 | 79 +++++++++++-------- 5 files changed, 72 insertions(+), 40 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 9940b56c..f1c59295 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 @@ import static com.fastaccess.ui.widgets.DiffLineSpan.HUNK_TITLE; 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 @@ import static com.fastaccess.ui.widgets.DiffLineSpan.HUNK_TITLE; 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 f6e3d883..c4de7aaf 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 class CommitLinesViewHolder extends BaseViewHolder { @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 f653768a..23bb22f4 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.assignees.AssigneesDialogFragment; 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 class PullRequestPagerActivity extends BaseActivity 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 fe1aff7d..4a69d1d6 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"> + + + + + + + + + + + + \ 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 65ec7fe7..8cd02dd1 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 1be01dbe..9cca05ca 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.R; 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 class PullStatusViewHolder extends BaseViewHolder } @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 70698d0c..1eb82bdd 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 00000000..4c81c543 --- /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 8b7e7418..14c5602c 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.data.dao.model.RepoFile; 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 4cee0cf7..53f5807c 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 1dc6d877..2750fc1a 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 8b8f51fa..a9dd9512 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 9cecc1d4..b8c2b32b 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 00000000..f572342d --- /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 f67089a7..faeb0a9a 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 f572342d..68ba7cef 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 0282ace2..f83ccd94 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 class MarkDownProvider { 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 362efc7f..74d30329 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 23bb22f4..51bac251 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 class PullRequestPagerActivity extends BaseActivity, 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 8e428ebd..d25a0c7c 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 27f3de75..f110d1fa 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 class DynamicRecyclerView extends RecyclerView { } } + 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 d25a0c7c..5477272f 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 9cca05ca..82fb891a 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.R; 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 class PullStatusViewHolder extends BaseViewHolder } @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 53f5807c..8e17d13f 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.data.dao.types.ReactionTypes; 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 cdec7b99..979f9fa3 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 5b1f1453..6182df76 100644 --- a/app/src/main/java/com/fastaccess/App.java +++ b/app/src/main/java/com/fastaccess/App.java @@ -72,7 +72,7 @@ public class App extends Application { 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 fbfb288e..eaf66c5d 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 @@ import java.util.Date; private String htmlUrl; private String pullRequestUrl; private ReactionsModel reactions; + private String authorAssociation; public ReviewCommentModel() {} @@ -161,6 +162,14 @@ import java.util.Date; 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 @@ import java.util.Date; 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 @@ import java.util.Date; 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 @@ import java.util.Date; 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 @@ import java.util.Date; @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 eb00a991..ffcb0991 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 @@ import static com.fastaccess.data.dao.model.Comment.UPDATED_AT; 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 @@ import static com.fastaccess.data.dao.model.Comment.UPDATED_AT; 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 @@ import static com.fastaccess.data.dao.model.Comment.UPDATED_AT; 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 9cf75c62..6b26f0d5 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 82fb891a..6bb63784 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.R; 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 class PullStatusViewHolder extends BaseViewHolder } @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 5753b250..dd6eeff4 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 class ReviewCommentsViewHolder extends BaseViewHolder 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 bf92290e..920fa1e3 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 class TimelineCommentsViewHolder extends BaseViewHolder { 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 f83ccd94..7f2088d3 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 class MarkDownProvider { } 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 class MarkDownProvider { } 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 979f9fa3..c4f09ca1 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 6937184c..2dbbecd8 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 d5258b84..09eb11ff 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 @@ import lombok.Setter; 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 5ccaf819..4ee8ca70 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 68ba7cef..17c3cfb4 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 bc16cd71..3133b75a 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 89768701..b36dd012 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 7f2088d3..907c5973 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 006afb24..6940d85b 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 @@ public class NotificationSchedulerJobTask extends JobService { } 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 @@ public class NotificationSchedulerJobTask extends JobService { } 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 @@ public class NotificationSchedulerJobTask extends JobService { 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 @@ public class NotificationSchedulerJobTask extends JobService { .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 @@ public class NotificationSchedulerJobTask extends JobService { 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 4d69efd5..debe140e 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 149dd7e2..df35994b 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 5e3fe89c..df12f2b9 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 commentEditorFragment.onAddUserName(username); } + @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 (pager.getCurrentItem() == 1) { @@ -286,8 +293,4 @@ public class GistActivity extends BaseActivity getSupportFragmentManager().beginTransaction().hide(commentEditorFragment).commit(); } } - - @Override public void onCreateComment(String text, Bundle bundle) { - - } } 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 64870fa7..bbdcef30 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 @@ -199,6 +199,7 @@ public class GistCommentsFragment extends 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 2ab4fa76..f042656f 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 ca6ab1d7..0e5c7f4b 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 class IssuePagerActivity extends BaseActivity @@ -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 4ee8ca70..0f87b2a7 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 @@
  • (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 ad479246..21fa616c 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.helper.InputHelper; 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 @@ import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; 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 759fe787..8b8078a5 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 @@ import java.util.Set; * 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 class DrawableGetter implements Html.ImageGetter, Drawable.Callback { 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 6dc63a6c..35ff6c95 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 3478859e..b0bfac97 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 @@ package com.fastaccess.provider.timeline.handler.drawable; 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 @@ class UrlDrawable extends BitmapDrawable implements Drawable.Callback { } 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 17c3cfb4..f7aeec1e 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 6122416f..9b2b0a39 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 df12f2b9..6d7996ee 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 2a1ddcd8..a0223b5d 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.ForegroundImageView; 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 class GistActivity extends BaseActivity } @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 class GistActivity extends BaseActivity 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 bbdcef30..a520df8b 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.dialog.MessageDialogView; 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 class GistCommentsFragment extends BaseFragment 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 c3cee504..281f2399 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 GistCommentsMvp { 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 1fd3ecf7..699d4c48 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.SpannableBuilder; 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 class CommitPagerActivity extends BaseActivity 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 @@ public class CommitPagerActivity extends BaseActivity 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 029d6f8f..36351979 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 CommitCommentsMvp { 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 0e5c7f4b..4fa91883 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.ViewPagerView; 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 class IssuePagerActivity extends BaseActivity 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 5507a98f..864dcd59 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.dialog.MessageDialogView; 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 ed964702..30b5901c 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 @@ public interface IssueTimelineMvp { 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 9881fa48..eff5221e 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 class PullRequestPagerActivity extends BaseActivity 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 3b159078..fe9d2135 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.dialog.MessageDialogView; 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 58901ad9..21e20cb0 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 @@ public interface PullRequestTimelineMvp { @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 7416fd8c..1a023ca6 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 c4f09ca1..b3715652 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 6a0130ef..00000000 --- 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 a9dd9512..c93c5569 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 2dbbecd8..60dc4579 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 66107396..af293c90 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 8b8078a5..759fe787 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 @@ import java.util.Set; * 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 class DrawableGetter implements Html.ImageGetter { 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 b0bfac97..3478859e 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 @@ package com.fastaccess.provider.timeline.handler.drawable; 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 @@ class UrlDrawable extends BitmapDrawable { } 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 2d2a30ac..912acb68 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.TypeFaceHelper; 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 class RepoPagerActivity extends BaseActivity

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 @@
  • (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 238f4a76..00000000 --- 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 e03a897d..03b6eeac 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 a6a418ba..bccfb2ae 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 class ColorsProvider { .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 09eb11ff..36e3ca3e 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.extras.branches.BranchesFragment; 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 @@ import lombok.Setter; 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 00000000..4072614f --- /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 00000000..1f465d51 --- /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 7ce11705..a21f77ad 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.GistService; 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 class RestProvider { 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 ff1bab2d..a3d1a537 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 @@ public class TableHandler extends TagNodeHandler { 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 00000000..2adc506c --- /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 00000000..a954bb20 --- /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 25ed188e..3477aa69 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 f1961227..756689fc 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.provider.rest.RestProvider; 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 @@ class RepoPagerPresenter extends BasePresenter implements Rep 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 @@ class RepoPagerPresenter extends BasePresenter implements Rep 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 00000000..d55c5ee5 --- /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 00000000..812d9792 --- /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 00000000..91a5062a --- /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 00000000..fec58940 --- /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 00000000..75d0dde7 --- /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 2136f9ed..424d09d5 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 2750fc1a..a7da4708 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 ae76808c..58314821 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.app.PendingIntent; 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 @@ import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleResult; * 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 final class GooglePlayDriver implements Driver { @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 7de7c038..88f97f1e 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 @@ public class HtmlHelper { } 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 9b2b0a39..d3cec953 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 b45dac4e..1809861d 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 com.fastaccess.ui.base.mvp.presenter.BasePresenter; 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 @@ class RepoIssuesPresenter extends BasePresenter implements R @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 6182df76..e7a1d601 100644 --- a/app/src/main/java/com/fastaccess/App.java +++ b/app/src/main/java/com/fastaccess/App.java @@ -72,7 +72,7 @@ public class App extends Application { 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 2993bfd5..8a8c6306 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 @@ import static com.fastaccess.data.dao.model.Repo.UPDATED_AT; int networkCount; String starredUser; String reposOwner; + @Nullable boolean hasProjects; public Disposable save(Repo entity) { return Single.create(e -> { @@ -322,6 +323,7 @@ import static com.fastaccess.data.dao.model.Repo.UPDATED_AT; 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 @@ import static com.fastaccess.data.dao.model.Repo.UPDATED_AT; 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 912acb68..2e20a815 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 class RepoPagerActivity extends BaseActivity 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 424d09d5..2136f9ed 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 00000000..7f9e45e4 --- /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 03b6eeac..b3832d3d 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 4137ecd4..93b388a8 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 class AppHelper { 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 d24120f8..6dacd3b7 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 4137ecd4..52496c84 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 class AppHelper { 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 907c5973..6394fe67 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 android.widget.TextView; 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 @@ public class MarkDownProvider { 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 0c85d599..41076e76 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 @@ public abstract class JobService extends Service { 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 abstract class JobService extends Service { 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 @@ public abstract class JobService extends Service { } 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 8cd02dd1..20bf7810 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 @@ import lombok.Setter; .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 00000000..4f0fba75 --- /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 00000000..19e56f90 --- /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 4072614f..92244fb6 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 class ProjectsModel implements Parcelable { 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 class ProjectsModel implements Parcelable { 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 @@ public class ProjectsModel implements Parcelable { 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 @@ public class ProjectsModel implements Parcelable { 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 1f465d51..1525c2e2 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 fec58940..35f89fd7 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 00000000..0e0113f6 --- /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 00000000..11dadb5c --- /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 00000000..7e4c0e43 --- /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 00000000..edfc7ae1 --- /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 00000000..691c766c --- /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 00000000..848b5831 --- /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 b3832d3d..0b0e929f 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 20bf7810..a8dc0933 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 19e56f90..e2492024 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 @@ import java.util.Date; 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 class ProjectColumnModel implements Parcelable { @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 class ProjectColumnModel implements Parcelable { 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 @@ public class ProjectColumnModel implements Parcelable { 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 1525c2e2..53231ea5 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 00000000..25d93947 --- /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 00000000..d568f237 --- /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 00000000..bda5cd4d --- /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 00000000..ef244b28 --- /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 00000000..9fca8046 --- /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 11dadb5c..a756458d 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 35f89fd7..f9089135 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 0e0113f6..00000000 --- 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 e7c246d8..ca41c086 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 @@ public class RecyclerViewFastScroller extends FrameLayout { 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 691c766c..22c30db4 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 00000000..41b07da4 --- /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 a7da4708..66812982 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 415b1a63..61dd1576 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 @@ public class GitCommitModel implements Parcelable { }; @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 00000000..15b895db --- /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 26793c22..ee84a907 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 java.util.List; 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 f607bf30..dcb45ede 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 android.support.annotation.DrawableRes; 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 63889376..f260ea42 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 com.fastaccess.helper.ParseDateFormat; 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 fcddb8f9..0cd8bd06 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 java.util.List; 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 00000000..23084334 --- /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 eb14b699..de64981f 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 2adc506c..b56807dd 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 f26e124e..e050efcb 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 a954bb20..5a3a8f09 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 c6f1d8ea..6b29ed9e 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 36512e87..08133991 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 com.fastaccess.ui.widgets.recyclerview.BaseViewHolder; 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 6578a7f3..085990ae 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 0432f365..15a531f6 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 java.util.List; 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 0e271ff5..a3117244 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 com.fastaccess.ui.widgets.contributions.GitHubContributionsView; 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 76302e9a..55f91001 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 android.text.TextUtils; 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 com.fastaccess.ui.widgets.contributions.GitHubContributionsView; 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 5d2f9edb..404cd03e 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 c2f38c8d..bc072157 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 @@ import lombok.Getter; 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 @@ import lombok.Getter; 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 @@ import lombok.Getter; 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 @@ import lombok.Getter; 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 d55c5ee5..61467b61 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 812d9792..057e5a38 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 91a5062a..59db0339 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 f9089135..782596aa 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 8e17d13f..570dafb7 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.data.dao.types.ReactionTypes; 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(