Repository: halzhang/StartupNews Branch: master Commit: 05b04cec88ab Files: 184 Total size: 318.2 KB Directory structure: gitextract_me0tpa6o/ ├── .gitignore ├── .travis.yml ├── 3rdlib/ │ ├── commonlog/ │ │ ├── AndroidManifest.xml │ │ ├── README.md │ │ ├── build.gradle │ │ ├── build.xml │ │ ├── proguard-project.txt │ │ ├── project.properties │ │ └── src/ │ │ └── com/ │ │ └── halzhang/ │ │ └── android/ │ │ └── common/ │ │ └── CDLog.java │ └── commontoast/ │ ├── AndroidManifest.xml │ ├── README.md │ ├── build.gradle │ ├── build.xml │ ├── proguard-project.txt │ ├── project.properties │ └── src/ │ └── com/ │ └── halzhang/ │ └── android/ │ └── common/ │ └── CDToast.java ├── README.md ├── android-wait-for-emulator ├── app/ │ ├── build.gradle │ ├── libs/ │ │ └── libGoogleAnalyticsServices.jar │ ├── proguard-project.txt │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── halzhang/ │ │ └── android/ │ │ └── apps/ │ │ └── startupnews/ │ │ ├── ApplicationModule.java │ │ ├── Constants.java │ │ ├── MyApplication.java │ │ ├── SnApiComponent.java │ │ ├── analytics/ │ │ │ ├── MyExceptionParser.java │ │ │ └── Tracker.java │ │ ├── presenter/ │ │ │ ├── BasePresenter.java │ │ │ ├── BaseView.java │ │ │ ├── CommentsListContract.java │ │ │ ├── CommentsListPresenter.java │ │ │ ├── CommentsListPresenterModule.java │ │ │ ├── DiscussComponent.java │ │ │ ├── DiscussContract.java │ │ │ ├── DiscussPresenter.java │ │ │ ├── DiscussPresenterModule.java │ │ │ ├── LoginComponent.java │ │ │ ├── LoginContract.java │ │ │ ├── LoginPresenter.java │ │ │ ├── LoginPresenterModule.java │ │ │ ├── MainActivityContract.java │ │ │ ├── MainActivityPresenter.java │ │ │ ├── MainActivityPresenterModule.java │ │ │ ├── MainComponent.java │ │ │ ├── NewsListContract.java │ │ │ ├── NewsListFragmentComponent.java │ │ │ ├── NewsListPresenter.java │ │ │ └── NewsListPresenterModule.java │ │ ├── ui/ │ │ │ ├── AboutActivity.java │ │ │ ├── BaseActivity.java │ │ │ ├── BaseFragmentActivity.java │ │ │ ├── DiscussActivity.java │ │ │ ├── LauncherActivity.java │ │ │ ├── LoginActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── ShareHelper.java │ │ │ ├── SubmitActivity.java │ │ │ ├── fragment/ │ │ │ │ ├── BaseFragment.java │ │ │ │ ├── CommentsListFragment.java │ │ │ │ ├── LoginFragment.java │ │ │ │ ├── NewsListFragment.java │ │ │ │ └── SwipeRefreshRecyclerFragment.java │ │ │ ├── phone/ │ │ │ │ └── BrowseActivity.java │ │ │ ├── tablet/ │ │ │ │ ├── BrowseFragment.java │ │ │ │ └── DiscussFragment.java │ │ │ └── widgets/ │ │ │ ├── CardViewDividerDecoration.java │ │ │ ├── DividerDecoration.java │ │ │ ├── ObservableWebView.java │ │ │ ├── SwitchPreference.java │ │ │ └── WebViewController.java │ │ └── utils/ │ │ ├── ActivityScoped.java │ │ ├── ActivityUtils.java │ │ ├── AppUtils.java │ │ ├── CrashHandler.java │ │ ├── CustomTabsActivityHelper.java │ │ ├── CustomTabsHelper.java │ │ ├── DateUtils.java │ │ ├── FragmentScoped.java │ │ ├── PreferenceUtils.java │ │ └── UIUtils.java │ └── res/ │ ├── anim/ │ │ ├── push_down_out.xml │ │ ├── push_up_in.xml │ │ ├── slide_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ ├── drawable/ │ │ ├── action_bar_background.xml │ │ ├── background_boardless.xml │ │ ├── background_holo_dark.xml │ │ ├── background_holo_light.xml │ │ ├── bg_discuss_article.xml │ │ ├── fragment_shadow.xml │ │ ├── send_button_selector.xml │ │ └── sidebar_shadow.xml │ ├── layout/ │ │ ├── actionbar_indeterminate_progress.xml │ │ ├── activity_browse.xml │ │ ├── activity_discuss.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── browse_bar.xml │ │ ├── comment_list_item.xml │ │ ├── discuss_comment_item.xml │ │ ├── discuss_header_view.xml │ │ ├── fragment_browse.xml │ │ ├── fragment_discuss.xml │ │ ├── fragment_login.xml │ │ ├── news_list_item.xml │ │ ├── preference_toolbar.xml │ │ ├── ptr_list_layout.xml │ │ └── refresh_recycler_view_layout.xml │ ├── menu/ │ │ ├── activity_browse.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── fragment_browse.xml │ │ ├── fragment_discuss.xml │ │ └── fragment_news.xml │ ├── values/ │ │ ├── analytics.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── integers.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── xml/ │ └── preferences.xml ├── build.gradle ├── buildsystem/ │ └── dependencies.gradle ├── changelog.md ├── data/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── halzhang/ │ │ └── android/ │ │ └── startupnews/ │ │ └── data/ │ │ └── ApplicationTest.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── halzhang/ │ │ └── android/ │ │ └── startupnews/ │ │ └── data/ │ │ ├── Constant.java │ │ ├── CookieFactoryModule.java │ │ ├── JsoupConnectorModule.java │ │ ├── OkHttpClientModule.java │ │ ├── SessionManagerModule.java │ │ ├── SnApiModule.java │ │ ├── entity/ │ │ │ ├── SNComment.java │ │ │ ├── SNComments.java │ │ │ ├── SNDiscuss.java │ │ │ ├── SNFeed.java │ │ │ ├── SNNew.java │ │ │ ├── SNSession.java │ │ │ ├── SNUser.java │ │ │ └── Status.java │ │ ├── exception/ │ │ │ ├── LoginException.java │ │ │ ├── NetworkException.java │ │ │ └── SessionExpiredException.java │ │ ├── net/ │ │ │ ├── ISnApi.java │ │ │ ├── JsoupConnector.java │ │ │ └── SnApiImpl.java │ │ ├── parser/ │ │ │ ├── BaseHTMLParser.java │ │ │ ├── SNCommentsParser.java │ │ │ ├── SNCommentsParserV1.java │ │ │ ├── SNDiscussParser.java │ │ │ └── SNFeedParser.java │ │ └── utils/ │ │ ├── CookieFactoryImpl.java │ │ ├── NetworkUtils.java │ │ ├── OkHttpClientHelper.java │ │ ├── PersistentCookieStore.java │ │ ├── PrefUtils.java │ │ ├── SerializableCookie.java │ │ └── SessionManager.java │ └── res/ │ └── values/ │ └── strings.xml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── release/ │ ├── StartupNews-release-0.1.apk │ ├── StartupNews-release-0.2.0.0.apk │ ├── StartupNews-release-0.3.0.0.apk │ ├── StartupNews-release-0.3.1.0.apk │ ├── StartupNews-release-0.4.0.0.apk │ ├── StartupNews-release-0.5.0.0.apk │ ├── StartupNews-release-0.5.1.0.apk │ ├── StartupNews-release-0.5.2.0.apk │ ├── StartupNews-release-0.6.0.0.apk │ ├── StartupNews-release-1.0.0.0.apk │ ├── StartupNews-release-1.0.1.0.apk │ └── StartupNews-release.apk ├── settings.gradle └── wait_for_emulator.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # built application files *.ap_ # files for the dex VM *.dex # Java class files *.class # generated files bin/ gen/ # Local configuration file (sdk path, etc) local.properties # Eclipse project files .classpath .project .settings #build ant.properties *.keystore StartupNews/app/src/main/res/values/analytics.xml # IntelliJ files .idea *.iml out/ module_*.xml #gradle build/ .gradle/ ================================================ FILE: .travis.yml ================================================ language: android jdk: - openjdk7 - oraclejdk7 - oraclejdk8 env: matrix: - ANDROID_TARGET=android-23 ANDROID_ABI=armeabi-v7a android: components: - tools - platform-tools - build-tools-23.0.1 - android-23 - extra-android-support - extra-google-google_play_services - extra-google-m2repository - extra-android-m2repository notifications: email: true # Turn off caching to avoid any caching problems cache: false # Use the Travis Container-Based Infrastructure (see #203) sudo: false before_script: - echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a - emulator -avd test -no-skin -no-audio -no-window & - android-wait-for-emulator - adb shell input keyevent 82 & script: ./gradlew assembleDebug connectedCheck ================================================ FILE: 3rdlib/commonlog/AndroidManifest.xml ================================================ ================================================ FILE: 3rdlib/commonlog/README.md ================================================ CommonLog ========= Android Log library ================================================ FILE: 3rdlib/commonlog/build.gradle ================================================ buildscript { repositories { mavenCentral() } } apply plugin: 'com.android.library' dependencies { compile fileTree(dir:'libs',include:'*.jar') } android { def globalConfiguration = rootProject.extensions.getByName("ext") compileSdkVersion globalConfiguration.getAt("androidCompileSdkVersion") buildToolsVersion globalConfiguration.getAt("androidBuildToolsVersion") defaultConfig { minSdkVersion globalConfiguration.getAt("androidMinSdkVersion") targetSdkVersion globalConfiguration.getAt("androidTargetSdkVersion") } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } instrumentTest.setRoot('tests') } } ================================================ FILE: 3rdlib/commonlog/build.xml ================================================ ================================================ FILE: 3rdlib/commonlog/proguard-project.txt ================================================ # To enable ProGuard in your project, edit project.properties # to define the proguard.config property as described in that file. # # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in ${sdk.dir}/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: 3rdlib/commonlog/project.properties ================================================ # This file is automatically generated by Android Tools. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file must be checked in Version Control Systems. # # To customize properties used by the Ant build system edit # "ant.properties", and override values to adapt the script to your # project structure. # # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. target=android-17 android.library=true ================================================ FILE: 3rdlib/commonlog/src/com/halzhang/android/common/CDLog.java ================================================ package com.halzhang.android.common; import android.util.Log; /** * CommonLog
* log * * @author Hal * @version ec 24, 2012 9:55:18 PM */ public class CDLog { public static final String LOG_TAG = CDLog.class.getSimpleName(); public static final boolean DEBUG = true; private static final String LOG_PREFIX = "CDLOG_"; private static final int LOG_PREFIX_LENGTH = LOG_PREFIX.length(); private static final int MAX_LOG_TAG_LENGTH = 23; public static String makeLogTag(String str) { if (str.length() > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) { return LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1); } return LOG_PREFIX + str; } /** * WARNING: Don't use this when obfuscating class names with Proguard! */ public static String makeLogTag(Class cls) { return makeLogTag(cls.getSimpleName()); } public static void i(String tag, String msg, Throwable throwable) { if (DEBUG) { Log.i(tag, msg, throwable); } } public static void i(String tag, Object... objects) { StringBuilder builder = new StringBuilder(); for (Object obj : objects) { builder.append(obj); } i(tag, builder.toString()); } public static void i(String tag, String msg) { i(tag, msg, null); } public static void i(String msg) { i(LOG_TAG, msg, null); } public static void d(String tag, String msg, Throwable throwable) { if (DEBUG) { Log.d(tag, msg, throwable); } } public static void d(String tag, String msg) { d(tag, msg, null); } public static void d(String msg) { d(LOG_TAG, msg, null); } public static void w(String tag, String msg, Throwable throwable) { if (DEBUG) { Log.w(tag, msg, throwable); } } public static void w(String tag, String msg) { w(tag, msg, null); } public static void w(String msg) { w(LOG_TAG, msg, null); } public static void e(String tag, String msg, Throwable throwable) { if (DEBUG) { Log.e(tag, msg, throwable); } } public static void e(String tag, String msg) { e(tag, msg, null); } public static void e(String msg) { e(LOG_TAG, msg, null); } private CDLog() { } } ================================================ FILE: 3rdlib/commontoast/AndroidManifest.xml ================================================ ================================================ FILE: 3rdlib/commontoast/README.md ================================================ CommonToast =========== Android Toast library ================================================ FILE: 3rdlib/commontoast/build.gradle ================================================ buildscript { repositories { mavenCentral() } } apply plugin: 'com.android.library' dependencies { compile fileTree(dir:'libs',include:'*.jar') } android { def globalConfiguration = rootProject.extensions.getByName("ext") compileSdkVersion globalConfiguration.getAt("androidCompileSdkVersion") buildToolsVersion globalConfiguration.getAt("androidBuildToolsVersion") defaultConfig { minSdkVersion globalConfiguration.getAt("androidMinSdkVersion") targetSdkVersion globalConfiguration.getAt("androidTargetSdkVersion") } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } instrumentTest.setRoot('tests') } } ================================================ FILE: 3rdlib/commontoast/build.xml ================================================ ================================================ FILE: 3rdlib/commontoast/proguard-project.txt ================================================ # To enable ProGuard in your project, edit project.properties # to define the proguard.config property as described in that file. # # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in ${sdk.dir}/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: 3rdlib/commontoast/project.properties ================================================ # This file is automatically generated by Android Tools. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file must be checked in Version Control Systems. # # To customize properties used by the Ant build system edit # "ant.properties", and override values to adapt the script to your # project structure. # # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. target=android-17 android.library=true ================================================ FILE: 3rdlib/commontoast/src/com/halzhang/android/common/CDToast.java ================================================ package com.halzhang.android.common; import android.content.Context; import android.os.Handler; import android.widget.Toast; /** * CommonToast
* Toast * * @author Hal * @version Dec 27, 2012 11:39:39 AM */ public class CDToast { private static Toast sToast; private static Handler mHandler; /** * @param context * @param text */ public static void showToast(Context context, String text) { if (sToast == null) { mHandler = new Handler(context.getMainLooper()); sToast = Toast.makeText(context.getApplicationContext(), text, Toast.LENGTH_SHORT); } else { sToast.setText(text); } mHandler.post(new Runnable() { @Override public void run() { sToast.show(); } }); } /** * @param context * @param resId */ public static void showToast(Context context, int resId) { String text = context.getString(resId); showToast(context, text); } } ================================================ FILE: README.md ================================================ # [Startup News](http://news.dbanotes.net) Android App [![Build Status](https://travis-ci.org/halzhang/StartupNews.svg?branch=master)](https://travis-ci.org/halzhang/StartupNews) [![Google Play](http://developer.android.com/images/brand/en_generic_rgb_wo_45.png)](https://play.google.com/store/apps/details?id=com.halzhang.android.apps.startupnews) ![image](https://raw.github.com/halzhang/StartupNews/master/QR.jpg) [本地下载](https://github.com/halzhang/StartupNews/raw/master/release/StartupNews-release.apk) 支持系统 --- Android 4.0+ 反馈 --- 邮箱:[ghanguo@gmail.com](mailto:ghanguo@gmail.com) 欢迎直接在[issues](https://github.com/halzhang/StartupNews/issues/new)提bug; 作者 --- [HalZhang](http://weibo.com/halzhang) 感谢 [@Fenng](http://www.weibo.com/fenng) 创建了Startup News. Changelog --- [changelog](https://github.com/halzhang/StartupNews/wiki/Changelog) License -------- Copyright 2016 Zhang Hanguo 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. ================================================ FILE: android-wait-for-emulator ================================================ #!/bin/bash # Originally written by Ralf Kistner , but placed in the public domain set +e bootanim="" failcounter=0 timeout_in_sec=360 until [[ "$bootanim" =~ "stopped" ]]; do bootanim=`adb -e shell getprop init.svc.bootanim 2>&1 &` if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" || "$bootanim" =~ "running" ]]; then let "failcounter += 1" echo "Waiting for emulator to start" if [[ $failcounter -gt timeout_in_sec ]]; then echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" exit 1 fi fi sleep 1 done echo "Emulator is ready" ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' android { def globalConfiguration = rootProject.extensions.getByName("ext") def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim() compileSdkVersion globalConfiguration.getAt("androidCompileSdkVersion") buildToolsVersion globalConfiguration.getAt("androidBuildToolsVersion") defaultConfig { minSdkVersion globalConfiguration.getAt("androidMinSdkVersion") targetSdkVersion globalConfiguration.getAt("androidTargetSdkVersion") applicationId "com.halzhang.android.apps.startupnews" versionCode 13 versionName "2.0.0-${gitSha}" } signingConfigs { release } File storeFile = file("../local.properties") if (storeFile.canRead()) { Properties p = new Properties() p.load(storeFile.newDataInputStream()) if (p != null && p.containsKey('key.store') && p.containsKey("key.store.password") && p.containsKey("key.alias") && p.containsKey("key.alias.password")) { android.signingConfigs.release.storeFile = file(p.getProperty("key.store")) android.signingConfigs.release.storePassword = p.getProperty("key.store.password") android.signingConfigs.release.keyAlias = p.getProperty("key.alias") android.signingConfigs.release.keyPassword = p.getProperty("key.alias.password") } else { android.buildTypes.release.signingConfig = null } } else { android.buildTypes.release.signingConfig = null } buildTypes { debug { debuggable true versionNameSuffix "-debug" applicationIdSuffix ".debug" resValue "string", "app_name", "SN(debug)" } release { applicationVariants.all { variant -> variant.outputs.each { output -> if (output.outputFile != null && output.outputFile.name.endsWith('.apk') && 'release'.equals(variant.buildType.name)) { def apkFile = new File( output.outputFile.getParent(), "StartupNews_${variant.flavorName}_v${variant.versionName}.apk") output.outputFile = apkFile } } } resValue "string", "app_name", "StartupNews" shrinkResources true debuggable false signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' } } productFlavors { googleplay {} } } dependencies { def androidSupportVersion = rootProject.ext.androidSupportVersion def mvpVersion = rootProject.ext.mvpVersion compile fileTree(dir: 'libs', include: '*.jar') compile "com.android.support:appcompat-v7:${androidSupportVersion}" compile "com.android.support:support-v4:${androidSupportVersion}" compile "com.android.support:support-annotations:${androidSupportVersion}" compile "com.android.support:recyclerview-v7:${androidSupportVersion}" compile "com.android.support:cardview-v7:${androidSupportVersion}" compile "com.android.support:design:${androidSupportVersion}" compile "com.android.support:customtabs:${androidSupportVersion}" compile "com.github.halzhang:mvp:${mvpVersion}" compile "com.github.halzhang:mvp-support-v4:${mvpVersion}" compile "com.github.halzhang:mvp-support-v7:${mvpVersion}" // Dagger dependencies apt "com.google.dagger:dagger-compiler:$rootProject.daggerVersion" provided 'org.glassfish:javax.annotation:10.0-b28' compile "com.google.dagger:dagger:$rootProject.daggerVersion" compile project(':3rdlib:commonlog') compile project(':3rdlib:commontoast') compile project(':data') releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3' debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3' compile 'org.jsoup:jsoup:1.7.2' compile 'com.android.support:support-v4:23.4.0' } ================================================ FILE: app/proguard-project.txt ================================================ # To enable ProGuard in your project, edit project.properties # to define the proguard.config property as described in that file. # # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in ${sdk.dir}/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -keep class org.jsoup.** { *; } -keep interface org.jsoup.** { *; } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ApplicationModule.java ================================================ package com.halzhang.android.apps.startupnews; import android.content.Context; import com.halzhang.android.startupnews.data.utils.OkHttpClientHelper; import dagger.Module; import dagger.Provides; /** * Created by Hal on 2016/5/30. */ @Module public class ApplicationModule { private final Context mContext; public ApplicationModule(Context context) { mContext = context; } @Provides Context provideContext() { return mContext; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/Constants.java ================================================ package com.halzhang.android.apps.startupnews; /** * Constants used by StartupNews application * * @author Hal * @version Jan 16, 2013 11:46:03 AM */ public abstract class Constants { /** * abs class */ protected Constants() { } /** * Root Dir used by SN app */ public static final String SDCARD_TOP_DIR = "/SN/"; /** * Crash log dir */ public static final String CRASH_LOG_DIR = SDCARD_TOP_DIR+"/crash/"; /** * 历史记录保存文件名 */ public static final String HISTORY_FILE_NAME = "history.db"; public final class IntentAction { /** * 登陆后结果cookie user * * @see #ACTION_LOGIN */ public static final String EXTRA_LOGIN_USER = "startupnews.intent.extra.login.USER"; /** * 登陆结果 *

* 输出:登陆后的cookie extras:LOGIN_USER(value:String) *

* * @see #EXTRA_LOGIN_USER */ public static final String ACTION_LOGIN = "startupnews.intent.action.LOGIN"; /** * 注销 */ public static final String ACTION_LOGOUT = "startupnews.intent.action.LOGOUT"; } public static final String TAG_BROWSE_FRAGMENT = "tag_browse_fragment"; } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/MyApplication.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews; import android.app.Application; import android.os.StrictMode; import android.text.TextUtils; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.utils.CrashHandler; import com.halzhang.android.common.CDLog; import com.halzhang.android.startupnews.data.CookieFactoryModule; import com.halzhang.android.startupnews.data.OkHttpClientModule; import com.halzhang.android.startupnews.data.SnApiModule; import com.squareup.leakcanary.LeakCanary; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.util.HashSet; import java.util.Iterator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /** * StartupNews *

* app *

* * @author Hal * @version Mar 8, 2013 */ public class MyApplication extends Application { private static final String LOG_TAG = MyApplication.class.getSimpleName(); private HashSet mHistorySet = new HashSet(); private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "StartupNews thread #" + mCount.getAndIncrement()); } }; private ExecutorService mExecutorService; private static MyApplication me; public static MyApplication instance() { return me; } public MyApplication() { super(); me = this; } private SnApiComponent mSnApiComponent; @Override public void onCreate() { super.onCreate(); Tracker.getInstance().init(this); CrashHandler.getInstance().init(this); mExecutorService = Executors.newSingleThreadExecutor(sThreadFactory); initHistory(); if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(new StrictMode .ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectActivityLeaks() .detectLeakedClosableObjects() .penaltyLog() .build()); LeakCanary.install(this); } mSnApiComponent = DaggerSnApiComponent.builder().applicationModule(new ApplicationModule(this)) .okHttpClientModule(new OkHttpClientModule()) .snApiModule(new SnApiModule()) .cookieFactoryModule(new CookieFactoryModule()).build(); mSnApiComponent.getSessionManager().initSession(this); } public SnApiComponent getSnApiComponent() { return mSnApiComponent; } private void initHistory() { File file = new File(getFilesDir().getAbsolutePath() + File.separator + Constants.HISTORY_FILE_NAME); if (!file.exists()) { return; } BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(file)); String s = null; while (!TextUtils.isEmpty((s = reader.readLine()))) { mHistorySet.add(s); } } catch (FileNotFoundException e) { CDLog.e(LOG_TAG, "", e); Tracker.getInstance().sendException("History file not found!", e, false); } catch (IOException e) { CDLog.e(LOG_TAG, "", e); Tracker.getInstance().sendException("Read History file error!", e, false); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { } } } } private void storeHistory() { if (!mHistorySet.isEmpty()) { Iterator iterator = mHistorySet.iterator(); StringBuilder builder = new StringBuilder(); while (iterator.hasNext()) { builder.append(iterator.next()).append("\n"); } File file = new File(getFilesDir().getAbsolutePath() + File.separator + Constants.HISTORY_FILE_NAME); if (!file.exists()) { try { file.createNewFile(); } catch (IOException e) { CDLog.e(LOG_TAG, "Create history file error!"); Tracker.getInstance().sendException("Create history file error!", e, false); } } PrintWriter writer = null; try { writer = new PrintWriter(file); writer.write(builder.toString()); writer.close(); } catch (FileNotFoundException e) { CDLog.e(LOG_TAG, "History file not found!"); Tracker.getInstance().sendException("History file not found!", e, false); } finally { if (writer != null) { writer.close(); } } } } /** * 增加访问历史记录 * * @param url */ public void addHistory(String url) { if (!TextUtils.isEmpty(url) && !mHistorySet.contains(url)) { mHistorySet.add(url); mExecutorService.submit(new Runnable() { @Override public void run() { storeHistory(); } }); } } /** * 清空历史记录 */ public void clearHistory() { mHistorySet.clear(); } public boolean isHistoryContains(String url) { return mHistorySet.contains(url); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/SnApiComponent.java ================================================ package com.halzhang.android.apps.startupnews; import com.halzhang.android.startupnews.data.CookieFactoryModule; import com.halzhang.android.startupnews.data.JsoupConnectorModule; import com.halzhang.android.startupnews.data.OkHttpClientModule; import com.halzhang.android.startupnews.data.SessionManagerModule; import com.halzhang.android.startupnews.data.net.ISnApi; import com.halzhang.android.startupnews.data.SnApiModule; import com.halzhang.android.startupnews.data.net.JsoupConnector; import com.halzhang.android.startupnews.data.utils.SessionManager; import javax.inject.Singleton; import dagger.Component; /** * Created by Hal on 2016/5/30. */ @Singleton @Component(modules = {ApplicationModule.class, SnApiModule.class, OkHttpClientModule.class, SessionManagerModule.class, JsoupConnectorModule.class, CookieFactoryModule.class}) public interface SnApiComponent { ISnApi getSnApi(); SessionManager getSessionManager(); } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/analytics/MyExceptionParser.java ================================================ /* * Copyright (C) 2013 HalZhang. * * http://www.gnu.org/licenses/gpl-3.0.txt */ package com.halzhang.android.apps.startupnews.analytics; import com.google.analytics.tracking.android.ExceptionParser; import com.halzhang.android.apps.startupnews.utils.AppUtils; import android.content.Context; import android.os.Build; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; /** * StartupNews *

* 异常解析 *

* * @author Hal * @version Mar 26, 2013 */ public class MyExceptionParser implements ExceptionParser { private Context mContext; private Map infos = new HashMap(); public MyExceptionParser(Context context) { mContext = context; } @Override public String getDescription(String message, Throwable throwable) { collectDeviceInfo(mContext); StringBuilder sb = new StringBuilder(); for (Map.Entry entry : infos.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); sb.append(key).append("=").append(value).append("\n"); } sb.append(getStackTraceString(throwable)); return sb.toString(); } public static String getStackTraceString(Throwable tr) { if (tr == null) { return ""; } Throwable t = tr; while (t != null) { if (t instanceof UnknownHostException) { return ""; } t = t.getCause(); } StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); return sw.toString(); } public void collectDeviceInfo(Context ctx) { infos.clear(); infos.put("versionName", AppUtils.getVersionName(mContext)); infos.put("versionCode", String.valueOf(AppUtils.getVersionCode(mContext))); Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); infos.put(field.getName(), field.get(null).toString()); } catch (Exception e) { } } } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/analytics/Tracker.java ================================================ package com.halzhang.android.apps.startupnews.analytics; import android.content.Context; import android.text.TextUtils; import android.util.Log; import com.google.analytics.tracking.android.EasyTracker; import com.google.analytics.tracking.android.MapBuilder; /** * 统计 * Created by Hal on 15/5/6. */ public class Tracker { private static final String LOG_TAG = "Tracker"; private EasyTracker mEasyTracker; private Tracker() { } private static class InstanceHolder { private static final Tracker INSTANCE = new Tracker(); } public static Tracker getInstance() { return InstanceHolder.INSTANCE; } public void init(Context context) { mEasyTracker = EasyTracker.getInstance(context); } public void sendException(String message, Throwable e, boolean fatal) { if(mEasyTracker == null){ Log.w(LOG_TAG,"Tracker has not init!"); return; } mEasyTracker.send(MapBuilder.createException(TextUtils.isEmpty(message) ? (e == null ? "" : e.getMessage()) : message, fatal).build()); } public void sendEvent(String category, String action, String label, Long value) { if(mEasyTracker == null){ Log.w(LOG_TAG,"Tracker has not init!"); return; } mEasyTracker.send(MapBuilder.createEvent(category, action, label, value).build()); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/BasePresenter.java ================================================ package com.halzhang.android.apps.startupnews.presenter; /** * Created by Hal on 16/6/11. */ public interface BasePresenter { void start(); void stop(); } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/BaseView.java ================================================ package com.halzhang.android.apps.startupnews.presenter; /** * Created by Hal on 16/6/11. */ public interface BaseView { void setPresenter(T presenter); boolean isActive(); } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/CommentsListContract.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.startupnews.data.entity.SNComment; import java.util.ArrayList; /** * Created by Hal on 16/8/14. */ public interface CommentsListContract { interface Presenter extends BasePresenter { void getComments(String url); void getMoreComments(); } interface View extends BaseView { void onSuccess(ArrayList snComments); void onFailure(Throwable e); void onAtEnd(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/CommentsListPresenter.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import android.support.annotation.NonNull; import android.text.TextUtils; import com.halzhang.android.startupnews.data.entity.SNComments; import com.halzhang.android.startupnews.data.net.ISnApi; import javax.inject.Inject; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * Created by Hal on 16/8/14. */ public class CommentsListPresenter implements CommentsListContract.Presenter { @NonNull private final ISnApi mSnApi; @NonNull private final CommentsListContract.View mView; private SNComments mComments; @Inject public CommentsListPresenter(@NonNull ISnApi snApi, @NonNull CommentsListContract.View view) { mSnApi = snApi; mView = view; } @Inject void setupListener() { mView.setPresenter(this); } @Override public void getComments(String url) { mSnApi.getSNComments(url) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onFailure(e); } } @Override public void onNext(SNComments snComments) { if (snComments != null) { mComments = snComments; if (mView.isActive()) { mView.onSuccess(mComments.getSnComments()); } } } }); } @Override public void getMoreComments() { if (mComments == null || TextUtils.isEmpty(mComments.getMoreURL())) { if (mView.isActive()) { mView.onAtEnd(); } } else { mSnApi.getSNComments(mComments.getMoreURL()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onFailure(e); } } @Override public void onNext(SNComments snComments) { if (snComments != null) { if (mComments == null) { mComments = snComments; } else { mComments.addComments(snComments.getSnComments()); mComments.setMoreURL(snComments.getMoreURL()); } if (mView.isActive()) { mView.onSuccess(mComments.getSnComments()); } } } }); } } /* no-op */ @Override public void start() { } @Override public void stop() { } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/CommentsListPresenterModule.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import dagger.Module; import dagger.Provides; /** * Module for Comments provide Comments view * Created by Hal on 16/6/12. */ @Module public class CommentsListPresenterModule { private final CommentsListContract.View mView; public CommentsListPresenterModule(CommentsListContract.View view) { mView = view; } @Provides CommentsListContract.View provideCommentsListContractView() { return mView; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/DiscussComponent.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.ui.DiscussActivity; import com.halzhang.android.apps.startupnews.utils.ActivityScoped; import dagger.Component; /** * Created by Hal on 16/8/14. */ @ActivityScoped @Component(dependencies = SnApiComponent.class, modules = DiscussPresenterModule.class) public interface DiscussComponent { void inject(DiscussActivity discussActivity); } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/DiscussContract.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.startupnews.data.entity.SNDiscuss; import com.halzhang.android.startupnews.data.entity.Status; /** * Created by Hal on 16/8/14. */ public interface DiscussContract { interface Presenter extends BasePresenter { void getDiscuss(String url); void comment(String message); } interface View extends BaseView { void onGetDiscuss(SNDiscuss snDiscuss); void onGetDiscussFailure(Throwable e); void onCommentSuccess(Status status); void onCommentFailure(Throwable e); void onSessionExpired(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/DiscussPresenter.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import android.support.annotation.NonNull; import com.halzhang.android.startupnews.data.entity.SNDiscuss; import com.halzhang.android.startupnews.data.entity.Status; import com.halzhang.android.startupnews.data.net.ISnApi; import com.halzhang.android.startupnews.data.utils.SessionManager; import javax.inject.Inject; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * Created by Hal on 16/8/14. */ public class DiscussPresenter implements DiscussContract.Presenter { @NonNull private final DiscussContract.View mView; @NonNull private final ISnApi mSnApi; @NonNull private final SessionManager mSessionManager; private String mFnid; @Inject public DiscussPresenter(DiscussContract.View view, ISnApi snApi, SessionManager sessionManager) { mView = view; mSnApi = snApi; mSessionManager = sessionManager; } @Inject void setupListener() { mView.setPresenter(this); } @Override public void start() { } @Override public void stop() { } @Override public void getDiscuss(String url) { mSnApi.getDiscuss(url) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onGetDiscussFailure(e); } } @Override public void onNext(SNDiscuss snDiscuss) { if (mView.isActive()) { if (snDiscuss != null) { mFnid = snDiscuss.getFnid(); } mView.onGetDiscuss(snDiscuss); } } }); } @Override public void comment(String message) { if (!mSessionManager.isValid()) { if (mView.isActive()) { mView.onSessionExpired(); } return; } mSnApi.comment(message, mFnid) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onCommentFailure(e); } } @Override public void onNext(Status status) { if (mView.isActive()) { mView.onCommentSuccess(status); } } }); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/DiscussPresenterModule.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import dagger.Module; import dagger.Provides; /** * Created by Hal on 16/8/14. */ @Module public class DiscussPresenterModule { private DiscussContract.View mView; public DiscussPresenterModule(DiscussContract.View view) { mView = view; } @Provides public DiscussContract.View provideDiscussContractView() { return mView; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/LoginComponent.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.ui.LoginActivity; import com.halzhang.android.apps.startupnews.utils.ActivityScoped; import com.halzhang.android.apps.startupnews.utils.FragmentScoped; import dagger.Component; /** * Dagger component for login * Created by Hal on 16/6/12. */ @ActivityScoped @Component(dependencies = SnApiComponent.class, modules = LoginPresenterModule.class) public interface LoginComponent { void inject(LoginActivity loginActivity); } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/LoginContract.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import rx.Subscription; /** * Contract for login presenter and view * Created by Hal on 16/6/12. */ public interface LoginContract { interface Presenter extends BasePresenter { void login(String username, String password); } interface View extends BaseView { void onLoginError(Throwable e); void onLoginResult(String user); void addSubscription(Subscription subscription); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/LoginPresenter.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import android.support.annotation.NonNull; import com.halzhang.android.startupnews.data.net.ISnApi; import javax.inject.Inject; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Func1; import rx.schedulers.Schedulers; /** * Presenter for Login * Created by Hal on 16/6/12. */ public class LoginPresenter implements LoginContract.Presenter { @NonNull private final LoginContract.View mView; @NonNull private final ISnApi mSnApi; @Inject public LoginPresenter(@NonNull LoginContract.View view, @NonNull ISnApi snApi) { mView = view; mSnApi = snApi; } @Inject void setupListener() { mView.setPresenter(this); } @Override public void login(final String username, final String password) { mView.addSubscription( mSnApi.getFnid() .flatMap(new Func1>() { @Override public Observable call(String s) { return mSnApi.login(s, username, password); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onLoginError(e); } } @Override public void onNext(String s) { if (mView.isActive()) { mView.onLoginResult(s); } } }) ); } /* no-op */ @Override public void start() { } @Override public void stop() { } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/LoginPresenterModule.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import dagger.Module; import dagger.Provides; /** * Module for login provide login view * Created by Hal on 16/6/12. */ @Module public class LoginPresenterModule { private final LoginContract.View mView; public LoginPresenterModule(LoginContract.View view) { mView = view; } @Provides LoginContract.View provideLoginContractView() { return mView; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/MainActivityContract.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.startupnews.data.entity.Status; /** * Created by Hal on 16/8/23. */ public interface MainActivityContract { interface Presenter extends BasePresenter { void logout(); /** * 投票,顶!d=====( ̄▽ ̄*)b * * @param postId */ void upVote(String postId); } interface View extends BaseView { /** * 注销登录 * * @param result true 成功 */ void onLogoutResult(boolean result); void onUpVoteFailure(Throwable e); void onUpVoteSuccess(Status status); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/MainActivityPresenter.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import android.support.annotation.NonNull; import com.halzhang.android.startupnews.data.entity.Status; import com.halzhang.android.startupnews.data.net.ISnApi; import javax.inject.Inject; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * Created by Hal on 16/8/23. */ public class MainActivityPresenter implements MainActivityContract.Presenter { @NonNull private final ISnApi mSnApi; @NonNull private final MainActivityContract.View mView; @Inject public MainActivityPresenter(ISnApi snApi, MainActivityContract.View view) { mSnApi = snApi; mView = view; } @Inject void setupListener() { mView.setPresenter(this); } @Override public void logout() { mSnApi.logout().subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(Boolean aBoolean) { if (mView.isActive()) { mView.onLogoutResult(aBoolean); } } }); } @Override public void upVote(String postId) { mSnApi.upVote(postId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onUpVoteFailure(e); } } @Override public void onNext(Status status) { if (mView.isActive()) { mView.onUpVoteSuccess(status); } } }); } @Override public void start() { } @Override public void stop() { } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/MainActivityPresenterModule.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import dagger.Module; import dagger.Provides; /** * Created by Hal on 16/8/23. */ @Module public class MainActivityPresenterModule { private MainActivityContract.View mView; public MainActivityPresenterModule(MainActivityContract.View view) { mView = view; } @Provides public MainActivityContract.View provideMainActivityContractView() { return mView; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/MainComponent.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.ui.MainActivity; import com.halzhang.android.apps.startupnews.utils.ActivityScoped; import dagger.Component; /** * 主页 * Created by Hal on 16/8/14. */ @ActivityScoped @Component(dependencies = SnApiComponent.class, modules = {CommentsListPresenterModule.class, MainActivityPresenterModule.class}) public interface MainComponent { void inject(MainActivity mainActivity); } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/NewsListContract.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.startupnews.data.entity.SNNew; import java.util.ArrayList; /** * Created by Hal on 16/8/14. */ public interface NewsListContract { interface Presenter extends BasePresenter { void getFeed(String url); void getMoreFeed(); } interface View extends BaseView { void onSuccess(ArrayList snNews); void onFailure(Throwable e); void onAtEnd(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/NewsListFragmentComponent.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.ui.fragment.NewsListFragment; import com.halzhang.android.apps.startupnews.utils.FragmentScoped; import dagger.Component; /** * Created by Hal on 16/8/14. */ @FragmentScoped @Component(dependencies = SnApiComponent.class, modules = NewsListPresenterModule.class) public interface NewsListFragmentComponent { void inject(NewsListFragment newsListFragment); } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/NewsListPresenter.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import android.support.annotation.NonNull; import android.text.TextUtils; import com.halzhang.android.startupnews.data.entity.SNFeed; import com.halzhang.android.startupnews.data.net.ISnApi; import javax.inject.Inject; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * 最新文章列表 * Created by Hal on 2016/5/12. */ public class NewsListPresenter implements NewsListContract.Presenter { @NonNull private final ISnApi mSnApi; @NonNull private final NewsListContract.View mView; private SNFeed mSNFeed; @Inject public NewsListPresenter(ISnApi snApi, NewsListContract.View view) { mSnApi = snApi; mView = view; } @Inject void setupListener() { mView.setPresenter(this); } @Override public void start() { } @Override public void stop() { } @Override public void getFeed(String url) { mSnApi.getSNFeed(url) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onFailure(e); } } @Override public void onNext(SNFeed snFeed) { if (snFeed != null) { mSNFeed = snFeed; if (mView.isActive()) { mView.onSuccess(mSNFeed.getSnNews()); } } } }); } @Override public void getMoreFeed() { if (mSNFeed != null && !TextUtils.isEmpty(mSNFeed.getMoreUrl())) { mSnApi.getSNFeed(mSNFeed.getMoreUrl()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mView.isActive()) { mView.onFailure(e); } } @Override public void onNext(SNFeed snFeed) { if (snFeed != null) { if (mSNFeed == null) { mSNFeed = snFeed; } else { mSNFeed.setMoreUrl(snFeed.getMoreUrl()); mSNFeed.addNews(snFeed.getSnNews()); } if (mView.isActive()) { mView.onSuccess(mSNFeed.getSnNews()); } } } }); } else { if (mView.isActive()) { mView.onAtEnd(); } } } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/presenter/NewsListPresenterModule.java ================================================ package com.halzhang.android.apps.startupnews.presenter; import dagger.Module; import dagger.Provides; /** * Module for News List provide newt view * Created by Hal on 16/6/12. */ @Module public class NewsListPresenterModule { private final NewsListContract.View mView; public NewsListPresenterModule(NewsListContract.View view) { mView = view; } @Provides NewsListContract.View provideNewsListContractView() { return mView; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/AboutActivity.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui; import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceFragment; import android.support.v7.app.ActionBar; import android.view.MenuItem; import com.google.analytics.tracking.android.EasyTracker; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.utils.AppUtils; /** * StartupNews *

* 设置 *

* * @author Hal * @version Mar 8, 2013 */ public class AboutActivity extends BaseFragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } getFragmentManager().beginTransaction().replace(android.R.id.content, new AppPreferenceFragment()).commit(); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent(); // Toolbar bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.preference_toolbar, root, false); // root.addView(bar, 0); // insert at top // bar.setTitleTextColor(getResources().getColor(android.R.color.white)); // bar.setNavigationOnClickListener(new View.OnClickListener() { // @Override // public void onClick(View v) { // finish(); // } // }); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); break; default: break; } return super.onOptionsItemSelected(item); } public static class AppPreferenceFragment extends PreferenceFragment implements OnPreferenceChangeListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); ListPreference listPreference = (ListPreference) findPreference(getString(R.string.pref_key_html_provider)); listPreference.setOnPreferenceChangeListener(this); listPreference.setSummary(listPreference.getEntry()); (findPreference(getString(R.string.pref_key_default_browse))) .setOnPreferenceChangeListener(this); Preference versionPref = findPreference(getString(R.string.pref_key_version)); versionPref.setSummary(getString(R.string.pref_summary_version, AppUtils.getVersionName(getActivity()))); } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { final String key = preference.getKey(); if (getString(R.string.pref_key_html_provider).equals(key)) { Tracker.getInstance().sendEvent("preference_change_action", "preference_change_html_provider", String.format("html_provider_%1$s", (String) newValue), 0L); ListPreference listPreference = (ListPreference) preference; preference.setSummary(listPreference.getEntries()[listPreference .findIndexOfValue((String) newValue)]); } else if (getString(R.string.pref_key_default_browse).equals(key)) { Tracker.getInstance().sendEvent("preference_change_action", "preference_change_default_browse", String.format("default_browse_%1$s", String.valueOf(newValue)), 0L); } return true; } } @Override protected void onStart() { super.onStart(); EasyTracker.getInstance(this).activityStart(this); } @Override protected void onStop() { super.onStop(); EasyTracker.getInstance(this).activityStop(this); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/BaseActivity.java ================================================ package com.halzhang.android.apps.startupnews.ui; import android.support.v7.app.AppCompatActivity; /** * Created by Hal on 16/6/18. */ public class BaseActivity extends AppCompatActivity { } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/BaseFragmentActivity.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui; import com.google.analytics.tracking.android.EasyTracker; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.mvp.presenter.Presenter; import com.halzhang.android.mvp.support.v7.MVPAppCompatActivity; import android.os.Build; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.Window; import android.view.WindowManager; /** * StartupNews *

*

* * @author Hal * @version Mar 13, 2013 */ public class BaseFragmentActivity extends AppCompatActivity { @Override protected void onCreate(Bundle arg0) { super.onCreate(arg0); // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ // Window w = getWindow(); // w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); // w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); // } } @Override protected void onStart() { super.onStart(); EasyTracker.getInstance(this).activityStart(this); } @Override protected void onStop() { super.onStop(); EasyTracker.getInstance(this).activityStop(this); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/DiscussActivity.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui; import com.halzhang.android.apps.startupnews.Constants.IntentAction; import com.halzhang.android.apps.startupnews.MyApplication; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.presenter.DaggerDiscussComponent; import com.halzhang.android.apps.startupnews.presenter.DiscussPresenter; import com.halzhang.android.apps.startupnews.presenter.DiscussPresenterModule; import com.halzhang.android.apps.startupnews.ui.tablet.DiscussFragment; import com.halzhang.android.startupnews.data.entity.SNNew; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import javax.inject.Inject; /** * StartupNews *

* 评论界面 *

* * @author Hal * @version Mar 17, 2013 */ public class DiscussActivity extends BaseFragmentActivity { //private static final String LOG_TAG = DiscussActivity.class.getSimpleName(); public static final String ARG_DISCUSS_URL = "discuss_url";//required! public static final String ARG_SNNEW = "snnew";//Optional public static void start(Context context, String discussUrl, SNNew snNew) { Intent starter = new Intent(context, DiscussActivity.class); starter.putExtra(ARG_DISCUSS_URL, discussUrl); starter.putExtra(ARG_SNNEW, snNew); context.startActivity(starter); } @Inject DiscussPresenter mDiscussPresenter; private DiscussFragment mDiscussFragment; private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(android.content.Context context, Intent intent) { final String action = intent.getAction(); if (IntentAction.ACTION_LOGIN.equals(action)) { String user = intent.getStringExtra(IntentAction.EXTRA_LOGIN_USER); if (!TextUtils.isEmpty(user)) { mDiscussFragment.loadData(); } } } ; }; @Override protected void onCreate(Bundle arg0) { super.onCreate(arg0); setContentView(R.layout.activity_discuss); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } SNNew snNew = (SNNew) getIntent().getSerializableExtra(ARG_SNNEW); String mDiscussURL = getIntent().getStringExtra(ARG_DISCUSS_URL); if (TextUtils.isEmpty(mDiscussURL)) { finish(); } mDiscussFragment = DiscussFragment.newInstance(mDiscussURL,snNew); getSupportFragmentManager().beginTransaction() .add(R.id.fragment_container, mDiscussFragment).commitAllowingStateLoss(); IntentFilter filter = new IntentFilter(IntentAction.ACTION_LOGIN); registerReceiver(mReceiver, filter); SnApiComponent snApiComponent = ((MyApplication) getApplication()).getSnApiComponent(); DaggerDiscussComponent.builder().snApiComponent(snApiComponent) .discussPresenterModule(new DiscussPresenterModule(mDiscussFragment)).build().inject(this); } @Override protected void onStart() { super.onStart(); } @Override protected void onResume() { super.onResume(); invalidateOptionsMenu(); } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(mReceiver); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); return true; default: return super.onOptionsItemSelected(item); } } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/LauncherActivity.java ================================================ package com.halzhang.android.apps.startupnews.ui; /** * Created by Hal on 15/2/7. */ public class LauncherActivity extends BaseFragmentActivity { } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/LoginActivity.java ================================================ /* * Copyright (C) 2013 HalZhang. * * http://www.gnu.org/licenses/gpl-3.0.txt */ package com.halzhang.android.apps.startupnews.ui; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.view.MenuItem; import com.google.analytics.tracking.android.EasyTracker; import com.google.analytics.tracking.android.MapBuilder; import com.halzhang.android.apps.startupnews.MyApplication; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.presenter.DaggerLoginComponent; import com.halzhang.android.apps.startupnews.presenter.LoginPresenter; import com.halzhang.android.apps.startupnews.presenter.LoginPresenterModule; import com.halzhang.android.apps.startupnews.ui.fragment.LoginFragment; import javax.inject.Inject; /** * StartupNews *

* 登陆 *

* * @author Hal * @version Apr 20, 2013 */ public class LoginActivity extends BaseActivity { private static final String LOG_TAG = LoginActivity.class.getSimpleName(); @Inject LoginPresenter mPresenter; private LoginFragment mLoginFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } setContentView(R.layout.activity_login); mLoginFragment = LoginFragment.newInstance(); SnApiComponent snApiComponent = ((MyApplication) getApplication()).getSnApiComponent(); getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, mLoginFragment).commit(); DaggerLoginComponent.builder().loginPresenterModule(new LoginPresenterModule(mLoginFragment)) .snApiComponent(snApiComponent).build().inject(this); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); return true; case R.id.menu_login: EasyTracker.getInstance(this).send(MapBuilder.createEvent("ui_action", "options_item_selected", "loginactivity_menu_login", 0L).build()); mLoginFragment.attemptLogin(); return true; default: break; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/MainActivity.java ================================================ package com.halzhang.android.apps.startupnews.ui; import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; import android.support.customtabs.CustomTabsIntent; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; import com.halzhang.android.apps.startupnews.MyApplication; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.presenter.CommentsListPresenter; import com.halzhang.android.apps.startupnews.presenter.CommentsListPresenterModule; import com.halzhang.android.apps.startupnews.presenter.DaggerMainComponent; import com.halzhang.android.apps.startupnews.presenter.MainActivityContract; import com.halzhang.android.apps.startupnews.presenter.MainActivityPresenter; import com.halzhang.android.apps.startupnews.presenter.MainActivityPresenterModule; import com.halzhang.android.apps.startupnews.ui.fragment.CommentsListFragment; import com.halzhang.android.apps.startupnews.ui.fragment.NewsListFragment; import com.halzhang.android.apps.startupnews.ui.fragment.NewsListFragment.OnNewsSelectedListener; import com.halzhang.android.apps.startupnews.ui.phone.BrowseActivity; import com.halzhang.android.apps.startupnews.ui.tablet.BrowseFragment; import com.halzhang.android.apps.startupnews.ui.tablet.DiscussFragment; import com.halzhang.android.apps.startupnews.ui.tablet.DiscussFragment.OnMenuSelectedListener; import com.halzhang.android.apps.startupnews.utils.ActivityUtils; import com.halzhang.android.apps.startupnews.utils.AppUtils; import com.halzhang.android.apps.startupnews.utils.CustomTabsActivityHelper; import com.halzhang.android.common.CDLog; import com.halzhang.android.common.CDToast; import com.halzhang.android.startupnews.data.entity.SNNew; import com.halzhang.android.startupnews.data.entity.Status; import com.halzhang.android.startupnews.data.utils.SessionManager; import javax.inject.Inject; /** * 主页 * * @author Hal */ public class MainActivity extends BaseFragmentActivity implements OnNewsSelectedListener, OnMenuSelectedListener, MainActivityContract.View { private static final String LOG_TAG = MainActivity.class.getSimpleName(); private static final String TAG_NEWS = "tag_news"; private static final String TAG_NEWEST = "tag_newest"; private static final String TAG_COMMENT = "tag_comment"; private static final String TAG_BROWSE = "tag_browse"; private static final String TAG_DISCUSS = "tag_discuss"; private Intent mFeedbackEmailIntent; private SNNew mSnNew; private CustomTabsActivityHelper mHelper; private NewsListFragment mHotNewsListFragment; private NewsListFragment mNewsListFragment; private CommentsListFragment mCommentsListFragment; @Inject CommentsListPresenter mCommentsListPresenter; @Inject MainActivityPresenter mMainActivityPresenter; @Inject SessionManager mSessionManager; @Override protected void onCreate(Bundle savedInstanceState) { CDLog.i(LOG_TAG, "MainActivity create!"); super.onCreate(savedInstanceState); if (isFinishing()) { return; } setContentView(R.layout.activity_main); setupViews(); mFeedbackEmailIntent = createEmailIntent(); mHelper = new CustomTabsActivityHelper(); SnApiComponent snApiComponent = ((MyApplication) getApplication()).getSnApiComponent(); DaggerMainComponent.builder().snApiComponent(snApiComponent) .commentsListPresenterModule(new CommentsListPresenterModule(mCommentsListFragment)) .mainActivityPresenterModule(new MainActivityPresenterModule(this)) .build().inject(this); } @Override protected void onStart() { super.onStart(); mHelper.bindService(this); } @Override protected void onStop() { super.onStop(); mHelper.unBindService(this); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); } private void setupViews() { mHotNewsListFragment = NewsListFragment.newInstance(getString(R.string.host, "/news")); mNewsListFragment = NewsListFragment.newInstance(getString(R.string.host, "/newest")); mCommentsListFragment = CommentsListFragment.newInstance(); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); final ActionBar ab = getSupportActionBar(); if (ab != null) { ab.setDisplayHomeAsUpEnabled(false); } ViewPager viewPager = (ViewPager) findViewById(R.id.pager); if (viewPager != null) { SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); viewPager.setAdapter(sectionsPagerAdapter); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.setupWithViewPager(viewPager); } } private Intent createEmailIntent() { Intent emailIntent = new Intent(Intent.ACTION_SEND); emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{ "ghanguo@gmail.com" }); StringBuilder builder = new StringBuilder(); builder.append(getString(R.string.app_name)).append(" v") .append(AppUtils.getVersionName(getApplicationContext())) .append(getString(R.string.feedback)); emailIntent.putExtra(Intent.EXTRA_SUBJECT, builder.toString()); emailIntent.setType("message/rfc822"); return emailIntent; } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_main, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.removeItem(R.id.menu_login); menu.removeItem(R.id.menu_logout); if (mSessionManager.isValid()) { menu.add(Menu.NONE, R.id.menu_logout, Menu.NONE, R.string.menu_logout); } else { menu.add(Menu.NONE, R.id.menu_login, Menu.NONE, R.string.menu_login); } return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_settings: Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "mainactivity_menu_settings", 0L); startActivity(new Intent(this, AboutActivity.class)); return true; case R.id.menu_feedback: Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "mainactivity_menu_feedback", 0L); if (ActivityUtils.isIntentAvailable(getApplicationContext(), mFeedbackEmailIntent)) { startActivity(mFeedbackEmailIntent); } else { Toast.makeText(getApplicationContext(), R.string.error_noemailapp, Toast.LENGTH_LONG).show(); } return true; case R.id.menu_login: { Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "mainactivity_menu_login", 0L); Intent intent = new Intent(this, LoginActivity.class); startActivity(intent); } return true; case R.id.menu_logout: Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "mainactivity_menu_logout", 0L); mSessionManager.clear(); CDToast.showToast(this, R.string.tip_logout_success); mMainActivityPresenter.logout(); return true; case R.id.menu_show_comment: showDiscussFragment(); return true; default: return super.onOptionsItemSelected(item); } } private void showDiscussFragment() { showDiscussFragment(mSnNew, null); } private void showDiscussFragment(SNNew snNew, String discussUrl) { Bundle args = new Bundle(); if (snNew != null) { args.putParcelable(DiscussActivity.ARG_SNNEW, snNew); args.putString(DiscussActivity.ARG_DISCUSS_URL, mSnNew.getDiscussURL()); } else { args.putString(DiscussActivity.ARG_DISCUSS_URL, discussUrl); } DiscussFragment fragment = new DiscussFragment(); fragment.setArguments(args); getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) .replace(R.id.fragment_container, fragment, TAG_DISCUSS).commit(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } @Override public void onLogoutResult(boolean result) { CDToast.showToast(getApplicationContext(), result ? R.string.tip_logout_success : R.string.tip_logout_failure); } @Override public void onUpVoteFailure(Throwable e) { Tracker.getInstance().sendException("up vote error!", e, false); CDToast.showToast(this, getString(R.string.tip_vote_failure)); } @Override public void onUpVoteSuccess(Status status) { switch (status.code) { case Status.CODE_COOKIE_VALID: startActivity(new Intent(this, LoginActivity.class)); CDToast.showToast(this, R.string.tip_cookie_invalid); break; case Status.CODE_REPEAT: CDToast.showToast(this, getString(R.string.tip_vote_duplicate)); break; case Status.CODE_SUCCESS: CDToast.showToast(this, R.string.tip_vote_success); break; default: break; } } /* no-op */ @Override public void setPresenter(MainActivityContract.Presenter presenter) { } @Override public boolean isActive() { return !isFinishing(); } private class SectionsPagerAdapter extends FragmentPagerAdapter { private String[] titles; public SectionsPagerAdapter(FragmentManager fm) { super(fm); titles = getResources().getStringArray(R.array.section_titles); } @Override public Fragment getItem(int arg0) { switch (arg0) { case 0: return mHotNewsListFragment; case 1: return mNewsListFragment; case 2: return mCommentsListFragment; default: throw new IllegalArgumentException(); } } @Override public int getCount() { return titles.length; } @Override public CharSequence getPageTitle(int position) { return titles[position]; } @Override public long getItemId(int position) { return super.getItemId(position); } } @Override public void onNewsSelected(int position, final SNNew snNew) { mSnNew = snNew; CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(mHelper.getSession()); //设置 toolbar 颜色 builder.setToolbarColor(0XFF33B5E5); CustomTabsIntent customTabsIntent = builder.build(); mHelper.launchUrl(this, snNew.getUrl(), customTabsIntent, new CustomTabsActivityHelper.OnCustomTabsInvalidListener() { @Override public void onInvalid(String url) { ActivityUtils.openArticle(MainActivity.this, snNew); } }); } private void showBrowseFragment(SNNew snNew) { BrowseFragment browseFragment = (BrowseFragment) getSupportFragmentManager() .findFragmentByTag(TAG_BROWSE); if (browseFragment != null) { browseFragment.setTitle(snNew.getTitle()); browseFragment.load(snNew.getUrl()); } else { BrowseFragment fragment = new BrowseFragment(); Bundle bundle = new Bundle(); bundle.putString(BrowseActivity.EXTRA_URL, snNew.getUrl()); bundle.putString(BrowseActivity.EXTRA_TITLE, snNew.getTitle()); fragment.setArguments(bundle); getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right) .replace(R.id.fragment_container, fragment, TAG_BROWSE).commit(); } } @Override public void onShowArticleSelected(SNNew snNew) { mSnNew = snNew; showBrowseFragment(snNew); } @Override public void onUpVoteSelected(String postId) { mMainActivityPresenter.upVote(postId); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/ShareHelper.java ================================================ /* * Copyright (C) 2013 HalZhang. * * http://www.gnu.org/licenses/gpl-3.0.txt */ package com.halzhang.android.apps.startupnews.ui; /** * StartupNews *

*

* @author Hal * @version Jul 7, 2013 */ public class ShareHelper { //TODO implements } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/SubmitActivity.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui; import android.support.v4.app.FragmentActivity; /** * StartupNews *

*

* @author Hal * @version Mar 11, 2013 */ public class SubmitActivity extends FragmentActivity { } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/fragment/BaseFragment.java ================================================ package com.halzhang.android.apps.startupnews.ui.fragment; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import rx.subscriptions.CompositeSubscription; /** * Created by Hal on 16/6/18. */ public class BaseFragment extends Fragment { protected CompositeSubscription mCompositeSubscription; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCompositeSubscription = new CompositeSubscription(); } @Override public void onDestroyView() { mCompositeSubscription.clear(); super.onDestroyView(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/fragment/CommentsListFragment.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui.fragment; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.presenter.CommentsListContract; import com.halzhang.android.apps.startupnews.ui.DiscussActivity; import com.halzhang.android.startupnews.data.entity.SNComment; import com.halzhang.android.startupnews.data.entity.SNComments; import java.util.ArrayList; /** * StartupNews *

* 评论 *

* * @author Hal * @version Mar 7, 2013 */ public class CommentsListFragment extends SwipeRefreshRecyclerFragment implements CommentsListContract.View { private static final String LOG_TAG = CommentsListFragment.class.getSimpleName(); // private ArrayList mComments = new ArrayList(24); private CommentsListContract.Presenter mPresenter; private CommentsAdapter mAdapter; private SNComments mSnComments = new SNComments(); private static final String NEWCOMMENTS_URL_PATH = "/newcomments"; public CommentsListFragment() { } public static CommentsListFragment newInstance() { return new CommentsListFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAdapter = new CommentsAdapter(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mRecyclerView.setAdapter(mAdapter); if (mAdapter.isEmpty()) { mPresenter.getComments(getString(R.string.host, NEWCOMMENTS_URL_PATH)); } } @Override public void onDestroyView() { super.onDestroyView(); } @Override public void onDestroy() { super.onDestroy(); } @Override protected void onRefreshData() { super.onRefreshData(); Tracker.getInstance().sendEvent("ui_action", "pull_down_list_view_refresh", "comments_list_fragment_pull_down_list_view_refresh", 0L); mPresenter.getComments(getString(R.string.host, NEWCOMMENTS_URL_PATH)); } @Override protected void onLoadMore() { super.onLoadMore(); Tracker.getInstance().sendEvent("ui_action", "pull_up_list_view_refresh", "comments_list_fragment_pull_up_list_view_refresh", 0L); mPresenter.getMoreComments(); } @Override public void onSuccess(ArrayList snComments) { onRefreshComplete(); mSnComments.setSnComments(snComments); mAdapter.notifyDataSetChanged(); } @Override public void onFailure(Throwable e) { Log.e(LOG_TAG, "", e); onRefreshComplete(); Tracker.getInstance().sendException("CommentsTask", e, false); Toast.makeText(getActivity(), R.string.error, Toast.LENGTH_LONG).show(); } @Override public void onAtEnd() { onRefreshComplete(); Toast.makeText(getActivity(), R.string.tip_last_page, Toast.LENGTH_SHORT).show(); } @Override public void setPresenter(CommentsListContract.Presenter presenter) { mPresenter = presenter; } @Override public boolean isActive() { return isAdded(); } private class CommentsAdapter extends RecyclerView.Adapter { public final class ViewHolder extends RecyclerView.ViewHolder { public final TextView mUserId; public final TextView mCreated; public final TextView mCommentText; public final TextView mArtistTitle; public final View mItemView; public ViewHolder(View itemView) { super(itemView); mItemView = itemView; mUserId = (TextView) itemView.findViewById(R.id.comment_item_user_id); mCreated = (TextView) itemView.findViewById(R.id.comment_item_created); mCommentText = (TextView) itemView.findViewById(R.id.comment_item_text); mArtistTitle = (TextView) itemView.findViewById(R.id.comment_item_artist_titile); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(getActivity()).inflate(R.layout.comment_list_item, null)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { SNComment comment = mSnComments.getSnComments().get(position); holder.mUserId.setText(comment.getUser().getId()); holder.mCreated.setText(comment.getCreated()); holder.mCommentText.setText(comment.getText()); holder.mArtistTitle.setText(getString(R.string.comment_artist_title, comment.getArtistTitle())); holder.mItemView.setTag(position); holder.mItemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Tracker.getInstance().sendEvent("ui_action", "list_item_click", "comments_list_fragment_list_item_click", 0L); int position = (int) v.getTag(); SNComment comment = mSnComments.getSnComments().get(position); Intent intent = new Intent(getActivity(), DiscussActivity.class); intent.putExtra(DiscussActivity.ARG_DISCUSS_URL, comment.getDiscussURL()); startActivity(intent); } }); } @Override public long getItemId(int position) { return position; } @Override public int getItemCount() { return mSnComments.size(); } public boolean isEmpty() { return getItemCount() == 0; } } @Override public int getViewLayout() { return R.layout.refresh_recycler_view_layout; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/fragment/LoginFragment.java ================================================ package com.halzhang.android.apps.startupnews.ui.fragment; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.text.TextUtils; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import com.halzhang.android.apps.startupnews.Constants; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.presenter.LoginContract; import com.halzhang.android.common.CDToast; import rx.Subscription; /** * A simple {@link Fragment} subclass. * Use the {@link LoginFragment#newInstance} factory method to * create an instance of this fragment. */ public class LoginFragment extends BaseFragment implements LoginContract.View { // UI references. private EditText mUsernameView; private EditText mPasswordView; private View mLoginFormView; private View mLoginStatusView; private TextView mLoginStatusMessageView; private LoginContract.Presenter mPresenter; public LoginFragment() { } public static LoginFragment newInstance() { return new LoginFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public void onAttach(Context activity) { super.onAttach(activity); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_login, null); mUsernameView = (EditText) view.findViewById(R.id.username); mPasswordView = (EditText) view.findViewById(R.id.password); mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { if (id == R.id.login || id == EditorInfo.IME_NULL) { attemptLogin(); return true; } return false; } }); mLoginFormView = view.findViewById(R.id.login_form); mLoginStatusView = view.findViewById(R.id.login_status); mLoginStatusMessageView = (TextView) view.findViewById(R.id.login_status_message); mLoginStatusMessageView.setText(R.string.login_progress_init); showProgress(false); Button loginBtn = (Button) view.findViewById(R.id.btn_login); loginBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { attemptLogin(); } }); return view; } /** * Attempts to sign in or register the account specified by the login * form. If there are form errors (invalid email, missing fields, etc.), * the errors are presented and no actual login attempt is made. */ public void attemptLogin() { // Reset errors. mUsernameView.setError(null); mPasswordView.setError(null); String username = mUsernameView.getText().toString(); String password = mPasswordView.getText().toString(); boolean cancel = false; View focusView = null; if (TextUtils.isEmpty(password)) { mPasswordView.setError(getString(R.string.error_field_required)); focusView = mPasswordView; cancel = true; } if (TextUtils.isEmpty(username)) { mUsernameView.setError(getString(R.string.error_field_required)); focusView = mUsernameView; cancel = true; } if (cancel) { focusView.requestFocus(); } else { mLoginStatusMessageView.setText(R.string.login_progress_signing_in); showProgress(true); mPresenter.login(username, password); } } /** * Shows the progress UI and hides the login form. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) public void showProgress(final boolean show) { if (getActivity() == null) { //防止 not attact to activity 出错 return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { int shortAnimTime = getResources().getInteger( android.R.integer.config_shortAnimTime); mLoginStatusView.setVisibility(View.INVISIBLE); mLoginStatusView.animate().setDuration(shortAnimTime).alpha(show ? 1 : 0) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mLoginStatusView.setVisibility(show ? View.VISIBLE : View.GONE); } }); mLoginFormView.setVisibility(View.INVISIBLE); mLoginFormView.animate().setDuration(shortAnimTime).alpha(show ? 0 : 1) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); } }); } else { mLoginStatusView.setVisibility(show ? View.VISIBLE : View.GONE); mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); } } @Override public void onLoginError(Throwable e) { e.printStackTrace(); } @Override public void onLoginResult(String user) { Activity activity = getActivity(); if (activity == null || activity.isFinishing()) { return; } showProgress(false); if (!TextUtils.isEmpty(user)) { CDToast.showToast(activity, R.string.tip_login_success); Intent intent = new Intent(Constants.IntentAction.ACTION_LOGIN); intent.putExtra(Constants.IntentAction.EXTRA_LOGIN_USER, user); activity.sendBroadcast(intent); activity.finish(); } else { CDToast.showToast(activity, R.string.tip_login_failure); } } @Override public void addSubscription(Subscription subscription) { mCompositeSubscription.add(subscription); } @Override public void setPresenter(LoginContract.Presenter presenter) { mPresenter = presenter; } @Override public boolean isActive() { return isAdded(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/fragment/NewsListFragment.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui.fragment; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.TextView; import android.widget.Toast; import com.halzhang.android.apps.startupnews.Constants.IntentAction; import com.halzhang.android.apps.startupnews.MyApplication; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.SnApiComponent; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.presenter.DaggerNewsListFragmentComponent; import com.halzhang.android.apps.startupnews.presenter.NewsListContract; import com.halzhang.android.apps.startupnews.presenter.NewsListPresenter; import com.halzhang.android.apps.startupnews.presenter.NewsListPresenterModule; import com.halzhang.android.apps.startupnews.ui.DiscussActivity; import com.halzhang.android.apps.startupnews.utils.AppUtils; import com.halzhang.android.common.CDLog; import com.halzhang.android.startupnews.data.entity.SNFeed; import com.halzhang.android.startupnews.data.entity.SNNew; import java.util.ArrayList; import javax.inject.Inject; /** * StartupNews *

*

* * @author Hal * @version Mar 7, 2013 */ public class NewsListFragment extends SwipeRefreshRecyclerFragment implements NewsListContract.View { private static final String LOG_TAG = NewsListFragment.class.getSimpleName(); @Inject NewsListPresenter mNewsListPresenter; private NewsListContract.Presenter mPresenter; @Override public void setPresenter(NewsListContract.Presenter presenter) { mPresenter = presenter; } @Override public boolean isActive() { return isAdded(); } @Override public void onSuccess(ArrayList snNews) { onRefreshComplete(); mSnFeed.setSnNews(snNews); mAdapter.notifyDataSetChanged(); } @Override public void onFailure(Throwable e) { CDLog.w(LOG_TAG, "", e); onRefreshComplete(); Toast.makeText(getActivity(), R.string.error, Toast.LENGTH_LONG).show(); Tracker.getInstance().sendException("NewsTask", e, false); } @Override public void onAtEnd() { onRefreshComplete(); Toast.makeText(getActivity(), R.string.tip_last_page, Toast.LENGTH_SHORT).show(); } /** * {@link NewsListFragment}选中监听器 */ public interface OnNewsSelectedListener { /** * 处理news被选中事件 * * @param position list position * @param snNew {@link SNNew} */ public void onNewsSelected(int position, SNNew snNew); } private OnNewsSelectedListener mNewsSelectedListener; private String mNewsURL; public static final String ARG_URL = "new_url"; private SNFeed mSnFeed = new SNFeed(); private NewsAdapter mAdapter; public NewsListFragment() { } public static NewsListFragment newInstance(String url) { NewsListFragment fragment = new NewsListFragment(); Bundle args = new Bundle(1); args.putString(ARG_URL, url); fragment.setArguments(args); return fragment; } private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (IntentAction.ACTION_LOGIN.equals(action)) { String user = intent.getStringExtra(IntentAction.EXTRA_LOGIN_USER); if (!TextUtils.isEmpty(user)) { mPresenter.getFeed(mNewsURL); } } } }; @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mNewsSelectedListener = (OnNewsSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnNewsSelectedListener"); } } @Override public void onDetach() { super.onDetach(); mNewsSelectedListener = null; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAdapter = new NewsAdapter(); Bundle args = getArguments(); if (args != null) { mNewsURL = args.getString(ARG_URL); } else { mNewsURL = getString(R.string.host, "/news"); } IntentFilter filter = new IntentFilter(); filter.addAction(IntentAction.ACTION_LOGIN); getActivity().registerReceiver(mReceiver, filter); SnApiComponent snApiComponent = ((MyApplication) getActivity().getApplication()).getSnApiComponent(); DaggerNewsListFragmentComponent.builder().snApiComponent(snApiComponent) .newsListPresenterModule(new NewsListPresenterModule(this)).build().inject(this); } @Override protected int getViewLayout() { return R.layout.refresh_recycler_view_layout; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mRecyclerView.setAdapter(mAdapter); if (mAdapter.isEmpty()) { mPresenter.getFeed(mNewsURL); mSwipeRefreshLayout.post(new Runnable() { @Override public void run() { mSwipeRefreshLayout.setRefreshing(true); } }); } } @Override public void onDestroyView() { super.onDestroyView(); CDLog.d(LOG_TAG, this.toString() + " destroy view!"); } @Override public void onDestroy() { super.onDestroy(); getActivity().unregisterReceiver(mReceiver); } @Override public boolean onContextItemSelected(MenuItem item) { int position = ((AdapterContextMenuInfo) item.getMenuInfo()).position; final SNNew snNew = mSnFeed.getSnNews().get(position); Log.i(LOG_TAG, snNew.toString()); switch (item.getItemId()) { case R.id.menu_show_comment: Tracker.getInstance().sendEvent("ui_action", "context_item_selected", "newslistfragment_menu_show_comment", 0L); openDiscuss(snNew); return true; case R.id.menu_show_article: Tracker.getInstance().sendEvent("ui_action", "context_item_selected", "newslistfragment_menu_show_acticle", 0L); openArticle(position - 1, snNew); return true; case R.id.menu_up_vote: Tracker.getInstance().sendEvent("ui_action", "context_item_selected", "newslistfragment_menu_upvote", 0L); // TODO: 16/8/14 投票操作 return true; default: break; } return true; } @Override protected void onRefreshData() { super.onRefreshData(); Tracker.getInstance().sendEvent("ui_action", "pull_down_list_view_refresh", "news_list_fragment_pull_down_list_view_refresh", 0L); mPresenter.getFeed(mNewsURL); } @Override protected void onLoadMore() { super.onLoadMore(); Tracker.getInstance().sendEvent("ui_action", "pull_up_list_view_refresh", "news_list_fragment_pull_up_list_view_refresh", 0L); mPresenter.getMoreFeed(); } private void openArticle(int position, SNNew snNew) { if (mNewsSelectedListener != null) { mNewsSelectedListener.onNewsSelected(position, snNew); } } private void openDiscuss(SNNew snNew) { if (snNew == null) { return; } DiscussActivity.start(getActivity(),snNew.getDiscussURL(),snNew); } private class NewsAdapter extends RecyclerView.Adapter { public final class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener { public final TextView user; public final TextView createAt; public final TextView title; public final TextView subText; public final TextView domain; public final View mView; public ViewHolder(View itemView) { super(itemView); mView = itemView; user = (TextView) itemView.findViewById(R.id.news_item_user); createAt = (TextView) itemView.findViewById(R.id.news_item_createat); title = (TextView) itemView.findViewById(R.id.news_item_title); subText = (TextView) itemView.findViewById(R.id.news_item_subtext); domain = (TextView) itemView.findViewById(R.id.news_item_domain); mView.setOnCreateContextMenuListener(this); mView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int position = (int) v.getTag(); SNNew entity = mSnFeed.getSnNews().get(position); Tracker.getInstance().sendEvent("ui_action", "list_item_click", "news_list_fragment_list_item_click", 0L); mAdapter.notifyDataSetChanged(); openArticle(position, entity); } }); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { getActivity().getMenuInflater().inflate(R.menu.fragment_news, menu); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.news_list_item, null)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { final SNNew entity = mSnFeed.getSnNews().get(position); holder.user.setText(entity.getUser().getId()); holder.title.setText(entity.getTitle()); holder.subText.setText(getString(R.string.news_subtext, entity.getPoints(), entity.getCommentsCount())); holder.createAt.setText(entity.getCreateat()); holder.domain.setText(entity.getUrlDomain()); int textColor = AppUtils.getMyApplication(getActivity()).isHistoryContains( entity.getUrl()) ? Color.GRAY : Color.BLACK; holder.title.setTextColor(textColor); holder.subText.setTextColor(textColor); holder.domain.setTextColor(textColor); holder.createAt.setTextColor(textColor); holder.mView.setTag(position); } @Override public int getItemCount() { return mSnFeed.getSnNews().size(); } public boolean isEmpty() { return getItemCount() == 0; } } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/fragment/SwipeRefreshRecyclerFragment.java ================================================ package com.halzhang.android.apps.startupnews.ui.fragment; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.ui.widgets.CardViewDividerDecoration; import com.halzhang.android.apps.startupnews.ui.widgets.DividerDecoration; public abstract class SwipeRefreshRecyclerFragment extends Fragment { private static final String LOG_TAG = SwipeRefreshRecyclerFragment.class.getSimpleName(); protected SwipeRefreshLayout mSwipeRefreshLayout; protected RecyclerView mRecyclerView; private LinearLayoutManager mLinearLayoutManager; public SwipeRefreshRecyclerFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } protected abstract int getViewLayout(); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(getViewLayout(), null); mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); mSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh_layout); if (mRecyclerView == null || mSwipeRefreshLayout == null) { throw new IllegalArgumentException("mush be have RecyclerView and SwipeRefreshLayout"); } mSwipeRefreshLayout.setColorSchemeResources(android.R.color.holo_red_light, android.R.color.holo_orange_light, android.R.color.holo_green_light, android.R.color.holo_blue_light); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { onRefreshData(); } }); mLinearLayoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false); mRecyclerView.setLayoutManager(mLinearLayoutManager); mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); boolean enable = false; int visibleItemCount = mRecyclerView.getChildCount(); int itemCount = mLinearLayoutManager.getItemCount(); int firstVisibleItemPosition = mLinearLayoutManager.findFirstVisibleItemPosition(); int lastVisibleItemPosition = mLinearLayoutManager.findLastVisibleItemPosition(); // if (visibleItemCount > 0) { // boolean firstItemVisible = firstVisibleItemPosition == 0; // boolean topOfFirstItemVisible = mLinearLayoutManager.getChildAt(0).getTop() == 0; // enable = firstItemVisible && topOfFirstItemVisible; // Log.d(LOG_TAG, "SwipeRefreshLayout enable: " + enable); // } // mSwipeRefreshLayout.setEnabled(enable); if (lastVisibleItemPosition == itemCount - 1) { onLoadMore(); } } }); mRecyclerView.addItemDecoration(new CardViewDividerDecoration(getActivity())); return view; } /** * 刷新数据 */ protected void onRefreshData() { } /** * 加载更多 */ protected void onLoadMore() { } protected void onRefreshComplete() { if (mSwipeRefreshLayout != null) { mSwipeRefreshLayout.setRefreshing(false); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); } @Override public void onDetach() { super.onDetach(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/phone/BrowseActivity.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui.phone; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.ui.BaseFragmentActivity; import com.halzhang.android.apps.startupnews.ui.tablet.BrowseFragment; import android.annotation.TargetApi; import android.os.Build; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.text.TextUtils; import android.view.MenuItem; import android.view.Window; /** * StartupNews *

* 浏览页面 *

* * @author Hal * @version Mar 7, 2013 */ public class BrowseActivity extends BaseFragmentActivity { // private static final String LOG_TAG = // BrowseActivity.class.getSimpleName(); public static final String EXTRA_URL = "extra_url"; public static final String EXTRA_TITLE = "extra_title"; @Override protected void onCreate(Bundle arg0) { super.onCreate(arg0); setContentView(R.layout.activity_browse); String mOriginalUrl = getIntent().getStringExtra(EXTRA_URL); if (TextUtils.isEmpty(mOriginalUrl)) { finish(); return; } String mTitle = getIntent().getStringExtra(EXTRA_TITLE); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowTitleEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); } BrowseFragment fragment = new BrowseFragment(); Bundle bundle = new Bundle(); bundle.putString(EXTRA_URL, mOriginalUrl); bundle.putString(EXTRA_TITLE, mTitle); fragment.setArguments(bundle); getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment) .commitAllowingStateLoss(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); return true; default: return super.onOptionsItemSelected(item); } } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/tablet/BrowseFragment.java ================================================ package com.halzhang.android.apps.startupnews.ui.tablet; import android.annotation.TargetApi; import android.app.Activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.view.MenuCompat; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.ShareActionProvider; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.ui.BaseFragmentActivity; import com.halzhang.android.apps.startupnews.ui.phone.BrowseActivity; import com.halzhang.android.apps.startupnews.ui.widgets.ObservableWebView; import com.halzhang.android.apps.startupnews.ui.widgets.ObservableWebView.OnScrollChangedCallback; import com.halzhang.android.apps.startupnews.ui.widgets.WebViewController; import com.halzhang.android.apps.startupnews.utils.PreferenceUtils; import com.halzhang.android.common.CDLog; /** * 浏览器 Created by Hal on 13-5-25. */ public class BrowseFragment extends Fragment implements OnScrollChangedCallback { private static final String LOG_TAG = BrowseFragment.class.getSimpleName(); private ObservableWebView mWebView; private WebViewController mWebViewController; private String mTitle; private String mOriginalUrl; @Override public void onAttach(Activity activity) { super.onAttach(activity); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mWebViewController = new WebViewController(getActivity()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { setHasOptionsMenu(true); final View view = inflater.inflate(R.layout.fragment_browse, null); mWebView = (ObservableWebView) view.findViewById(R.id.webview); Activity activity = getActivity(); mWebView.setOverScrollMode(View.OVER_SCROLL_NEVER); mWebView.setOnScrollChangedCallback(this); mWebViewController.initControllerView(mWebView, view); Bundle args = getArguments(); if (args != null && args.containsKey(BrowseActivity.EXTRA_TITLE) && args.containsKey(BrowseActivity.EXTRA_URL)) { mTitle = args.getString(BrowseActivity.EXTRA_TITLE); mOriginalUrl = args.getString(BrowseActivity.EXTRA_URL); String mHtmlProvider = PreferenceUtils.getHtmlProvider(activity); final String url = mHtmlProvider + mOriginalUrl; mWebViewController.loadUrl(url); } return view; } @Override public void onDestroy() { mWebViewController.destroy(); super.onDestroy(); } public void setTitle(String title) { mTitle = title; } public void load(String url) { mOriginalUrl = url; String mHtmlProvider = PreferenceUtils.getHtmlProvider(getActivity()); mWebViewController.loadUrl(mHtmlProvider + mOriginalUrl); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.fragment_browse, menu); super.onCreateOptionsMenu(menu, inflater); MenuItem actionItem = menu.findItem(R.id.menu_share); ShareActionProvider actionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(actionItem); if (actionProvider == null) { return; } actionProvider.setShareHistoryFileName(ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME); actionProvider.setShareIntent(createShareIntent()); actionProvider.setOnShareTargetSelectedListener(new ShareActionProvider.OnShareTargetSelectedListener() { @Override public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) { /* * 这里改变intent是没用的,intent只是一份拷贝,只能自己启动修改后的Intent * 然而自己启动Intent并不会改变历史记录 增加setAllowPolicyChangeIntent方法解决这问题 */ String packageName = intent.getComponent().getPackageName(); CDLog.i(LOG_TAG, packageName); String shareContent = getShareContent(); intent.putExtra(Intent.EXTRA_SUBJECT, mTitle); Tracker.getInstance().sendEvent("ui_action", "share", packageName, 0L); if (getString(R.string.weibo_package_name).equals(packageName)) { intent.putExtra(Intent.EXTRA_TEXT, shareContent + " " + getString(R.string.weibo_share_suffix)); } else { intent.putExtra(Intent.EXTRA_TEXT, shareContent); } return false; } }); } /** * Creates a sharing {@link Intent}. * * @return The sharing intent. */ private Intent createShareIntent() { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, mTitle); intent.putExtra(Intent.EXTRA_TEXT, getShareContent()); return intent; } private String getShareContent() { StringBuilder builder = new StringBuilder(); return builder.append(mTitle).append(" ").append(mOriginalUrl).toString(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_original_url: Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "browseactivity_menu_original_url", 0L); mWebViewController.loadUrl(mOriginalUrl); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onScroll(int l, int t, int oldl, int oldt) { } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void onScrollUp() { //TODO 需要判断 Activity 的类型 BaseFragmentActivity activity = (BaseFragmentActivity) getActivity(); activity.getSupportActionBar().show(); mWebViewController.showBrowseBar(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void onScrollDown() { BaseFragmentActivity activity = (BaseFragmentActivity) getActivity(); activity.getSupportActionBar().hide(); mWebViewController.hideBrowseBar(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/tablet/DiscussFragment.java ================================================ package com.halzhang.android.apps.startupnews.ui.tablet; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.presenter.DiscussContract; import com.halzhang.android.apps.startupnews.ui.LoginActivity; import com.halzhang.android.apps.startupnews.utils.ActivityUtils; import com.halzhang.android.common.CDToast; import com.halzhang.android.startupnews.data.entity.SNComment; import com.halzhang.android.startupnews.data.entity.SNDiscuss; import com.halzhang.android.startupnews.data.entity.SNNew; import com.halzhang.android.startupnews.data.entity.Status; import com.halzhang.android.startupnews.data.utils.SessionManager; import javax.inject.Inject; /** * 查看评论 * Created by Hal on 13-5-26. */ public class DiscussFragment extends Fragment implements OnItemClickListener, DiscussContract.View { public static final String ARG_DISCUSS_URL = "discuss_url"; public static final String ARG_SNNEW = "snnew"; private static final String LOG_TAG = DiscussFragment.class.getSimpleName(); private DiscussContract.Presenter mPresenter; @Override public void setPresenter(DiscussContract.Presenter presenter) { mPresenter = presenter; } @Override public boolean isActive() { return isAdded(); } @Override public void onGetDiscuss(SNDiscuss snDiscuss) { mSnDiscuss.clearComments(); mSnDiscuss.copy(snDiscuss); setRefreshActionButtonState(false); wrapHeaderView(mSnDiscuss.getSnNew()); mAdapter.notifyDataSetChanged(); } @Override public void onGetDiscussFailure(Throwable e) { Tracker.getInstance().sendException("DiscussTask", e, false); Toast.makeText(getActivity(), R.string.error, Toast.LENGTH_SHORT).show(); } @Override public void onCommentSuccess(Status status) { switch (status.code){ case Status.CODE_COOKIE_VALID: CDToast.showToast(getActivity(), R.string.tip_cookie_invalid); startActivity(new Intent(getActivity(), LoginActivity.class)); Tracker.getInstance().sendEvent("ui_action_feedback", "comment_feedback", getString(R.string.tip_cookie_invalid), 0L); break; case Status.CODE_SUCCESS: mCommentEdit.setText(null); CDToast.showToast(getActivity(), R.string.tip_comment_success); Tracker.getInstance().sendEvent("ui_action_feedback", "comment_feedback", "success", 0L); loadData(); break; default: Tracker.getInstance().sendEvent("ui_action_feedback", "comment_feedback", status.message, 0L); CDToast.showToast(getActivity(), R.string.tip_comment_failure); break; } } @Override public void onCommentFailure(Throwable e) { CDToast.showToast(getActivity(), R.string.tip_comment_failure); Tracker.getInstance().sendException("comment error!", e, false); } @Override public void onSessionExpired() { Intent intent = new Intent(getActivity(), LoginActivity.class); startActivity(intent); } public interface OnMenuSelectedListener { public void onShowArticleSelected(SNNew snNew); public void onUpVoteSelected(String postId); } private SNDiscuss mSnDiscuss; private ListView mListView; private String mDiscussURL; private DiscussCommentAdapter mAdapter; private TextView mTitle; private TextView mSubTitle; private TextView mText; private Menu mOptionsMenu; private EditText mCommentEdit; private ImageButton mSendBtn; private OnMenuSelectedListener mListener; @Inject SessionManager mSessionManager; public DiscussFragment() { } public static DiscussFragment newInstance(String discussURL, SNNew snNew) { DiscussFragment fragment = new DiscussFragment(); Bundle args = new Bundle(); args.putString(ARG_DISCUSS_URL, discussURL); args.putParcelable(ARG_SNNEW, snNew); fragment.setArguments(args); return fragment; } @Override public void onAttach(Activity activity) { super.onAttach(activity); if (activity instanceof OnMenuSelectedListener) { mListener = (OnMenuSelectedListener) activity; } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { setHasOptionsMenu(true); View view = inflater.inflate(R.layout.fragment_discuss, null); mListView = (ListView) view.findViewById(android.R.id.list); mListView.setOnItemClickListener(this); mSnDiscuss = new SNDiscuss(); Bundle args = getArguments(); SNNew snNew = null; if (args != null && args.containsKey(ARG_DISCUSS_URL)) { mDiscussURL = args.getString(ARG_DISCUSS_URL); } else { throw new IllegalArgumentException("Discuss URL is required!"); } if (args.containsKey(ARG_SNNEW)) { snNew = args.getParcelable(ARG_SNNEW); mSnDiscuss.setSnNew(snNew); } mAdapter = new DiscussCommentAdapter(); View headerView = inflater.inflate(R.layout.discuss_header_view, null); mTitle = (TextView) headerView.findViewById(R.id.discuss_news_title); mSubTitle = (TextView) headerView.findViewById(R.id.discuss_news_subtitle); mText = (TextView) headerView.findViewById(R.id.discuss_text); mSendBtn = (ImageButton) view.findViewById(R.id.discuss_comment_send_btn); mSendBtn.setEnabled(false); mSendBtn.setOnClickListener(mSendBtnClickListener); mCommentEdit = (EditText) view.findViewById(R.id.discuss_comment_edit); mCommentEdit.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { mSendBtn.setEnabled(s.length() > 0); } }); mListView.addHeaderView(headerView); mListView.setAdapter(mAdapter); wrapHeaderView(snNew); loadData(); return view; } private OnClickListener mSendBtnClickListener = new OnClickListener() { @Override public void onClick(View v) { Tracker.getInstance().sendEvent("ui_action", "view_clicked", "discussactivity_button_comment", 0L); mPresenter.comment(mCommentEdit.getText().toString()); } }; @Override public void onDestroy() { super.onDestroy(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); mOptionsMenu = menu; inflater.inflate(R.menu.fragment_discuss, menu); } private void wrapHeaderView(SNNew snNew) { if (snNew != null) { mTitle.setText(snNew.getTitle()); mSubTitle.setText(Html.fromHtml(snNew.getSubText())); if (snNew.isDiscuss()) { mText.setVisibility(View.VISIBLE); mText.setText(snNew.getText()); } else { mText.setVisibility(View.GONE); } } } public void setRefreshActionButtonState(boolean refreshing) { if (mOptionsMenu == null) { Log.i(LOG_TAG, "Option menu is null!"); return; } final MenuItem refreshItem = mOptionsMenu.findItem(R.id.menu_refresh); if (refreshItem != null) { if (refreshing) { refreshItem.setActionView(R.layout.actionbar_indeterminate_progress); } else { refreshItem.setActionView(null); } } } public void loadData() { mPresenter.getDiscuss(mDiscussURL); setRefreshActionButtonState(true); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "discussactivity_menu_refresh", 0L); loadData(); return true; case R.id.menu_show_article: if (mListener != null) { mListener.onShowArticleSelected(mSnDiscuss.getSnNew()); } return true; case R.id.menu_up_vote: if (mListener != null) { mListener.onUpVoteSelected(mSnDiscuss.getSnNew().getPostID()); } default: return super.onOptionsItemSelected(item); } } private class DiscussCommentAdapter extends BaseAdapter { @Override public int getCount() { return mSnDiscuss.commentSize(); } @Override public Object getItem(int position) { return mSnDiscuss.getComments().get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = LayoutInflater.from(getActivity()).inflate( R.layout.discuss_comment_item, null); holder.mUserId = (TextView) convertView .findViewById(R.id.discuss_comment_item_user_id); holder.mCreated = (TextView) convertView .findViewById(R.id.discuss_comment_item_created); holder.mCommentText = (TextView) convertView .findViewById(R.id.discuss_comment_item_text); holder.mArtistTitle = (TextView) convertView .findViewById(R.id.discuss_comment_item_artist_titile); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } SNComment comment = mSnDiscuss.getComments().get(position); holder.mUserId.setText(comment.getUser().getId()); holder.mCreated.setText(comment.getCreated()); holder.mCommentText.setText(comment.getText()); holder.mArtistTitle.setVisibility(View.GONE); return convertView; } class ViewHolder { TextView mUserId; TextView mCreated; TextView mCommentText; TextView mArtistTitle; } } @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (position == 0 && mListener == null) { Tracker.getInstance().sendEvent("ui_action", "list_item_click", "discuss_activity_list_header_click", 0L); // 查看文章 ActivityUtils.openArticle(getActivity(), mSnDiscuss.getSnNew()); } } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/widgets/CardViewDividerDecoration.java ================================================ package com.halzhang.android.apps.startupnews.ui.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.view.View; import com.halzhang.android.apps.startupnews.R; /** * Divider for {@link RecyclerView} * Created by Hal on 15/7/19. */ public class CardViewDividerDecoration extends RecyclerView.ItemDecoration { private Drawable mDivider; private int mInsets; public CardViewDividerDecoration(Context context) { mDivider = new ColorDrawable(context.getResources().getColor(android.R.color.transparent)); mInsets = context.getResources().getDimensionPixelSize(R.dimen.card_insets); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { drawVertical(c, parent); } /** * Draw dividers underneath each child view */ public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin + mInsets; final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { //We can supply forced insets for each item view here in the Rect outRect.set(mInsets, mInsets, mInsets, mInsets); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/widgets/DividerDecoration.java ================================================ package com.halzhang.android.apps.startupnews.ui.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.view.View; import com.halzhang.android.apps.startupnews.R; /** * Divider for {@link RecyclerView} * Created by Hal on 15/7/19. */ public class DividerDecoration extends RecyclerView.ItemDecoration { private static final int[] ATTRS = {android.R.attr.listDivider}; private Drawable mDivider; private int mInsets; public DividerDecoration(Context context) { TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); mInsets = context.getResources().getDimensionPixelSize(R.dimen.card_insets); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { drawVertical(c, parent); } /** * Draw dividers underneath each child view */ public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin + mInsets; final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { //We can supply forced insets for each item view here in the Rect outRect.set(mInsets, mInsets, mInsets, mInsets); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/widgets/ObservableWebView.java ================================================ /* * * Copyright (C) 2013 HalZhang. * http://www.gnu.org/licenses/gpl-3.0.txt */ package com.halzhang.android.apps.startupnews.ui.widgets; import android.content.Context; import android.util.AttributeSet; import android.webkit.WebView; /** * StartupNews *

*

* * @author Hal * @version Aug 25, 2013 */ public class ObservableWebView extends WebView { private final static int TOUCH_STATE_REST = 0; private final static int TOUCH_STATE_SCROLL_UP = 1; private final static int TOUCH_STATE_SCROLL_DOWN = 2; private int mTouchState = TOUCH_STATE_REST; private OnScrollChangedCallback mOnScrollChangedCallback; public ObservableWebView(final Context context) { super(context); } public ObservableWebView(final Context context, final AttributeSet attrs) { super(context, attrs); } public ObservableWebView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } @Override protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (mOnScrollChangedCallback != null) { mOnScrollChangedCallback.onScroll(l, t, oldl, oldt); if (t >= 0 && oldt >= 0) { if (oldt > t && mTouchState != TOUCH_STATE_SCROLL_UP) { mTouchState = TOUCH_STATE_SCROLL_UP; mOnScrollChangedCallback.onScrollUp(); } else if (oldt < t && mTouchState != TOUCH_STATE_SCROLL_DOWN) { mTouchState = TOUCH_STATE_SCROLL_DOWN; mOnScrollChangedCallback.onScrollDown(); } } } } public OnScrollChangedCallback getOnScrollChangedCallback() { return mOnScrollChangedCallback; } public void setOnScrollChangedCallback(final OnScrollChangedCallback onScrollChangedCallback) { mOnScrollChangedCallback = onScrollChangedCallback; } /** * Impliment in the activity/fragment/view that you want to listen to the * webview */ public static interface OnScrollChangedCallback { public void onScroll(int l, int t, int oldl, int oldt); /** * 向上滚动 */ public void onScrollUp(); /** * 向下滚动 */ public void onScrollDown(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/widgets/SwitchPreference.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.ui.widgets; import android.content.Context; import android.preference.TwoStatePreference; import android.util.AttributeSet; /** * StartupNews *

*

* @author Hal * @version Mar 17, 2013 */ public class SwitchPreference extends TwoStatePreference { public SwitchPreference(Context context, AttributeSet attrs) { super(context, attrs); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/ui/widgets/WebViewController.java ================================================ package com.halzhang.android.apps.startupnews.ui.widgets; import com.halzhang.android.apps.startupnews.R; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.apps.startupnews.ui.BaseFragmentActivity; import com.halzhang.android.apps.startupnews.utils.UIUtils; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ImageButton; import java.lang.ref.WeakReference; /** * WebView and Browse Bar Controller * * @author Hal */ public class WebViewController implements OnClickListener { private WeakReference mActivityRef; private WebView mWebView; private ImageButton mBackButton; private ImageButton mForwardButton; private ImageButton mReadabilityButton; private ImageButton mRefreshButton; private ImageButton mWebSiteButton; private View mToolBar; private String mCurrentUrl; public WebViewController(Activity activity) { mActivityRef = new WeakReference(activity); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SuppressLint("SetJavaScriptEnabled") public void initControllerView(WebView webView, View view) { if (webView == null || view == null) { return; } mWebView = webView; mWebView.setWebChromeClient(new MyWebChromeClient()); mWebView.setWebViewClient(new MyWebViewClient()); WebSettings settings = mWebView.getSettings(); settings.setSupportZoom(true); settings.setBuiltInZoomControls(true); if (UIUtils.hasHoneycomb()) { settings.setDisplayZoomControls(false); } settings.setJavaScriptEnabled(true); settings.setUseWideViewPort(true); settings.setLoadWithOverviewMode(true); mBackButton = (ImageButton) view.findViewById(R.id.browse_back); mForwardButton = (ImageButton) view.findViewById(R.id.browse_forward); mReadabilityButton = (ImageButton) view.findViewById(R.id.browse_readability); mRefreshButton = (ImageButton) view.findViewById(R.id.browse_refresh); mWebSiteButton = (ImageButton) view.findViewById(R.id.browse_website); mToolBar = view.findViewById(R.id.browse_bar); mBackButton.setOnClickListener(this); mForwardButton.setOnClickListener(this); mReadabilityButton.setOnClickListener(this); mRefreshButton.setOnClickListener(this); mWebSiteButton.setOnClickListener(this); } private class MyWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); setCurrentUrl(url); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); setCurrentUrl(url); } } private void setCurrentUrl(String url) { mCurrentUrl = url; } private String getCurrentUrl() { return mCurrentUrl; //TextUtils.isEmpty(mCurrentUrl) ? mOriginalUrl : mCurrentUrl; } private class MyWebChromeClient extends WebChromeClient { @Override public void onProgressChanged(WebView view, int newProgress) { super.onProgressChanged(view, newProgress); int progress = (Window.PROGRESS_END - Window.PROGRESS_START) / 100 * newProgress; Activity activity = mActivityRef.get(); if (activity != null && activity instanceof BaseFragmentActivity) { ((BaseFragmentActivity) activity).setSupportProgress(progress); } } @Override public void onReceivedTitle(WebView view, String title) { super.onReceivedTitle(view, title); mActivityRef.get().setTitle(title); } } @Override public void onClick(View v) { switch (v.getId()) { case R.id.browse_back: back(); break; case R.id.browse_forward: forward(); break; case R.id.browse_readability: readability(); break; case R.id.browse_refresh: refresh(); break; case R.id.browse_website: webSite(); break; default: break; } } private void back() { Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "browseactivity_menu_back", 0L); if (mWebView.canGoBack()) { mWebView.goBack(); } } private void forward() { Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "browseactivity_menu_forward", 0L); if (mWebView.canGoForward()) { mWebView.goForward(); } } private void readability() { Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "browseactivity_menu_readability", 0L); if (TextUtils.isEmpty(mCurrentUrl)) { return; } mWebView.loadUrl("http://www.readability.com/m?url=" + getCurrentUrl()); } private void refresh() { Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "browseactivity_menu_refresh", 0L); mWebView.reload(); } private void webSite() { // 打开原链接,还是转码的链接呢? Tracker.getInstance().sendEvent("ui_action", "options_item_selected", "browseactivity_menu_website", 0L); if (TextUtils.isEmpty(mCurrentUrl)) { return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(getCurrentUrl())); mActivityRef.get().startActivity(intent); } public WebView getWebView() { return mWebView; } public void destroy() { if (mWebView != null) { ((ViewGroup) mWebView.getParent()).removeAllViews(); mWebView.clearHistory(); mWebView.clearCache(true); mWebView.loadUrl("about:blank"); mWebView.pauseTimers(); mWebView.destroy(); mWebView = null; } mActivityRef.clear(); } public void loadUrl(String url) { if (TextUtils.isEmpty(url) || url.equals(mCurrentUrl)) { return; } mWebView.clearHistory(); mWebView.loadUrl(url); } public void showBrowseBar() { if (mToolBar != null) { Animation animation = AnimationUtils.loadAnimation(mActivityRef.get(), R.anim.push_up_in); mToolBar.startAnimation(animation); mToolBar.setVisibility(View.VISIBLE); } } public void hideBrowseBar() { if (mToolBar != null) { Animation animation = AnimationUtils.loadAnimation(mActivityRef.get(), R.anim.push_down_out); mToolBar.startAnimation(animation); mToolBar.setVisibility(View.GONE); } } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/ActivityScoped.java ================================================ package com.halzhang.android.apps.startupnews.utils; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Scope; /** * Created by Hal on 16/6/12. */ @Documented @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScoped { } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/ActivityUtils.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.utils; import android.app.Activity; import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.View; import com.halzhang.android.apps.startupnews.ui.phone.BrowseActivity; import com.halzhang.android.startupnews.data.entity.SNNew; import java.util.List; /** * StartupNews *

*

* * @author Hal * @version Mar 22, 2013 */ public class ActivityUtils { public static void openArticle(Activity activity, SNNew snNew) { if (snNew == null || activity == null) { return; } Intent intent = null; if (PreferenceUtils.isUseInnerBrowse(activity)) { intent = new Intent(activity, BrowseActivity.class); intent.putExtra(BrowseActivity.EXTRA_URL, snNew.getUrl()); intent.putExtra(BrowseActivity.EXTRA_TITLE, snNew.getTitle()); } else { intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(PreferenceUtils.getHtmlProvider(activity) + snNew.getUrl())); } activity.startActivity(intent); AppUtils.getMyApplication(activity).addHistory(snNew.getUrl()); } /** * With window Animations * * @param activity * @param snNew * @param v */ public static void openActicle(Activity activity, SNNew snNew, View v) { if (snNew == null || activity == null) { return; } Bundle b = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // b = ActivityOptions.makeScaleUpAnimation(view, 0, 0, // view.getWidth(), // view.getHeight()).toBundle(); Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); bitmap.eraseColor(Color.WHITE); b = ActivityOptions.makeThumbnailScaleUpAnimation(v, bitmap, 0, 0).toBundle(); } Intent intent = null; if (PreferenceUtils.isUseInnerBrowse(activity)) { intent = new Intent(activity, BrowseActivity.class); intent.putExtra(BrowseActivity.EXTRA_URL, snNew.getUrl()); intent.putExtra(BrowseActivity.EXTRA_TITLE, snNew.getTitle()); } else { intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(PreferenceUtils.getHtmlProvider(activity) + snNew.getUrl())); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { activity.startActivity(intent, b); } else { activity.startActivity(intent); } } /** * 判断Intent是否可用 * * @param context * @param intent * @return */ public static boolean isIntentAvailable(Context context, Intent intent) { final PackageManager packageManager = context.getPackageManager(); List list = packageManager.queryIntentActivities(intent, PackageManager.GET_ACTIVITIES); return list.size() > 0; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/AppUtils.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.utils; import com.halzhang.android.apps.startupnews.MyApplication; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; /** * StartupNews *

*

* * @author Hal * @version Mar 12, 2013 */ public class AppUtils { public static String getVersionName(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionName; } catch (NameNotFoundException e) { return ""; } } public static int getVersionCode(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { return 0; } } public static MyApplication getMyApplication(Context context) { return (MyApplication) context.getApplicationContext(); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/CrashHandler.java ================================================ package com.halzhang.android.apps.startupnews.utils; import com.google.analytics.tracking.android.EasyTracker; import com.halzhang.android.apps.startupnews.BuildConfig; import com.halzhang.android.apps.startupnews.Constants; import com.halzhang.android.apps.startupnews.MyApplication; import com.halzhang.android.apps.startupnews.analytics.Tracker; import com.halzhang.android.common.CDLog; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.Thread.UncaughtExceptionHandler; import java.lang.reflect.Field; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; /** * Crash info collection */ public class CrashHandler implements UncaughtExceptionHandler { private static final String LOG_TAG = CrashHandler.class.getSimpleName(); private static CrashHandler me = null; private Context mContext; private Map infos = new HashMap(); private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()); private CrashHandler() { } public static CrashHandler getInstance() { if (me == null) { me = new CrashHandler(); } return me; } public void init(Context context) { mContext = context; Thread.setDefaultUncaughtExceptionHandler(this); } @Override public void uncaughtException(Thread thread, Throwable ex) { if (BuildConfig.DEBUG) { Log.e(LOG_TAG, ex.getMessage(), ex); handleException(ex); } Tracker.getInstance().sendException(ex.getMessage(), ex, true); android.os.Process.killProcess(android.os.Process.myPid()); System.exit(0); } private boolean handleException(Throwable ex) { if (ex != null) { collectDeviceInfo(mContext); saveCrashInfo2File(ex); return true; } return false; } private void collectDeviceInfo(Context ctx) { try { PackageManager pm = ctx.getPackageManager(); PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES); if (pi != null) { String versionName = pi.versionName == null ? "null" : pi.versionName; String versionCode = pi.versionCode + ""; infos.put("versionName", versionName); infos.put("versionCode", versionCode); } } catch (NameNotFoundException e) { } Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); infos.put(field.getName(), field.get(null).toString()); //Log.d(LOG_TAG, field.getName() + " : " + field.get(null)); } catch (Exception e) { } } } /** */ private String saveCrashInfo2File(Throwable ex) { StringBuffer sb = new StringBuffer(); for (Map.Entry entry : infos.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); sb.append(key + "=" + value + "\n"); } Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); ex.printStackTrace(printWriter); Throwable cause = ex.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); String result = writer.toString(); sb.append(result); try { long timestamp = System.currentTimeMillis(); String time = formatter.format(new Date()); String fileName = "crash-" + time + "-" + timestamp + ".txt"; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { String path = Environment.getExternalStorageDirectory().getAbsolutePath() + Constants.CRASH_LOG_DIR; File dir = new File(path); if (!dir.exists()) { dir.mkdirs(); } FileOutputStream fos = new FileOutputStream(path + fileName); fos.write(sb.toString().getBytes()); fos.close(); } return fileName; } catch (Exception e) { Log.e(LOG_TAG, "an error occured while writing file...", e); } return null; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/CustomTabsActivityHelper.java ================================================ package com.halzhang.android.apps.startupnews.utils; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.customtabs.CustomTabsCallback; import android.support.customtabs.CustomTabsClient; import android.support.customtabs.CustomTabsIntent; import android.support.customtabs.CustomTabsServiceConnection; import android.support.customtabs.CustomTabsSession; import android.text.TextUtils; import android.util.Log; /** * 界面 helper * Created by Hal on 15/11/25. */ public class CustomTabsActivityHelper { private static final String TAG = CustomTabsActivityHelper.class.getSimpleName(); private CustomTabsClient mClient; private CustomTabsSession mSession; private CustomTabsServiceConnection mConnection; public void bindService(Context context) { if (mConnection != null) { return; } String packageName = CustomTabsHelper.getPackageNameToUse(context); Log.i(TAG, "Package name: " + packageName); if (TextUtils.isEmpty(packageName)) { return; } mConnection = new CustomTabsServiceConnection() { @Override public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient customTabsClient) { Log.i(TAG, "Custom tabs service connected!"); mClient = customTabsClient; mClient.warmup(0); mSession = mClient.newSession(new CustomTabsCallback()); } @Override public void onServiceDisconnected(ComponentName name) { mClient = null; mSession = null; } }; CustomTabsClient.bindCustomTabsService(context, packageName, mConnection); } public CustomTabsSession getSession() { if (mClient == null) { return null; } if (mSession == null) { mSession = mClient.newSession(new CustomTabsCallback()); } return mSession; } public void unBindService(Context context) { if (mConnection == null) { return; } context.unbindService(mConnection); mConnection = null; } /** * 启动 * * @param activity {@link Activity} * @param url 链接 * @param customTabsIntent {@link CustomTabsIntent},由用户自定义属性 * @param listener {@link com.halzhang.android.apps.startupnews.utils.CustomTabsActivityHelper.OnCustomTabsInvalidListener},没有处理,使用默认浏览器打开 */ public void launchUrl(Activity activity, String url, CustomTabsIntent customTabsIntent, OnCustomTabsInvalidListener listener) { String packageName = CustomTabsHelper.getPackageNameToUse(activity); if (TextUtils.isEmpty(packageName)) { if (listener != null) { listener.onInvalid(url); } else { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); activity.startActivity(intent); } } else { customTabsIntent.launchUrl(activity, Uri.parse(url)); } } /** * 无效监听 */ public interface OnCustomTabsInvalidListener { /** * 当 custom tabs 无效时处理 * * @param url 链接 */ public void onInvalid(String url); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/CustomTabsHelper.java ================================================ package com.halzhang.android.apps.startupnews.utils; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.support.customtabs.CustomTabsService; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; import java.util.List; /** * helper * Created by Hal on 15/11/25. */ public class CustomTabsHelper { private static final String TAG = "CustomTabsHelper"; static final String STABLE_PACKAGE = "com.android.chrome"; static final String BETA_PACKAGE = "com.chrome.beta"; static final String DEV_PACKAGE = "com.chrome.dev"; static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE"; private static String sPackageNameToUse; private CustomTabsHelper() { } /** * Goes through all apps that handle VIEW intents and have a warmup service. Picks * the one chosen by the user if there is one, otherwise makes a best effort to return a * valid package name. *

* This is not threadsafe. * * @param context {@link Context} to use for accessing {@link PackageManager}. * @return The package name recommended to use for connecting to custom tabs related components. */ public static String getPackageNameToUse(Context context) { if (sPackageNameToUse != null) return sPackageNameToUse; PackageManager pm = context.getPackageManager(); // Get default VIEW intent handler. Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); String defaultViewHandlerPackageName = null; if (defaultViewHandlerInfo != null) { defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; } // Get all apps that can handle VIEW intents. List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); List packagesSupportingCustomTabs = new ArrayList<>(); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs.add(info.activityInfo.packageName); } } // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents // and service calls. if (packagesSupportingCustomTabs.isEmpty()) { sPackageNameToUse = null; } else if (packagesSupportingCustomTabs.size() == 1) { sPackageNameToUse = packagesSupportingCustomTabs.get(0); } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) && !hasSpecializedHandlerIntents(context, activityIntent) && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { sPackageNameToUse = defaultViewHandlerPackageName; } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { sPackageNameToUse = STABLE_PACKAGE; } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { sPackageNameToUse = BETA_PACKAGE; } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { sPackageNameToUse = DEV_PACKAGE; } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { sPackageNameToUse = LOCAL_PACKAGE; } return sPackageNameToUse; } /** * Used to check whether there is a specialized handler for a given intent. * * @param intent The intent to check with. * @return Whether there is a specialized handler for the given intent. */ private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) { try { PackageManager pm = context.getPackageManager(); List handlers = pm.queryIntentActivities( intent, PackageManager.GET_RESOLVED_FILTER); if (handlers == null || handlers.size() == 0) { return false; } for (ResolveInfo resolveInfo : handlers) { IntentFilter filter = resolveInfo.filter; if (filter == null) continue; if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue; if (resolveInfo.activityInfo == null) continue; return true; } } catch (RuntimeException e) { Log.e(TAG, "Runtime exception while getting specialized handlers"); } return false; } /** * @return All possible chrome package names that provide custom tabs feature. */ public static String[] getPackages() { return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/DateUtils.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.utils; import android.content.Context; import java.text.SimpleDateFormat; import java.util.Date; /** * StartupNews *

*

* @author Hal * @version Mar 12, 2013 */ public class DateUtils { public static String getLastUpdateLabel(Context context){ final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String ds = format.format(new Date(System.currentTimeMillis())); return "上次更新:"+ds; } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/FragmentScoped.java ================================================ package com.halzhang.android.apps.startupnews.utils; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Scope; /** * Created by Hal on 16/6/12. */ @Documented @Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScoped { } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/PreferenceUtils.java ================================================ /** * Copyright (C) 2013 HalZhang */ package com.halzhang.android.apps.startupnews.utils; import com.halzhang.android.apps.startupnews.R; import android.content.Context; import android.preference.PreferenceManager; /** * StartupNews *

*

* * @author Hal * @version Mar 17, 2013 */ public class PreferenceUtils { /** * 获取网页阅读模式 * * @param context * @return */ public static String getHtmlProvider(Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getString( context.getString(R.string.pref_key_html_provider), context.getString(R.string.default_html_provider)); } /** * 使用内置浏览器 * * @param context * @return */ public static boolean isUseInnerBrowse(Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean( context.getString(R.string.pref_key_default_browse), true); } public static void set(Context ctx, final String key, final String value) { PreferenceManager.getDefaultSharedPreferences(ctx).edit().putString(key, value).commit(); } public static String get(Context context, final String key) { return PreferenceManager.getDefaultSharedPreferences(context).getString(key, null); } } ================================================ FILE: app/src/main/java/com/halzhang/android/apps/startupnews/utils/UIUtils.java ================================================ package com.halzhang.android.apps.startupnews.utils; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Configuration; import android.os.Build; import android.view.View; /** * UI工具 * Created by Hal on 13-5-26. */ public final class UIUtils { @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static void setActivatedCompat(View view, boolean activated) { if (hasHoneycomb()) { view.setActivated(activated); } } public static boolean isGoogleTV(Context context) { return context.getPackageManager().hasSystemFeature("com.google.android.tv"); } public static boolean hasFroyo() { // Can use static final constants like FROYO, declared in later versions // of the OS since they are inlined at compile time. This is guaranteed behavior. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; } public static boolean hasGingerbread() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; } public static boolean hasHoneycomb() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } public static boolean hasHoneycombMR1() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1; } public static boolean hasICS() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; } public static boolean hasJellyBean() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; } public static boolean isTablet(Context context) { return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; } public static boolean isHoneycombTablet(Context context) { return hasHoneycomb() && isTablet(context); } } ================================================ FILE: app/src/main/res/anim/push_down_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/push_up_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_in_left.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_in_right.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_out_left.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_out_right.xml ================================================ ================================================ FILE: app/src/main/res/drawable/action_bar_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/background_boardless.xml ================================================ ================================================ FILE: app/src/main/res/drawable/background_holo_dark.xml ================================================ ================================================ FILE: app/src/main/res/drawable/background_holo_light.xml ================================================ ================================================ FILE: app/src/main/res/drawable/bg_discuss_article.xml ================================================ ================================================ FILE: app/src/main/res/drawable/fragment_shadow.xml ================================================ ================================================ FILE: app/src/main/res/drawable/send_button_selector.xml ================================================ ================================================ FILE: app/src/main/res/drawable/sidebar_shadow.xml ================================================ ================================================ FILE: app/src/main/res/layout/actionbar_indeterminate_progress.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_browse.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_discuss.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_login.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/browse_bar.xml ================================================ ================================================ FILE: app/src/main/res/layout/comment_list_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/discuss_comment_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/discuss_header_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_browse.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_discuss.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_login.xml ================================================