Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac3e2375bc | |||
| 3b8e0fcad2 | |||
| f8e3158e8f | |||
| 7bb5f380a7 | |||
| 618077c128 | |||
| 23aa0ebde7 | |||
| 17e98407c7 | |||
| 6d8605ed4d |
15
.gitignore
vendored
15
.gitignore
vendored
@ -1,11 +1,8 @@
|
||||
.idea/
|
||||
/.idea
|
||||
.idea/misc.xml
|
||||
*.iml
|
||||
.gradle/
|
||||
local.properties
|
||||
# sign.properties
|
||||
.gradle
|
||||
/local.properties
|
||||
.DS_Store
|
||||
captures/
|
||||
build/
|
||||
release-app/
|
||||
scripts/apk-channel/
|
||||
app/src/test/java/com/gh/gamecenter
|
||||
/build
|
||||
/captures
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@ -1,4 +0,0 @@
|
||||
[submodule "libraries/LGLibrary"]
|
||||
path = libraries/LGLibrary
|
||||
url = git@gitlab.ghzhushou.com:client/client-common.git
|
||||
branch = master
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
||||
GH-ASSISTv1.45
|
||||
22
.idea/compiler.xml
generated
Normal file
22
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<resourceExtensions />
|
||||
<wildcardResourcePatterns>
|
||||
<entry name="!?*.java" />
|
||||
<entry name="!?*.form" />
|
||||
<entry name="!?*.class" />
|
||||
<entry name="!?*.groovy" />
|
||||
<entry name="!?*.scala" />
|
||||
<entry name="!?*.flex" />
|
||||
<entry name="!?*.kt" />
|
||||
<entry name="!?*.clj" />
|
||||
<entry name="!?*.aj" />
|
||||
</wildcardResourcePatterns>
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="false">
|
||||
<processorPath useClasspath="true" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/copyright/profiles_settings.xml
generated
Normal file
3
.idea/copyright/profiles_settings.xml
generated
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
24
.idea/gradle.xml
generated
Normal file
24
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.10" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="myModules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
46
.idea/misc.xml
generated
Normal file
46
.idea/misc.xml
generated
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<entry_points version="2.0" />
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/modules.xml
generated
Normal file
9
.idea/modules.xml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/GH-ASSISTv1.45.iml" filepath="$PROJECT_DIR$/GH-ASSISTv1.45.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
Normal file
12
.idea/runConfigurations.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@ -1,38 +0,0 @@
|
||||
# 版本升级备忘
|
||||
|
||||
### Ver 2.5
|
||||
* 此处写本次更新所做的业务和代码修改
|
||||
|
||||
### Ver 2.6
|
||||
* xx
|
||||
|
||||
### Ver 3.0
|
||||
* 升级账号系统(登录流程/用户信息相关/用户账号相关操作(评论,礼包...))
|
||||
* 新增收藏功能(文章/工具箱)
|
||||
* 删除用户相关的所有本地数据库
|
||||
* 重做总开服表
|
||||
* 重做首页插件化模块
|
||||
* 礼包重复领取机制改变(可重复领取的礼包,领取后立刻显示再领一个/再淘一个)
|
||||
* 游戏下载平台面板修改(加快弹出速度,不再读取本地平台图片)
|
||||
* 接入bugly(tinker)
|
||||
|
||||
### VER 3.1
|
||||
### VER 3.2
|
||||
### VER 3.3
|
||||
### VER 3.4
|
||||
### VER 3.5
|
||||
|
||||
### Ver 3.6
|
||||
* 首页游戏增加预览骨架,游戏ITEM样式微调和开服标签
|
||||
* 首页问答增加关注页面
|
||||
* 重构游戏更新管理(游戏更新/插件化/已安装的游戏),具体细节参考PackageRepository & PackageViewModel
|
||||
* 重构APP更新管理(已VersionVode为更新基准,小版本更新改为光环后台控制)
|
||||
- 删除TINKER_VERISON_NAME
|
||||
- tinker打包方式变更(以小版本作为Base包,防止与数据后台小版本更新发生冲突)
|
||||
* 社区增加版主功能(版主可以对存在的相关内容进行修改/隐藏操作,内容包括问题/回答/回答评论)
|
||||
* 社区互动引导优化(问答推荐增加`推荐关注`,回答详情增加一些交互动效)
|
||||
|
||||
### Ver 3.6.1
|
||||
* 可以后台控制关闭资讯功能
|
||||
* 版块、分类、专题详情、游戏详情、礼包详情增加预览骨架
|
||||
* 下载按钮状态可以通过接口屏蔽相应的包
|
||||
69
README.md
69
README.md
@ -1,69 +0,0 @@
|
||||
# 光环助手Android客户端
|
||||
|
||||
### APK打包配置
|
||||
|
||||
* 使用[ApkChannelPackage](https://github.com/ltlovezh/ApkChannelPackage)的方案
|
||||
* 打包命令,视情况使用:
|
||||
|
||||
> 打包Tinker基准包:`./scripts/tinker_release_base.sh`
|
||||
|
||||
> 以Tinker基准包打渠道包:`./scripts/tinker_release_channel.sh`
|
||||
|
||||
> 以Tinker基准包打补丁包:`./scripts/tinker_release_patch.sh`
|
||||
|
||||
### 混淆配置
|
||||
|
||||
* 配置文件:Android默认配置+proguard-rules.txt等
|
||||
* 参考libraries下每个项目独立的配置文件`proguard-project.txt`
|
||||
|
||||
### apk大小优化
|
||||
|
||||
* 限制resConfig资源集
|
||||
* 开启ShrinkResources
|
||||
* 开启混淆,使用minifyEnabled(仅在release开启)
|
||||
* pngquant对png压缩、png/jpg->webp(未尝试)
|
||||
|
||||
### 第三方appkey等配置
|
||||
|
||||
* 修改`gradle.properties`文件将各种key填入其中,实现统一管理
|
||||
* 通过gradle文件内的resValue/buildConfigField/manifestPlaceHolder方式实现编译期间修改,具体情况请参考``./build.gradle``和``./app/build.gradle``配置
|
||||
|
||||
### 拉取代码步骤
|
||||
|
||||
1. 拉取主项目代码: `git clone -b dev git@gitlab.ghzhushou.com:halo/assistant-android.git`
|
||||
2. 初始化公用类库: `bash ./scripts/submodules_init.sh`
|
||||
|
||||
### submodule管理方式(只拉取master)
|
||||
|
||||
* 提交代码,需要cd到submodule文件夹去做修改
|
||||
* 更新远端代码,`bash ./scripts/submodules_update.sh`
|
||||
|
||||
### TODO
|
||||
|
||||
* GSON 序列化用统一的一个, GsonUtil fromJson
|
||||
* CleanApkAdapter 转化字符串size工具函数 比如SpeedUtils
|
||||
* getString 解决 字符串hardcode问题
|
||||
* ~~Adapter 里面clicklistener 用接口传参将点击操作委托给controller~~
|
||||
* ~~Adapter ViewHolder的功能,部分重写到ViewHolder类本身~~
|
||||
|
||||
* ~~activity 统一入口未完成(外部入口相关),去除多余activity使用,统一toolbar~~
|
||||
* ~~release / debug compile不同的类库,不需要再做什么开关~~
|
||||
|
||||
* ~~Toolbar分离,有图形按钮/没有图形按钮~~
|
||||
|
||||
### TODO Since 3.1
|
||||
|
||||
- 解决 Utils 工具类引发的内存泄漏问题
|
||||
- 把原有 EventBus 的消息 Type 统一到一个文件内
|
||||
- 将实现细节从 View(Fragment、Activity) 剥离并以 MVVM 结构改造
|
||||
- ~~将 ListViewModel 所对应的 ListRepository 合并到 ListViewModel 中~~
|
||||
- 依照光环助手界面功能以大模块 - 小模块的方式去修改包结构,包内文件建议以包名摘要作为前缀
|
||||
- ~~使用 RxJava 的 Debounce 和 Map 操作优化搜索触发机制 参考资料:[1](https://proandroiddev.com/building-an-autocompleting-edittext-using-rxjava-f69c5c3f5a40),[2](https://medium.com/@kurtisnusbaum/rxandroid-basics-part-2-6e877af352)~~
|
||||
|
||||
- ~~把 ListViewModel 的数据结构类型转换方式换为抽象方法,让继承的类实现,避免出现无响应的问题~~
|
||||
|
||||
- ~~rxjava2 如果接口返回为空 会发生异常:java.lang.NullPointerException: Null is not a valid element (答案编辑) 解决方法->com.gh.gamecenter.retrofit.Response~~
|
||||
- constraintLayout 1.1.2 导致布局出现异常(问题编辑标签选择弹窗)
|
||||
|
||||
- 搞清楚 GameManager 的用途,看能不能去掉
|
||||
- 重构一下 MainActivity
|
||||
409
app/build.gradle
409
app/build.gradle
@ -1,342 +1,127 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
apply plugin: 'org.jetbrains.kotlin.android.extensions'
|
||||
apply plugin: 'kotlin-android' // kotlin
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
// apkChannelPackage
|
||||
apply plugin: 'channel'
|
||||
|
||||
apply from: 'tinker-support.gradle'
|
||||
|
||||
android {
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
// jumboMode = true
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
compileSdkVersion 21
|
||||
buildToolsVersion "23.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.gh.gamecenter"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 21
|
||||
versionCode 17
|
||||
versionName "2.2"
|
||||
|
||||
multiDexEnabled true
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = [eventBusIndex: 'com.gh.EventBusIndex']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只支持两种架构,减少apk大小,有疑问请参考:
|
||||
* https://developer.android.com/ndk/guides/abis.html
|
||||
* http://allenfeng.com/2016/11/06/what-you-should-know-about-android-abi-and-so/
|
||||
* (为了性能考虑,armeabi可以考虑替换成armeabi-v7a[需要先收集用户设备情况])
|
||||
*/
|
||||
ndk {
|
||||
abiFilters "armeabi", "x86"
|
||||
}
|
||||
|
||||
// 由于app只针对中文用户,所以仅保留zh资源,其他删掉
|
||||
resConfigs "zh"
|
||||
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode rootProject.ext.versionCode
|
||||
versionName rootProject.ext.versionName
|
||||
applicationId rootProject.ext.applicationId
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-fresco.txt'
|
||||
|
||||
/**
|
||||
* All third-party appid/appkey
|
||||
*/
|
||||
buildConfigField "String", "WECHAT_APPID", "\"${WECHAT_APPID}\""
|
||||
buildConfigField "String", "WECHAT_SECRET", "\"${WECHAT_SECRET}\""
|
||||
buildConfigField "String", "TENCENT_APPID", "\"${TENCENT_APPID}\""
|
||||
buildConfigField "String", "WEIBO_APPKEY", "\"${WEIBO_APPKEY}\""
|
||||
buildConfigField "String", "MTA_APPKEY", "\"${MTA_APPKEY}\""
|
||||
buildConfigField "String", "TD_APPID", "\"${TD_APPID}\""
|
||||
|
||||
// 默认的渠道
|
||||
// manifestPlaceholders = [CHANNEL_VALUE: "GH_TEST"]
|
||||
}
|
||||
|
||||
// gradle 2.2以上默认同时启用v1和v2(优先用于Android N)
|
||||
/**
|
||||
* 签名设置
|
||||
*/
|
||||
signingConfigs {
|
||||
debug {
|
||||
v1SigningEnabled true
|
||||
v2SigningEnabled true
|
||||
}
|
||||
release {
|
||||
v1SigningEnabled true
|
||||
v2SigningEnabled true
|
||||
storeFile file("gh.keystore")
|
||||
keyAlias "gh.keystore"
|
||||
keyPassword "20150318"
|
||||
storePassword "20150318"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
debuggable true
|
||||
minifyEnabled false
|
||||
zipAlignEnabled false
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
buildConfigField "String", "EXPOSURE_REPO", "\"test\""
|
||||
buildConfigField "String", "EXPOSURE_VERSION", "\"E2\""
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
release {
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
zipAlignEnabled true
|
||||
shrinkResources true
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
|
||||
buildConfigField "String", "EXPOSURE_REPO", "\"exposure\""
|
||||
buildConfigField "String", "EXPOSURE_VERSION", "\"E2\""
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "nonsense"
|
||||
|
||||
/**
|
||||
* 多渠道打包,渠道请参考"channel.txt"文件,所有渠道值均通过java code设置
|
||||
* 多渠道打包
|
||||
*/
|
||||
productFlavors {
|
||||
// publish release host
|
||||
publish {
|
||||
dimension "nonsense"
|
||||
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "USER_HOST", "\"${USER_HOST}\""
|
||||
buildConfigField "String", "COMMENT_HOST", "\"${COMMENT_HOST}\""
|
||||
buildConfigField "String", "LIBAO_HOST", "\"${LIBAO_HOST}\""
|
||||
buildConfigField "String", "MESSAGE_HOST", "\"${MESSAGE_HOST}\""
|
||||
buildConfigField "String", "DATA_HOST", "\"${DATA_HOST}\""
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "MIPUSH_APPID", "\"${MIPUSH_APPID}\""
|
||||
buildConfigField "String", "MIPUSH_APPKEY", "\"${MIPUSH_APPKEY}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPID", "\"${MEIZUPUSH_APPID}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPKEY", "\"${MEIZUPUSH_APPKEY}\""
|
||||
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
|
||||
|
||||
}
|
||||
// internal test dev host
|
||||
internal {
|
||||
dimension "nonsense"
|
||||
versionNameSuffix "-debug"
|
||||
|
||||
buildConfigField "String", "API_HOST", "\"${DEV_API_HOST}\""
|
||||
buildConfigField "String", "USER_HOST", "\"${DEV_USER_HOST}\""
|
||||
buildConfigField "String", "COMMENT_HOST", "\"${DEV_COMMENT_HOST}\""
|
||||
buildConfigField "String", "LIBAO_HOST", "\"${DEV_LIBAO_HOST}\""
|
||||
buildConfigField "String", "MESSAGE_HOST", "\"${DEV_MESSAGE_HOST}\""
|
||||
buildConfigField "String", "DATA_HOST", "\"${DEV_DATA_HOST}\""
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${DEBUG_UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEBUG_UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "MIPUSH_APPID", "\"${DEBUG_MIPUSH_APPID}\""
|
||||
buildConfigField "String", "MIPUSH_APPKEY", "\"${DEBUG_MIPUSH_APPKEY}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPID", "\"${DEBUG_MEIZUPUSH_APPID}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPKEY", "\"${DEBUG_MEIZUPUSH_APPKEY}\""
|
||||
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${DEBUG_BUGLY_APPID}\""
|
||||
}
|
||||
GH_100 {}
|
||||
GH_101 {}
|
||||
GH_102 {}
|
||||
GH_103 {}
|
||||
GH_104 {}
|
||||
GH_106 {}
|
||||
GH_107 {}
|
||||
GH_108 {}
|
||||
GH_109 {}
|
||||
GH_110 {}
|
||||
GH_111 {}
|
||||
GH_113 {}
|
||||
GH_114 {}
|
||||
GH_115 {}
|
||||
GH_116 {}
|
||||
GH_117 {}
|
||||
GH_118 {}
|
||||
GH_119 {}
|
||||
GH_120 {}
|
||||
GH_121 {}
|
||||
GH_123 {}
|
||||
GH_127 {}
|
||||
GH_200 {}
|
||||
GH_201 {}
|
||||
GH_202 {}
|
||||
GH_203 {}
|
||||
GH_204 {}
|
||||
GH_205 {}
|
||||
GH_222 {}
|
||||
GH_307 {}
|
||||
GH_TEST {}
|
||||
}
|
||||
|
||||
// productFlavors.all { flavor ->
|
||||
// flavor.manifestPlaceholders = [CHANNEL_VALUE: name]//命令 gradlew assembleRelease
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// apkChannelPackage
|
||||
channel {
|
||||
//多渠道包的输出目录,默认为new File(project.buildDir,"channel")
|
||||
baseOutputDir = new File(project.buildDir, "channel")
|
||||
//多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}
|
||||
apkNameFormat = '${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
|
||||
}
|
||||
|
||||
rebuildChannel {
|
||||
// baseDebugApk = 已有Debug APK
|
||||
// baseReleaseApk = 已有Release APK
|
||||
// //默认为new File(project.buildDir, "rebuildChannel/debug")
|
||||
// debugOutputDir = Debug渠道包输出目录
|
||||
// //默认为new File(project.buildDir, "rebuildChannel/release")
|
||||
// releaseOutputDir = Release渠道包输出目录
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs 'libs/aars'
|
||||
productFlavors.all { flavor ->
|
||||
flavor.manifestPlaceholders = [CHANNEL_VALUE: name]//命令 gradlew assembleRelease
|
||||
}
|
||||
}
|
||||
|
||||
//butterknife
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
|
||||
}
|
||||
}
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakcanary}"
|
||||
debugImplementation "com.facebook.stetho:stetho:${stetho}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stetho}"
|
||||
debugImplementation "com.squareup.okhttp3:logging-interceptor:${okHttp}"
|
||||
|
||||
implementation "com.android.support:multidex:${multidex}"
|
||||
implementation "com.android.support:design:${androidSupport}"
|
||||
implementation "com.android.support:support-v4:${androidSupport}"
|
||||
implementation "com.android.support:appcompat-v7:${androidSupport}"
|
||||
implementation "com.android.support:cardview-v7:${androidSupport}"
|
||||
implementation "com.android.support:support-annotations:${androidSupport}"
|
||||
implementation "com.android.support:percent:${androidSupport}"
|
||||
implementation "com.android.support.constraint:constraint-layout:${constraintLayout}"
|
||||
implementation "com.kyleduo.switchbutton:library:${switchButton}"
|
||||
|
||||
implementation "com.facebook.fresco:fresco:${fresco}"
|
||||
implementation "com.facebook.fresco:animated-gif:${fresco}"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:${okHttp}"
|
||||
|
||||
implementation "com.leon.channel:helper:${apkChannelPackage}"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:${retrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-gson:${retrofit}" // include gson 2.7
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${retrofit}"
|
||||
|
||||
implementation "com.j256.ormlite:ormlite-android:${ormlite}"
|
||||
implementation "com.j256.ormlite:ormlite-core:${ormlite}"
|
||||
|
||||
implementation "com.jakewharton:butterknife:${butterKnife}"
|
||||
kapt "com.jakewharton:butterknife-compiler:${butterKnife}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||
|
||||
implementation "org.greenrobot:eventbus:${eventbus}"
|
||||
kapt "org.greenrobot:eventbus-annotation-processor:${eventbusApt}"
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:${rxJava2}"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:${rxAndroid2}"
|
||||
implementation "com.jakewharton.rxbinding2:rxbinding:${rxBinding2}"
|
||||
|
||||
implementation "com.google.zxing:core:${zxing}"
|
||||
implementation "com.google.zxing:android-core:${zxing}"
|
||||
|
||||
implementation "com.daimajia.swipelayout:library:${swipeLayout}"
|
||||
implementation("cn.trinea.android.view.autoscrollviewpager:android-auto-scroll-view-pager:${autoScrollViewPager}") {
|
||||
exclude module: 'support-v4'
|
||||
}
|
||||
|
||||
implementation "com.sina.weibo.sdk:core:${weiboSDK}"
|
||||
|
||||
// bugly with tinker support
|
||||
implementation "com.tencent.bugly:crashreport_upgrade:${buglyTinkerSupport}"
|
||||
|
||||
implementation "pub.devrel:easypermissions:${easypermissions}"
|
||||
// mvvm
|
||||
implementation "android.arch.lifecycle:runtime:${archLifecycleVersion}"
|
||||
kapt "android.arch.lifecycle:compiler:${archLifecycleVersion}"
|
||||
implementation "android.arch.lifecycle:extensions:${archLifecycleVersion}"
|
||||
implementation "android.arch.persistence.room:runtime:${archRoomVersion}"
|
||||
kapt "android.arch.persistence.room:compiler:${archRoomVersion}"
|
||||
|
||||
implementation 'com.google.android:flexbox:0.2.2'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
kapt 'com.android.databinding:compiler:3.1.3'
|
||||
|
||||
implementation 'com.contrarywind:Android-PickerView:4.1.3'
|
||||
|
||||
implementation "com.scwang.smartrefresh:SmartRefreshLayout:${smartRefreshLayout}"
|
||||
implementation "net.cachapa.expandablelayout:expandablelayout:${expandableLayout}"
|
||||
|
||||
// 用于比较 versionName 是大于小于或等于
|
||||
implementation "com.g00fy2:versioncompare:${versioncompare}"
|
||||
|
||||
implementation "top.zibin:Luban:${luban}"
|
||||
|
||||
// for video streaming
|
||||
implementation "cn.jzvd:jiaozivideoplayer:${jiaoziVideoView}"
|
||||
implementation "com.danikula:videocache:${videoCache}"
|
||||
|
||||
implementation "com.llew.huawei:verifier:1.0.6"
|
||||
|
||||
implementation 'com.ethanhua:skeleton:1.1.1'
|
||||
implementation 'io.supercharge:shimmerlayout:2.1.0'
|
||||
|
||||
implementation project(':libraries:gid')
|
||||
implementation project(':libraries:LGLibrary')
|
||||
implementation project(':libraries:MTA')
|
||||
implementation project(':libraries:QQShare')
|
||||
implementation project(':libraries:TalkingData')
|
||||
implementation project(':libraries:UmengPush')
|
||||
implementation project(':libraries:WechatShare')
|
||||
implementation project(':libraries:LogHub')
|
||||
implementation project(':libraries:im')
|
||||
}
|
||||
File propFile = file('sign.properties')
|
||||
if (propFile.exists()) {
|
||||
Properties props = new Properties()
|
||||
props.load(new FileInputStream(propFile))
|
||||
|
||||
if (props.containsKey('keyAlias') && props.containsKey('keyPassword') &&
|
||||
props.containsKey('storeFile') && props.containsKey('storePassword')) {
|
||||
android.signingConfigs {
|
||||
// debug 不要使用正式签名,这样tinker才不会打补丁。
|
||||
debug {
|
||||
keyAlias props.get('keyAlias')
|
||||
keyPassword props.get('keyPassword')
|
||||
storeFile file(props.get('storeFile'))
|
||||
storePassword props.get('storePassword')
|
||||
}
|
||||
release {
|
||||
keyAlias props.get('keyAlias')
|
||||
keyPassword props.get('keyPassword')
|
||||
storeFile file(props.get('storeFile'))
|
||||
storePassword props.get('storePassword')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
} else {
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
|
||||
// 用于测试读取 META-INF 里的 JSON 的代码
|
||||
//task generateMetaJson {
|
||||
// def resDir = new File(buildDir, 'generated/FILES_FOR_META_INF/')
|
||||
// def destDir = new File(resDir, 'META-INF/')
|
||||
// // Add resDir as a resource directory so that it is automatically included in the APK.
|
||||
// android {
|
||||
// sourceSets {
|
||||
// main.resources {
|
||||
// srcDir resDir
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// doLast {
|
||||
// if (!destDir.exists()) destDir.mkdirs()
|
||||
// copy {
|
||||
// into destDir
|
||||
// from new File('generated/FILES_FOR_META_INF/META-INF/halo_skip.json')
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//// Specify when put_files_in_META_INF should run
|
||||
//project.afterEvaluate {
|
||||
// tasks.findAll { task ->
|
||||
// task.name.startsWith('merge') && task.name.endsWith('Resources')
|
||||
// }.each { t -> t.dependsOn generateMetaJson }
|
||||
//}
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile 'com.android.support:appcompat-v7:21.0.0'
|
||||
compile 'com.android.support:cardview-v7:21.0.0'
|
||||
// fresco图片框架
|
||||
compile 'com.facebook.fresco:fresco:0.12.0'
|
||||
compile 'com.facebook.fresco:animated-gif:0.12.0'
|
||||
// Retrofit2所需要的包
|
||||
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
|
||||
// okhttp
|
||||
compile 'com.squareup.okhttp3:okhttp:3.2.0'
|
||||
// ConverterFactory的Gson依赖包
|
||||
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
|
||||
// ConverterFactory的String依赖包
|
||||
compile 'com.squareup.retrofit2:converter-scalars:2.0.0-beta4'
|
||||
// ConverterFactory的RxJava依赖包
|
||||
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'
|
||||
// gson
|
||||
compile 'com.google.code.gson:gson:2.8.0'
|
||||
// OrmLite数据库
|
||||
compile 'com.j256.ormlite:ormlite-android:5.0'
|
||||
compile 'com.j256.ormlite:ormlite-core:5.0'
|
||||
// butterknife
|
||||
compile 'com.jakewharton:butterknife:8.4.0'
|
||||
apt 'com.jakewharton:butterknife-compiler:8.4.0'
|
||||
// RxJava && RxAndroid
|
||||
compile 'io.reactivex:rxandroid:1.1.0'
|
||||
compile 'io.reactivex:rxjava:1.1.0'
|
||||
// RxBinding
|
||||
compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
|
||||
// compile 'com.jakewharton.rxbinding:rxbinding-appcompat-v7:0.3.0'
|
||||
// compile 'com.jakewharton.rxbinding:rxbinding-design:0.3.0'
|
||||
}
|
||||
BIN
app/libs/EventBus-2.4.0.jar
Normal file
BIN
app/libs/EventBus-2.4.0.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/libs/MiPush_SDK_Client_2_2_18.jar
Normal file
BIN
app/libs/MiPush_SDK_Client_2_2_18.jar
Normal file
Binary file not shown.
BIN
app/libs/MobCommons-2016.0426.1819.jar
Normal file
BIN
app/libs/MobCommons-2016.0426.1819.jar
Normal file
Binary file not shown.
BIN
app/libs/MobTools-2016.0426.1819.jar
Normal file
BIN
app/libs/MobTools-2016.0426.1819.jar
Normal file
Binary file not shown.
BIN
app/libs/TalkingDataAnalytics_V1.2.79.jar
Normal file
BIN
app/libs/TalkingDataAnalytics_V1.2.79.jar
Normal file
Binary file not shown.
BIN
app/libs/mid-sdk-2.3.jar
Normal file
BIN
app/libs/mid-sdk-2.3.jar
Normal file
Binary file not shown.
BIN
app/libs/mta-android-stat-sdk-2.2.0_20160504.jar
Normal file
BIN
app/libs/mta-android-stat-sdk-2.2.0_20160504.jar
Normal file
Binary file not shown.
BIN
app/libs/open_sdk_r5756.jar
Normal file
BIN
app/libs/open_sdk_r5756.jar
Normal file
Binary file not shown.
BIN
app/libs/utdid4all-1.0.4.jar
Normal file
BIN
app/libs/utdid4all-1.0.4.jar
Normal file
Binary file not shown.
BIN
app/libs/weiboSDKCore_3.1.4.jar
Normal file
BIN
app/libs/weiboSDKCore_3.1.4.jar
Normal file
Binary file not shown.
@ -1,21 +0,0 @@
|
||||
# Keep our interfaces so they can be used by other ProGuard rules.
|
||||
# See http://sourceforge.net/p/proguard/bugs/466/
|
||||
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
|
||||
|
||||
# Do not strip any method/class that is annotated with @DoNotStrip
|
||||
-keep @com.facebook.common.internal.DoNotStrip class *
|
||||
-keepclassmembers class * {
|
||||
@com.facebook.common.internal.DoNotStrip *;
|
||||
}
|
||||
|
||||
# Keep native methods
|
||||
-keepclassmembers class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-dontwarn okio.**
|
||||
-dontwarn com.squareup.okhttp.**
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn com.android.volley.toolbox.**
|
||||
-dontwarn com.facebook.infer.**
|
||||
@ -1,11 +1,8 @@
|
||||
# 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.
|
||||
# in C:\Android\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
@ -17,4 +14,4 @@
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
#}
|
||||
@ -1,225 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Android\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# 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 *;
|
||||
#}
|
||||
|
||||
#--------- remove logs start ----------------
|
||||
-assumenosideeffects class com.lightgame.config.CommonDebug {
|
||||
private static String getLogTag(...);
|
||||
private static String getMethodName();
|
||||
public static void logMethodName(...);
|
||||
public static void logParams(...);
|
||||
public static void logFields(...);
|
||||
public static void logMethodWithParams(...);
|
||||
}
|
||||
|
||||
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
|
||||
|
||||
#-dontoptimize
|
||||
#--------- remove logs end ----------------
|
||||
|
||||
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
|
||||
-dontwarn InnerClasses
|
||||
|
||||
# OrmLite uses reflection
|
||||
-keep class com.j256.**
|
||||
-keepclassmembers class com.j256.** { *; }
|
||||
-keep enum com.j256.**
|
||||
-keepclassmembers enum com.j256.** { *; }
|
||||
-keep interface com.j256.**
|
||||
-keepclassmembers interface com.j256.** { *; }
|
||||
-dontwarn com.j256.**
|
||||
|
||||
#okhttp3
|
||||
-dontwarn com.squareup.okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-keep class com.squareup.okhttp3.** { *;}
|
||||
|
||||
# stetho
|
||||
-keep class com.facebook.stetho.** { *; }
|
||||
-dontwarn com.facebook.stetho.**
|
||||
|
||||
# Retrofit 2.2
|
||||
# Platform calls Class.forName on types which do not exist on Android to determine platform.
|
||||
-dontnote retrofit2.Platform
|
||||
# Platform used when running on Java 8 VMs. Will not be used at runtime.
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
# Retain generic type information for use by reflection by converters and adapters.
|
||||
-keepattributes Signature
|
||||
# Retain declared checked exceptions for use by a Proxy instance.
|
||||
-keepattributes Exceptions
|
||||
|
||||
# Retrofit 2.X
|
||||
## https://square.github.io/retrofit/ ##
|
||||
|
||||
-dontwarn retrofit2.**
|
||||
-keep class retrofit2.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
|
||||
# rxjava
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.ImmediateScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.TestScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static ** test();
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||
long producerIndex;
|
||||
long consumerIndex;
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
||||
long producerNode;
|
||||
long consumerNode;
|
||||
}
|
||||
-dontwarn rx.internal.util.**
|
||||
|
||||
## AutoScrollViewPager
|
||||
-keep class cn.trinea.android.** { *; }
|
||||
-keepclassmembers class cn.trinea.android.** { *; }
|
||||
-dontwarn cn.trinea.android.**
|
||||
|
||||
## butterknife
|
||||
# Retain generated class which implement Unbinder.
|
||||
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
|
||||
#
|
||||
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
|
||||
## is used to reflectively look up the generated ViewBinding.
|
||||
#-keep class butterknife.*
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
|
||||
|
||||
-dontwarn butterknife.internal.**
|
||||
-keep class **$$ViewInjector { *; }
|
||||
-keepnames class * { @butterknife.InjectView *;}
|
||||
-dontwarn butterknife.Views$InjectViewProcessor
|
||||
-dontwarn com.gc.materialdesign.views.**
|
||||
|
||||
# eventbus
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class ** {
|
||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||
}
|
||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||
|
||||
# Only required if you use AsyncExecutor
|
||||
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
|
||||
<init>(java.lang.Throwable);
|
||||
}
|
||||
|
||||
# weiboSdk
|
||||
-keep class com.sina.weibo.sdk.** { *; }
|
||||
-dontwarn android.webkit.WebView
|
||||
-dontwarn android.webkit.WebViewClient
|
||||
|
||||
# app models
|
||||
-keep class com.gh.common.view.** {*;}
|
||||
-keep class com.gh.gamecenter.db.info.** {*;}
|
||||
-keep class com.gh.gamecenter.entity.** {*;}
|
||||
-keep class com.gh.gamecenter.qa.entity.** {*;}
|
||||
-keep class com.gh.gamecenter.retrofit.** {*;}
|
||||
-keep class com.gh.gamecenter.eventbus.** {*;}
|
||||
-keep class * extends rx.Subscriber
|
||||
|
||||
#---------------------------------webview------------------------------------
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
|
||||
public boolean *(android.webkit.WebView, java.lang.String);
|
||||
}
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String);
|
||||
}
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
-keepclassmembers enum * { *; }
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
# ------ bugly ---------
|
||||
-dontwarn com.tencent.bugly.**
|
||||
-keep public class com.tencent.bugly.**{*;}
|
||||
|
||||
# easypermission
|
||||
-keepclassmembers class * {
|
||||
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
|
||||
}
|
||||
|
||||
# 重命名文件为SourceFile,再配合mapping符号表,可以拿到真实的类名
|
||||
-renamesourcefileattribute SourceFile
|
||||
# 保留源文件行号
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
-ignorewarnings
|
||||
|
||||
-keep @android.support.annotation.Keep class *
|
||||
-keepclassmembers class ** {
|
||||
@android.support.annotation.Keep *;
|
||||
}
|
||||
|
||||
-keep class com.gh.loghub.** { *; }
|
||||
|
||||
### greenDAO 3
|
||||
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
|
||||
public static java.lang.String TABLENAME;
|
||||
}
|
||||
-keep class **$Properties
|
||||
-keep class org.greenrobot.greendao.** { *; }
|
||||
# If you do not use SQLCipher:
|
||||
-dontwarn org.greenrobot.greendao.database.**
|
||||
# If you do not use RxJava:
|
||||
-dontwarn rx.**
|
||||
-dontwarn org.greenrobot.greendao.rx.**
|
||||
-dontwarn org.greenrobot.greendao.**
|
||||
|
||||
### fastJson
|
||||
-dontwarn com.alibaba.fastjson.**
|
||||
-keep class com.alibaba.fastjson.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Annotation
|
||||
|
||||
### 广点通
|
||||
-dontwarn com.qq.gdt.action.**
|
||||
-keep class com.qq.gdt.action.** {*;}
|
||||
@ -1,4 +0,0 @@
|
||||
storeFile=gh.keystore
|
||||
storePassword=20150318
|
||||
keyAlias=gh.keystore
|
||||
keyPassword=20150318
|
||||
@ -0,0 +1,13 @@
|
||||
package com.gh.gamecenter;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package com.gh.gamecenter;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
|
||||
/**
|
||||
* @author CsHeng
|
||||
* @Date 03/09/2017
|
||||
* @Time 4:34 PM
|
||||
*/
|
||||
|
||||
public class Injection {
|
||||
|
||||
public static boolean appInit(Application application) {
|
||||
|
||||
// init leakcanary
|
||||
if (LeakCanary.isInAnalyzerProcess(application)) {
|
||||
// This process is dedicated to LeakCanary for heap analysis.
|
||||
// You should not init your app in this process.
|
||||
return false;
|
||||
}
|
||||
LeakCanary.install(application);
|
||||
|
||||
// init stetho
|
||||
Stetho.initializeWithDefaults(application);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static OkHttpClient.Builder provideRetrofitBuilder() {
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
builder.addNetworkInterceptor(interceptor);
|
||||
builder.addNetworkInterceptor(new StethoInterceptor());
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,472 +1,309 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android = "http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools = "http://schemas.android.com/tools"
|
||||
package = "com.gh.gamecenter" >
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter" >
|
||||
|
||||
<!-- 允许应用程序访问网络连接 -->
|
||||
<uses-permission android:name = "android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- 允许应用程序写入外部存储,如SD卡上写文件 -->
|
||||
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<!-- 允许应用程序读取扩展存储器 -->
|
||||
<uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- 允许挂载和反挂载文件系统可移动存储 -->
|
||||
<uses-permission android:name = "android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
|
||||
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
|
||||
<!-- 允许应用程序访问Wi-Fi网络状态信息 -->
|
||||
<uses-permission android:name = "android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<!-- 允许应用程序获取网络信息状态 -->
|
||||
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- 允许应用程序读取电话状态 -->
|
||||
<uses-permission android:name = "android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<!-- 允许应用程序获取当前或最近运行的应用 -->
|
||||
<uses-permission android:name = "android.permission.GET_TASKS" />
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
<!-- 允许访问振动设备 -->
|
||||
<uses-permission android:name = "android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- 允许应用程序通过WiFi或移动基站获取粗略的位置信息 -->
|
||||
<uses-permission android:name = "android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<!-- 允许应用程序通过GPS获取精确的位置信息 -->
|
||||
<uses-permission android:name = "android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<!-- 允许应用程序改变Wi-Fi连接状态 -->
|
||||
<uses-permission android:name = "android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<!-- 允许应用程序管理AccountManager中的账户列表 -->
|
||||
<uses-permission android:name = "android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<!-- 允许应用程序访问GMail账户列表 -->
|
||||
<uses-permission android:name = "android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<!-- 允许应用程序连接配对过的蓝牙设备 -->
|
||||
<uses-permission android:name = "android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<!-- 允许应用程序管理蓝牙,搜索和配对新的蓝牙设备 -->
|
||||
<uses-permission android:name = "android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<!-- 允许应用程序打开系统窗口,显示其他应用程序 -->
|
||||
<uses-permission android:name = "android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<!-- 修改系统设置的权限 -->
|
||||
<uses-permission android:name = "android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<!-- 小米推送需要的权限 -->
|
||||
<uses-permission android:name="com.gh.gamecenter.permission.MIPUSH_RECEIVE" />
|
||||
|
||||
<!-- bugly with tinker -->
|
||||
<uses-permission android:name = "android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name = "android.permission.INTERNET" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name = "android.permission.READ_LOGS" />
|
||||
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name = "android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<permission
|
||||
android:name="com.gh.gamecenter.permission.MIPUSH_RECEIVE"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity = "true"
|
||||
android:largeScreens = "true"
|
||||
android:normalScreens = "true"
|
||||
android:resizeable = "true"
|
||||
android:smallScreens = "true" />
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:resizeable="true"
|
||||
android:smallScreens="true" />
|
||||
|
||||
<!--android:largeHeap = "true"-->
|
||||
<application
|
||||
android:name = "com.halo.assistant.TinkerApp"
|
||||
android:allowBackup = "true"
|
||||
android:icon = "@drawable/logo"
|
||||
android:label = "@string/app_name"
|
||||
android:resizeableActivity = "true"
|
||||
android:theme = "@style/AppCompatTheme.APP"
|
||||
tools:targetApi = "n" >
|
||||
android:name="com.gh.base.AppController"
|
||||
android:icon="@drawable/logo"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppThemeNormal" >
|
||||
<!-- TalkingData -->
|
||||
<meta-data
|
||||
android:name="TD_APP_ID"
|
||||
android:value="81DB144D555386A38A70B833537EC256" />
|
||||
<meta-data
|
||||
android:name="TD_CHANNEL_ID"
|
||||
android:value="${CHANNEL_VALUE}"/>
|
||||
<!--android:value="${CHANNEL_VALUE}"-->
|
||||
|
||||
<!--android:launchMode = "singleTask"-->
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SplashScreenActivity"
|
||||
android:configChanges = "keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation = "portrait"
|
||||
android:theme = "@style/AppGuideTheme" >
|
||||
<intent-filter >
|
||||
<action android:name = "android.intent.action.MAIN" />
|
||||
|
||||
<category android:name = "android.intent.category.LAUNCHER" />
|
||||
</intent-filter >
|
||||
|
||||
</activity >
|
||||
<!-- MTA -->
|
||||
<meta-data
|
||||
android:name="TA_APPKEY"
|
||||
android:value="APV567FTBS7J"/>
|
||||
<meta-data
|
||||
android:name="InstallChannel"
|
||||
android:value="${CHANNEL_VALUE}"/>
|
||||
<!--android:value="${CHANNEL_VALUE}"-->
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MainActivity"
|
||||
android:launchMode = "singleTask"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateAlwaysHidden|adjustResize" />
|
||||
android:name="com.gh.gamecenter.SplashScreenActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme_Guide"
|
||||
android:uiOptions="splitActionBarWhenNarrow" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.DownloadManagerActivity"
|
||||
android:launchMode = "singleTask"
|
||||
android:screenOrientation = "portrait" />
|
||||
android:name="com.gh.gamecenter.MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.DownloadManagerActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ViewImageActivity"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SearchActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateVisible" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.GameDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.NewsDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SettingActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SuggestionActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateVisible" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ConcernActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SubjectActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.PluginActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.NewsSearchActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.GameNewsActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.CropImageActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.WebActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ShareCardPicActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ShareCardActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.MessageDetailActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity android:name="com.gh.gamecenter.SkipActivity"
|
||||
android:theme="@android:style/Theme.Translucent">
|
||||
<intent-filter>
|
||||
<data android:scheme="ghzhushou"/>
|
||||
|
||||
<!--android:theme = "@android:style/Theme.Black.NoTitleBar.Fullscreen" 退出时屏幕抖动 -->
|
||||
<activity android:name = "com.gh.gamecenter.ViewImageActivity" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SearchActivity"
|
||||
android:configChanges = "keyboardHidden"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.NewsDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SettingActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ConcernActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.subject.SubjectActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.NewsSearchActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.GameNewsActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CropImageActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.WebActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareCardPicActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareCardActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MessageDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.LibaoActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.LibaoDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareGhWfifActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareGhActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CleanApkActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.KcSelectGameActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ChooseReceiverActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ReceiverWaitingActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.FileSenderActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.FileReceiverActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SelectUserIconActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.AboutActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CommentDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.GameDetailActivity"
|
||||
android:configChanges = "orientation|screenSize|keyboardHidden"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SuggestSelectActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SuggestionActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.VoteActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateAlwaysHidden|adjustResize" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ToolBoxActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
android:name="com.mob.tools.MobUIShell"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize" >
|
||||
<intent-filter>
|
||||
<data android:scheme="tencent100371282" />
|
||||
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.WeiBoShareActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
|
||||
android:name="com.gh.gamecenter.wxapi.WXEntryActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="sdksample"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- QQ 分享 -->
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.InstallActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
android:name="com.tencent.tauth.AuthActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tencent1104659243" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name = ".category.CategoryDirectoryActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
android:name="com.tencent.connect.common.AssistActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:screenOrientation="behind"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
<!--微博分享-->
|
||||
<activity
|
||||
android:name = ".category.CategoryListActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
android:name="com.sina.weibo.sdk.component.WeiboSdkBrowser"
|
||||
android:configChanges="keyboardHidden|orientation"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="false" >
|
||||
</activity>
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.LoginActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
android:name="com.gh.gamecenter.WeiBoShareActivity"
|
||||
android:configChanges="keyboardHidden|orientation"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="com.sina.weibo.sdk.action.ACTION_SDK_REQ_ACTIVITY" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.UserInfoActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.UserRegionActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CollectionActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MessageActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.UserInfoEditActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.KaiFuActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.search.AskSearchActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.answer.detail.AnswerDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.questions.detail.QuestionsDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = ".qa.answer.fold.AnswerFoldActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.answer.edit.AnswerEditActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ConcernInfoActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.InfoActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MessageKeFuActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.select.CommunitiesSelectActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.subject.CommunitySubjectActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MessageInviteActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MessageVoteActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = ".qa.questions.invite.QuestionsInviteActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.myqa.MyAskActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.column.order.AskTabOrderActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.questions.edit.QuestionEditActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.kaifu.add.AddKaiFuActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.kaifu.patch.PatchKaifuActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.BlockActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.column.detail.AskColumnDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.NetworkDiagnosisActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.personalhome.fans.FansActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.personalhome.followers.FollowersActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.PersonalHomeActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.personalhome.answer.PersonalAnswerActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.personalhome.question.PersonalQuestionActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.article.edit.ArticleEditActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.article.MyArticleActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.article.draft.ArticleDraftActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.article.detail.ArticleDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.draft.CommunityDraftWrapperActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.gamedetail.rating.edit.RatingEditActivity"
|
||||
android:windowSoftInputMode = "stateVisible"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.questions.edit.manager.HistoryDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.questions.edit.manager.HistoryActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.qa.comment.CommentActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:theme = "@style/Theme.Transparent"
|
||||
android:windowSoftInputMode = "adjustNothing" />
|
||||
|
||||
<!-- 使用小米/华为推送弹窗功能提高推送成功率-->
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.PushProxyActivity"
|
||||
android:exported = "true"
|
||||
android:theme = "@android:style/Theme.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SkipActivity"
|
||||
android:theme = "@style/Theme.AppCompat.Light.Fullscreen.Transparent" >
|
||||
<intent-filter >
|
||||
<data android:scheme = "ghzhushou" />
|
||||
|
||||
<category android:name = "android.intent.category.DEFAULT" />
|
||||
<action android:name = "android.intent.action.VIEW" />
|
||||
<category android:name = "android.intent.category.BROWSABLE" />
|
||||
</intent-filter >
|
||||
</activity >
|
||||
|
||||
<provider
|
||||
android:name = "android.support.v4.content.FileProvider"
|
||||
android:authorities = "${applicationId}"
|
||||
android:exported = "false"
|
||||
android:grantUriPermissions = "true" >
|
||||
<meta-data
|
||||
android:name = "android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource = "@xml/provider_paths" />
|
||||
</provider >
|
||||
<receiver android:name="com.gh.gamecenter.receiver.InstallAndUninstallReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name = "com.gh.gamecenter.receiver.DownloadReceiver"
|
||||
android:exported = "false" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.DOWNLOAD" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
android:name="com.gh.gamecenter.receiver.NotificationReceiver"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="com.gh.gamecenter.NOTIFICATION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name = "com.gh.gamecenter.receiver.InstallReceiver"
|
||||
android:exported = "false" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.INSTALL" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
|
||||
android:name="com.gh.gamecenter.receiver.DownloadReceiver"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="com.gh.gamecenter.DOWNLOAD" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name = "com.gh.gamecenter.receiver.ActivitySkipReceiver"
|
||||
android:exported = "true" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.ACTIVITYSKIP" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
|
||||
<receiver android:name = "com.gh.gamecenter.receiver.UmengMessageReceiver" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.UMENG" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
|
||||
<!--魅族push应用定义消息receiver声明 -->
|
||||
<receiver android:name = "com.gh.gamecenter.receiver.MeizuPushReceiver" >
|
||||
<intent-filter >
|
||||
<!-- 接收push消息 -->
|
||||
<action android:name = "com.meizu.flyme.push.intent.MESSAGE" />
|
||||
<!-- 接收register消息 -->
|
||||
<action android:name = "com.meizu.flyme.push.intent.REGISTER.FEEDBACK" />
|
||||
<!-- 接收unregister消息-->
|
||||
<action android:name = "com.meizu.flyme.push.intent.UNREGISTER.FEEDBACK" />
|
||||
<!-- 兼容低版本Flyme3推送服务配置 -->
|
||||
<action android:name = "com.meizu.c2dm.intent.REGISTRATION" />
|
||||
<action android:name = "com.meizu.c2dm.intent.RECEIVE" />
|
||||
|
||||
<category android:name = "${applicationId}" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
|
||||
android:name="com.gh.gamecenter.receiver.InstallReceiver"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="com.gh.gamecenter.INSTALL" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="com.gh.gamecenter.receiver.NetworkStateReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name = "com.gh.common.im.ImReceiver"
|
||||
android:enabled = "true" >
|
||||
<intent-filter android:priority = "2147483647" >
|
||||
<action android:name = "com.gh.im" />
|
||||
<action android:name = "action_finish" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
android:name="com.xiaomi.push.service.receivers.PingReceiver"
|
||||
android:exported="false"
|
||||
android:process=":pushservice" >
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.push.PING_TIMER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name="com.gh.base.GHPushMessageReceiver"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.mipush.ERROR" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
|
||||
<service android:name = "com.gh.base.GHUmengNotificationService" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name="com.gh.gamecenter.receiver.ActivitySkipReceiver"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="com.gh.gamecenter.ACTIVITYSKIP" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!--<service android:name = "com.gh.gamecenter.statistics.AppStaticService" />-->
|
||||
<service
|
||||
android:name="com.gh.download.DownloadService" />
|
||||
<service
|
||||
android:name="com.xiaomi.push.service.XMPushService"
|
||||
android:enabled="true"
|
||||
android:process=":pushservice" />
|
||||
<service
|
||||
android:name="com.xiaomi.mipush.sdk.PushMessageHandler"
|
||||
android:enabled="true"
|
||||
android:exported="true" />
|
||||
<service
|
||||
android:name="com.xiaomi.mipush.sdk.MessageHandleService"
|
||||
android:enabled="true" />
|
||||
</application>
|
||||
|
||||
</application >
|
||||
|
||||
</manifest >
|
||||
</manifest>
|
||||
@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>光环助手</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
body {
|
||||
font: 100%/1.0 'Microsoft YaHei','Helvetica Neue',Helvetica,Arial,sans-serif;
|
||||
background-color: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
}
|
||||
|
||||
article {
|
||||
width:100%;
|
||||
max-width:720px;
|
||||
clear: both;
|
||||
margin: 0 auto;
|
||||
margin-top: 20%;
|
||||
text-align: center;
|
||||
margin-bottom:20%;
|
||||
}
|
||||
.title{margin-top: 4%;font-size:1.7em;color:#191919;text-align:center;}
|
||||
.info{margin-top: 18%;font-size:1.0em;color:#191919;line-height:1.3em;}
|
||||
.download {text-align: center;}
|
||||
.download a{font-size:1.8em;padding:0.2em; text-align:center;color:#ffffff;margin: 0 auto;width:56%;background-color:#2999f9;border-radius:8px; text-decoration:none;display:block;line-height:1.8em;}
|
||||
|
||||
@media only screen and (min-width: 1080px) {
|
||||
article {
|
||||
width:100%;
|
||||
max-width:720px;
|
||||
clear: both;
|
||||
margin: 0 auto;
|
||||
margin-top: 5%;
|
||||
text-align: center;
|
||||
margin-bottom:20%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
<img src="http://192.168.43.1:3100/image/gh_icon.png" width="28%">
|
||||
<p class="title">光环助手</p>
|
||||
<br class="info">乐于分享的人是最帅的^_^ </p>
|
||||
<div class="download">
|
||||
<a href="http://192.168.43.1:3100/download/ghzs.apk">免流量下载</a>
|
||||
</div>
|
||||
<p class="title"><font color="#9A9A9A" size="3em">仅限安卓系统 </font></p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
function requestContentFocus() {
|
||||
$('#editor').focus();
|
||||
}
|
||||
|
||||
function setupWhenContentEditable() {
|
||||
var editor = $('#editor');
|
||||
if (!editor[0].hasAttribute('contenteditable')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// paste 回调只会获取粘贴之前的光标位置,需要自己手动加上粘贴文本的长度,并保证粘贴的是纯文本
|
||||
editor.on('paste', function(e) {
|
||||
e.preventDefault();
|
||||
var text = (e.originalEvent || e).clipboardData.getData('text/plain');
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
if("" != text) {
|
||||
document.execCommand("insertHTML", false, text);
|
||||
} else {
|
||||
window.onPasteListener.onPaste();
|
||||
}
|
||||
});
|
||||
|
||||
requestContentFocus();
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
setupWhenContentEditable();
|
||||
});
|
||||
@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<link rel="stylesheet" type="text/css" href="normalize.css">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor" contenteditable="true"></div>
|
||||
<script type="text/javascript" src="zepto.min.js"></script>
|
||||
<script type="text/javascript" src="rich_editor.js"></script>
|
||||
<script type="text/javascript" src="content.js"></script>
|
||||
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/vanilla-lazyload/10.15.0/lazyload.min.js"></script>-->
|
||||
</body>
|
||||
</html>
|
||||
@ -1,40 +0,0 @@
|
||||
emoji_kf_1.png,:smile:
|
||||
emoji_kf_2.png,:smiley:
|
||||
emoji_kf_3.png,:laughing:
|
||||
emoji_kf_4.png,:blush:
|
||||
emoji_kf_5.png,:heart_eyes:
|
||||
emoji_kf_6.png,:smirk:
|
||||
emoji_kf_7.png,:flushed:
|
||||
emoji_kf_8.png,:kissing_heart:
|
||||
emoji_kf_9.png,:grin:
|
||||
emoji_kf_10.png,:wink:
|
||||
emoji_kf_11.png,:stuck_out_tongue_winking_eye:
|
||||
emoji_kf_12.png,:stuck_out_tongue_closed eyes:
|
||||
emoji_kf_13.png,:worried:
|
||||
emoji_kf_14.png,:sleeping:
|
||||
emoji_kf_15.png,:expressionless:
|
||||
emoji_kf_16.png,:sweat_smile:
|
||||
emoji_kf_17.png,:joy:
|
||||
emoji_kf_18.png,:cold_sweat:
|
||||
emoji_kf_19.png,:sob:
|
||||
emoji_kf_20.png,:angry:
|
||||
emoji_kf_21.png,:mask:
|
||||
emoji_kf_22.png,:scream:
|
||||
emoji_kf_23.png,:sunglasses:
|
||||
emoji_kf_24.png,:heart:
|
||||
emoji_kf_25.png,:broken_heart:
|
||||
emoji_kf_26.png,:star:
|
||||
emoji_kf_27.png,:anger:
|
||||
emoji_kf_28.png,:exclamation:
|
||||
emoji_kf_29.png,:question:
|
||||
emoji_kf_30.png,:zzz:
|
||||
emoji_kf_31.png,:thumbsup:
|
||||
emoji_kf_32.png,:thumbsdown:
|
||||
emoji_kf_33.png,:ok_hand:
|
||||
emoji_kf_34.png,:punch:
|
||||
emoji_kf_35.png,:yeah:
|
||||
emoji_kf_36.png,:clap:
|
||||
emoji_kf_37.png,:muscle:
|
||||
emoji_kf_38.png,:pray:
|
||||
emoji_kf_39.png,:skull:
|
||||
emoji_kf_40.png,:trollface:
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@ -1,5 +0,0 @@
|
||||
# This is a simple Microlog configuration file
|
||||
microlog.level=DEBUG
|
||||
microlog.appender=LogCatAppender;FileAppender
|
||||
microlog.formatter=PatternFormatter
|
||||
microlog.formatter.PatternFormatter.pattern=%c [%P] %m %T
|
||||
413
app/src/main/assets/normalize.css
vendored
413
app/src/main/assets/normalize.css
vendored
@ -1,413 +0,0 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,149 +1,30 @@
|
||||
9u=\u4e5d\u6e38
|
||||
4399=\u0034\u0033\u0039\u0039
|
||||
yyb=\u5e94\u7528\u5b9d
|
||||
xm=\u5c0f\u7c73
|
||||
dl=\u5f53\u4e50
|
||||
91=\u0039\u0031
|
||||
yw=\u76ca\u73a9
|
||||
gf-w=\u5b98\u65b9\u5916
|
||||
az=\u5b89\u667a
|
||||
oppo=\u006f\u0070\u0070\u006f
|
||||
wdj=\u8c4c\u8c46\u835a
|
||||
360=\u0033\u0036\u0030
|
||||
vivo=\u0076\u0069\u0076\u006f
|
||||
pps=\u0050\u0050\u0053
|
||||
hw=\u534e\u4e3a
|
||||
37=\u0033\u0037\u73a9
|
||||
baidu=\u767e\u5ea6
|
||||
ow=\u5076\u73a9
|
||||
gf=\u5b98\u65b9
|
||||
lenovo=\u8054\u60f3
|
||||
pptv=\u0050\u0050\u0054\u0056
|
||||
jf=\u673a\u950b
|
||||
mumayi=\u6728\u8682\u8681
|
||||
8868=\u0038\u0038\u0036\u0038
|
||||
19196=\u0031\u0039\u0031\u0039\u0036
|
||||
07073=\u0030\u0037\u0030\u0037\u0033
|
||||
gp=\u679c\u76d8
|
||||
mzw=\u62c7\u6307\u73a9
|
||||
af=\u5b89\u950b
|
||||
lb=\u730e\u5b9d
|
||||
ayx=\u963f\u6e38\u620f
|
||||
tt=\u0054\u0054
|
||||
xiongmao=\u718a\u732b\u73a9
|
||||
aq=\u5b89\u8da3
|
||||
ls=\u4e50\u89c6
|
||||
jl=\u91d1\u7acb
|
||||
lehh=\u4e50\u55e8\u55e8
|
||||
pyw=\u670b\u53cb\u73a9
|
||||
azsc=\u5b89\u5353\u5e02\u573a
|
||||
hyx=\u548c\u6e38\u620f
|
||||
aiyouxi=\u7231\u6e38\u620f
|
||||
woyouxi=\u6c83\u6e38\u620f
|
||||
lg=\u4e50\u8d2d\u0028\u70b9\u70b9\u0029
|
||||
gf-h=\u0028\u65e7\u0029\u5b98\u65b9\u4e13\u670d
|
||||
gf-z=\u0028\u65e7\u0029\u5b98\u65b9\u6df7\u670d
|
||||
gfzf=\u5b98\u65b9\u4e13\u670d
|
||||
gfhf=\u5b98\u65b9\u6df7\u670d
|
||||
owzf=\u5076\u73a9\u4e13\u670d
|
||||
gf-n=\u5b98\u65b9\u5185
|
||||
my=\u9b54\u9047
|
||||
jinshan=\u91d1\u5c71
|
||||
duoku=\u591a\u9177
|
||||
yk=\u4f18\u9177
|
||||
xl=\u65b0\u6d6a
|
||||
sougou=\u641c\u72d7
|
||||
dy=\u6597\u9c7c
|
||||
dw=\u591a\u73a9
|
||||
tf=\u53f0\u670d
|
||||
xunlei=\u8fc5\u96f7
|
||||
ky=\u5feb\u7528
|
||||
lenvono=\u0028\u65e7\u0029\u8054\u60f3
|
||||
jifeng=\u0028\u65e7\u0029\u673a\u950b
|
||||
anzhi=\u0028\u65e7\u0029\u5b89\u667a
|
||||
37wan=\u0028\u65e7\u0029\u0033\u0037\u73a9
|
||||
ewan=\u0028\u65e7\u0029\u76ca\u73a9
|
||||
mi=\u0028\u65e7\u0029\u5c0f\u7c73
|
||||
dangle=\u0028\u65e7\u0029\u5f53\u4e50
|
||||
ouwan=\u0028\u65e7\u0029\u5076\u73a9
|
||||
ttyy=\u0028\u65e7\u0029\u0054\u0054
|
||||
huawei=\u0028\u65e7\u0029\u534e\u4e3a
|
||||
shuizhu=\u0028\u65e7\u0029\u6c34\u716e
|
||||
43997=\u0034\u0033\u0039\u0039\u0037
|
||||
19196zf=\u0031\u0039\u0031\u0039\u0036\u4e13\u670d
|
||||
51508=\u0035\u0031\u0035\u0030\u0038
|
||||
zzb=\u81f3\u5c0a\u5b9d
|
||||
wkd=\u73a9\u5ba2\u7248
|
||||
yq=\u4f18\u8da3
|
||||
tiantian=\u5929\u5929
|
||||
ddw=\u70b9\u70b9\u73a9
|
||||
by=\u7206\u6e38
|
||||
as=\u7231\u4e0a
|
||||
flb=\u5c0f\u7b28\u6e38\u620f
|
||||
cf=\u695a\u98ce
|
||||
itools=\u0049\u0054\u004f\u004f\u004c\u0053
|
||||
ayw=\u7231\u7ea6\u73a9
|
||||
cc=\u866b\u866b
|
||||
kpzs=\u9760\u8c31\u52a9\u624b
|
||||
xtt=\u65b0\u0074\u0074
|
||||
yt=\u6e38\u9014\u7248
|
||||
9665=\u0039\u0036\u0036\u0035
|
||||
lehhkf=\u4e50\u55e8\u55e8\u006b\u0066
|
||||
lehhol=\u4e50\u55e8\u55e8\u006f\u006c
|
||||
afly=\u5b89\u950b\u006c\u0079
|
||||
360hf=\u0033\u0036\u0030\u6df7\u670d\u7248
|
||||
qql=\u9f50\u9f50\u4e50
|
||||
yehuo=\u91ce\u706b\u7248
|
||||
jizhi=\u6781\u81f4\u7248
|
||||
shengxun=\u76db\u8baf\u6e38\u620f
|
||||
9upt=\u4e5d\u6e38\u666e\u901a\u7248
|
||||
9ujs=\u4e5d\u6e38\u52a0\u901f\u7248
|
||||
xmpt=\u5c0f\u7c73\u666e\u901a\u7248
|
||||
xmjs=\u5c0f\u7c73\u52a0\u901f\u7248
|
||||
gfpt=\u5b98\u65b9\u666e\u901a\u7248
|
||||
gfjs=\u5b98\u65b9\u52a0\u901f\u7248
|
||||
wkd1=\u73a9\u5ba2\u65b0\u7248
|
||||
5288=\u0035\u0032\u0038\u0038
|
||||
7guo=\u4e03\u679c
|
||||
zhanyou=\u5c55\u6e38\u7248
|
||||
babie=\u5df4\u522b\u65f6\u4ee3\u7248
|
||||
yueyou=\u7ea6\u6e38\u7248
|
||||
kuniu=\u9177\u725b\u7248
|
||||
chukong=\u89e6\u63a7\u7248
|
||||
changxiang=\u7545\u60f3\u7248
|
||||
zhuohua=\u707c\u534e\u7248
|
||||
duokemeng=\u54c6\u53ef\u68a6\u7248
|
||||
xuanyun=\u7384\u4e91\u7248
|
||||
hanqu=\u701a\u8da3\u7248
|
||||
wapu=\u86d9\u6251\u7248
|
||||
shengli=\u80dc\u5229\u6e38\u620f\u7248
|
||||
heitao=\u9ed1\u6843\u4e92\u52a8\u7248
|
||||
xumei=\u65ed\u6885\u6e38\u620f\u7248
|
||||
weixun=\u5fae\u8baf\u7248
|
||||
tianxiang=\u5929\u8c61\u4e92\u52a8\u7248
|
||||
taiqi=\u6cf0\u5947\u7248
|
||||
chujian=\u521d\u89c1\u7248
|
||||
gaiya=\u76d6\u5a05\u7248
|
||||
wanmei=\u5b8c\u7f8e\u4e16\u754c\u7248
|
||||
zhuoyue=\u5353\u8d8a\u7248
|
||||
meifeng=\u7f8e\u5cf0\u7248
|
||||
xuanji=\u7384\u673a\u7248
|
||||
changyou=\u7545\u6e38\u7248
|
||||
syg=\u624b\u6e38\u72d7
|
||||
youzu=\u6e38\u65cf\u7248
|
||||
bili=\u0062\u0069\u006c\u0069\u0062\u0069\u006c\u0069\u7248
|
||||
ly=\u4e50\u6e38
|
||||
gfwy=\u7f51\u9875\u7248
|
||||
miqi=\u7c73\u5947\u73a9
|
||||
ayw=\u6e38\u620f\u0066\u0061\u006e
|
||||
ys=\u591c\u795e\u7248
|
||||
aofei=\u5965\u98de
|
||||
mgw=\u8611\u83c7\u73a9
|
||||
longc=\u9f99\u57ce\u7248
|
||||
16y=\u0031\u0036\u6e38
|
||||
xq=\u5c0f\u4e03
|
||||
yuwan=\u9c7c\u4e38\u7248
|
||||
jgp=\u679c\u76d8\u65e7
|
||||
lequ=\u6dd8\u8da3
|
||||
jianguo=\u575a\u679c\u7248
|
||||
yanmeng=\u5ef6\u68a6\u7248
|
||||
9u=\u4E5D\u6E38\u7248
|
||||
360=360\u7248
|
||||
baidu=\u767E\u5EA6\u7248
|
||||
dangle=\u5F53\u4E50\u7248
|
||||
ouwan=\u5076\u73A9\u7248
|
||||
gf=\u5B98\u65B9\u7248
|
||||
mi=\u5C0F\u7C73\u7248
|
||||
oppo=OPPO\u7248
|
||||
91=91\u7248
|
||||
wdj=\u8C4C\u8C46\u835A\u7248
|
||||
vivo=VIVO\u7248
|
||||
pps=PPS\u7248
|
||||
37wan=37\u73A9\u7248
|
||||
anzhi=\u5B89\u667A\u7248
|
||||
ewan=\u76CA\u73A9\u7248
|
||||
huawei=\u534E\u4E3A\u7248
|
||||
gf-h=\u5B98\u65B9-\u4E13\u670D\u7248
|
||||
gf-z=\u5B98\u65B9-\u6DF7\u670D\u7248
|
||||
shuizhu=\u6C34\u716E\u7248
|
||||
jifeng=\u673A\u950B\u7248
|
||||
azsc=\u5B89\u5353\u5E02\u573A\u7248
|
||||
lenvono=\u8054\u60F3\u7248
|
||||
jinshan=\u91D1\u5C71\u7248
|
||||
mumayi=\u6728\u8682\u8681\u7248
|
||||
gf-n=\u5B98\u7F51-\u5185\u7248
|
||||
gf-w=\u5B98\u7F51-\u5916\u7248
|
||||
duoku=\u591A\u9177\u7248
|
||||
pptv=PPTV\u7248
|
||||
\u5B98\u65B9\u7248=\u5B98\u65B9\u7248
|
||||
\u5B98\u65B9=\u5B98\u65B9\u7248
|
||||
@ -1,443 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2017 Wasabeef
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
var RE = {};
|
||||
|
||||
RE.currentSelection = {
|
||||
"startContainer": 0,
|
||||
"startOffset": 0,
|
||||
"endContainer": 0,
|
||||
"endOffset": 0};
|
||||
|
||||
RE.editor = document.getElementById('editor');
|
||||
|
||||
document.addEventListener("selectionchange", function() { RE.backuprange(); });
|
||||
|
||||
// Initializations
|
||||
RE.callback = function() {
|
||||
window.location.href = "re-callback://" + encodeURIComponent(RE.getHtml());
|
||||
}
|
||||
|
||||
RE.setHtml = function(contents) {
|
||||
RE.editor.innerHTML = decodeURIComponent(contents.replace(/\+/g, '%20'));
|
||||
}
|
||||
|
||||
RE.getHtml = function() {
|
||||
return RE.editor.innerHTML;
|
||||
}
|
||||
|
||||
RE.getText = function() {
|
||||
return RE.editor.innerText;
|
||||
}
|
||||
|
||||
RE.setBaseTextColor = function(color) {
|
||||
RE.editor.style.color = color;
|
||||
}
|
||||
|
||||
RE.setBaseFontSize = function(size) {
|
||||
RE.editor.style.fontSize = size;
|
||||
}
|
||||
|
||||
RE.setPadding = function(left, top, right, bottom) {
|
||||
RE.editor.style.paddingLeft = left;
|
||||
RE.editor.style.paddingTop = top;
|
||||
RE.editor.style.paddingRight = right;
|
||||
RE.editor.style.paddingBottom = bottom;
|
||||
}
|
||||
|
||||
RE.setBackgroundColor = function(color) {
|
||||
document.body.style.backgroundColor = color;
|
||||
}
|
||||
|
||||
RE.setBackgroundImage = function(image) {
|
||||
RE.editor.style.backgroundImage = image;
|
||||
}
|
||||
|
||||
RE.setWidth = function(size) {
|
||||
RE.editor.style.minWidth = size;
|
||||
}
|
||||
|
||||
RE.setHeight = function(size) {
|
||||
RE.editor.style.height = size;
|
||||
}
|
||||
|
||||
RE.setTextAlign = function(align) {
|
||||
RE.editor.style.textAlign = align;
|
||||
}
|
||||
|
||||
RE.setVerticalAlign = function(align) {
|
||||
RE.editor.style.verticalAlign = align;
|
||||
}
|
||||
|
||||
RE.setPlaceholder = function(placeholder) {
|
||||
RE.editor.setAttribute("placeholder", placeholder);
|
||||
}
|
||||
|
||||
RE.setEditorFocus = function() {
|
||||
RE.editor.focus();
|
||||
}
|
||||
|
||||
RE.setInputEnabled = function(inputEnabled) {
|
||||
RE.editor.contentEditable = String(inputEnabled);
|
||||
}
|
||||
|
||||
RE.setFocusByEnd = function() {
|
||||
//alert("111111")
|
||||
// var txt =RE.editor.createTextRange();
|
||||
// ("22222")
|
||||
// txt.moveStart('character',-1);
|
||||
// ("333333")
|
||||
// txt.collapse(true);
|
||||
// ("444444")
|
||||
// txt.select();
|
||||
// alert("ddddddd")
|
||||
}
|
||||
|
||||
RE.undo = function() {
|
||||
document.execCommand('undo', false, null);
|
||||
}
|
||||
|
||||
RE.redo = function() {
|
||||
document.execCommand('redo', false, null);
|
||||
}
|
||||
|
||||
RE.setBold = function() {
|
||||
document.execCommand('bold', false, null);
|
||||
}
|
||||
|
||||
RE.setItalic = function() {
|
||||
document.execCommand('italic', false, null);
|
||||
}
|
||||
|
||||
RE.setSubscript = function() {
|
||||
document.execCommand('subscript', false, null);
|
||||
}
|
||||
|
||||
RE.setSuperscript = function() {
|
||||
document.execCommand('superscript', false, null);
|
||||
}
|
||||
|
||||
RE.setStrikeThrough = function() {
|
||||
document.execCommand('strikeThrough', false, null);
|
||||
}
|
||||
|
||||
RE.setUnderline = function() {
|
||||
document.execCommand('underline', false, null);
|
||||
}
|
||||
|
||||
RE.setBullets = function() {
|
||||
document.execCommand('insertUnorderedList', false, null);
|
||||
}
|
||||
|
||||
RE.setNumbers = function() {
|
||||
document.execCommand('insertOrderedList', false, null);
|
||||
}
|
||||
|
||||
RE.setTextColor = function(color) {
|
||||
RE.restorerange();
|
||||
document.execCommand("styleWithCSS", null, true);
|
||||
document.execCommand('foreColor', false, color);
|
||||
document.execCommand("styleWithCSS", null, false);
|
||||
}
|
||||
|
||||
RE.setTextBackgroundColor = function(color) {
|
||||
RE.restorerange();
|
||||
document.execCommand("styleWithCSS", null, true);
|
||||
document.execCommand('hiliteColor', false, color);
|
||||
document.execCommand("styleWithCSS", null, false);
|
||||
}
|
||||
|
||||
RE.setFontSize = function(fontSize){
|
||||
document.execCommand("fontSize", false, fontSize);
|
||||
}
|
||||
|
||||
RE.setHeading = function(heading) {
|
||||
document.execCommand('formatBlock', false, '<h'+heading+'>');
|
||||
}
|
||||
|
||||
RE.setIndent = function() {
|
||||
document.execCommand('indent', false, null);
|
||||
}
|
||||
|
||||
RE.setOutdent = function() {
|
||||
document.execCommand('outdent', false, null);
|
||||
}
|
||||
|
||||
RE.setJustifyLeft = function() {
|
||||
document.execCommand('justifyLeft', false, null);
|
||||
}
|
||||
|
||||
RE.setJustifyCenter = function() {
|
||||
document.execCommand('justifyCenter', false, null);
|
||||
}
|
||||
|
||||
RE.setJustifyRight = function() {
|
||||
document.execCommand('justifyRight', false, null);
|
||||
}
|
||||
|
||||
RE.setBlockquote = function() {
|
||||
document.execCommand('formatBlock', false, '<blockquote>');
|
||||
}
|
||||
|
||||
RE.insertImage = function(url) {
|
||||
var html = "<div><img src =\"" + url + "\" style=\" max-width: 100%; display:block; margin:15px auto; height: auto;\"></div><br>"
|
||||
RE.insertHTML(html);
|
||||
}
|
||||
|
||||
//RE.lazyLoad = function() {
|
||||
// var myLazyLoad = new LazyLoad({
|
||||
// elements_selector: ".lazy"
|
||||
// })
|
||||
//}
|
||||
|
||||
// 替换成缩略图
|
||||
RE.replaceTbImage = function(imgRuleFlag, gifRuleFlag) {
|
||||
var imgs = document.getElementsByTagName("img");
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
if(img.src.indexOf("?") > 0) continue;
|
||||
|
||||
var tbImg
|
||||
if(img.src.indexOf(".gif") > 0) {
|
||||
tbImg = img.src + gifRuleFlag
|
||||
} else {
|
||||
tbImg = img.src + imgRuleFlag
|
||||
}
|
||||
|
||||
img.style.cssText = "max-width: 60%; display:block; margin:15px auto; height: auto;"
|
||||
img.src = tbImg;
|
||||
|
||||
if (i == 0) {
|
||||
var bigImg = document.createElement('img');
|
||||
bigImg.src = "file:///android_asset/web_load_dfimg_icon.png";
|
||||
bigImg.style.cssText = "max-width: 20%; margin:15px 0 0 0; height: auto;"
|
||||
img.parentNode.insertBefore(bigImg, img.parentNode.childNodes[0]);
|
||||
i++;
|
||||
|
||||
if(img.parentNode != null) {
|
||||
img.parentNode.style.cssText += "text-align: left;"
|
||||
}
|
||||
|
||||
if(img.parentNode != null && img.parentNode.parentNode != null) {
|
||||
img.parentNode.parentNode.style.cssText += "text-align: left;"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 替换成默认图
|
||||
RE.replaceAllDfImage = function(imgRuleFlag, gifRuleFlag) {
|
||||
var imgs = document.getElementsByTagName("img");
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
if(img.src.indexOf("web_load_dfimg_icon") > 0) {
|
||||
img.parentNode.removeChild(img.parentNode.childNodes[0]);
|
||||
i--;
|
||||
} else {
|
||||
if(img.src.indexOf(".gif") > 0) {
|
||||
if(gifRuleFlag.indexOf(",default") > 0) {
|
||||
img.style.cssText = "max-width: 100%; display:block; margin:8px auto; height: auto;"
|
||||
img.src = img.src.split("?")[0] + gifRuleFlag
|
||||
}
|
||||
} else {
|
||||
img.style.cssText = "max-width: 100%; display:block; margin:8px auto; height: auto;"
|
||||
img.src = img.src.split("?")[0] + imgRuleFlag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 去除显示大图
|
||||
RE.hideShowBigPic = function() {
|
||||
var imgs = document.getElementsByTagName("img");
|
||||
var j = 0;
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
if(img.src.indexOf(",thumbnail") > 0 && img.src.indexOf(".gif") == -1) {
|
||||
j++;
|
||||
}
|
||||
}
|
||||
// 去除显示大图
|
||||
if (j == 0) {
|
||||
var imgs = document.getElementsByTagName("img");
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
if(img.src.indexOf("web_load_dfimg_icon") > 0) {
|
||||
img.parentNode.removeChild(img.parentNode.childNodes[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RE.replaceDfImageByUrl = function(imgUrl, imgRuleFlag, gifRuleFlag) {
|
||||
var imgs = document.getElementsByTagName("img");
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
if (img.src.indexOf(imgUrl) != -1) {
|
||||
img.style.cssText = "max-width: 100%; display:block; margin:8px auto; height: auto;"
|
||||
if(img.src.indexOf(".gif") > 0) {
|
||||
img.src = img.src.split("?")[0] + gifRuleFlag
|
||||
} else {
|
||||
img.src = img.src.split("?")[0] + imgRuleFlag
|
||||
}
|
||||
}
|
||||
}
|
||||
RE.hideShowBigPic();
|
||||
}
|
||||
|
||||
RE.ImageClickListener = function() {
|
||||
var imgs = document.getElementsByTagName("img");
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
window.imagelistener.imageArr(img.src);
|
||||
img.onclick = function() {
|
||||
window.imagelistener.imageClick(this.src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RE.insertHTML = function(html) {
|
||||
RE.restorerange();
|
||||
document.execCommand('insertHTML', false, html);
|
||||
}
|
||||
|
||||
RE.insertLink = function(url, title) {
|
||||
RE.restorerange();
|
||||
var sel = document.getSelection();
|
||||
if (sel.toString().length == 0) {
|
||||
document.execCommand("insertHTML",false,"<a href='"+url+"'>"+title+"</a>");
|
||||
} else if (sel.rangeCount) {
|
||||
var el = document.createElement("a");
|
||||
el.setAttribute("href", url);
|
||||
el.setAttribute("title", title);
|
||||
|
||||
var range = sel.getRangeAt(0).cloneRange();
|
||||
range.surroundContents(el);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
RE.callback();
|
||||
}
|
||||
|
||||
RE.setTodo = function(text) {
|
||||
var html = '<input type="checkbox" name="'+ text +'" value="'+ text +'"/> ';
|
||||
document.execCommand('insertHTML', false, html);
|
||||
}
|
||||
|
||||
RE.prepareInsert = function() {
|
||||
RE.backuprange();
|
||||
}
|
||||
|
||||
RE.backuprange = function(){
|
||||
var selection = window.getSelection();
|
||||
if (selection.rangeCount > 0) {
|
||||
var range = selection.getRangeAt(0);
|
||||
RE.currentSelection = {
|
||||
"startContainer": range.startContainer,
|
||||
"startOffset": range.startOffset,
|
||||
"endContainer": range.endContainer,
|
||||
"endOffset": range.endOffset};
|
||||
}
|
||||
}
|
||||
|
||||
RE.restorerange = function(){
|
||||
var selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
var range = document.createRange();
|
||||
range.setStart(RE.currentSelection.startContainer, RE.currentSelection.startOffset);
|
||||
range.setEnd(RE.currentSelection.endContainer, RE.currentSelection.endOffset);
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
RE.enabledEditingItems = function(e) {
|
||||
var items = [];
|
||||
if (document.queryCommandState('bold')) {
|
||||
items.push('bold');
|
||||
}
|
||||
if (document.queryCommandState('italic')) {
|
||||
items.push('italic');
|
||||
}
|
||||
if (document.queryCommandState('subscript')) {
|
||||
items.push('subscript');
|
||||
}
|
||||
if (document.queryCommandState('superscript')) {
|
||||
items.push('superscript');
|
||||
}
|
||||
if (document.queryCommandState('strikeThrough')) {
|
||||
items.push('strikeThrough');
|
||||
}
|
||||
if (document.queryCommandState('underline')) {
|
||||
items.push('underline');
|
||||
}
|
||||
if (document.queryCommandState('insertOrderedList')) {
|
||||
items.push('orderedList');
|
||||
}
|
||||
if (document.queryCommandState('insertUnorderedList')) {
|
||||
items.push('unorderedList');
|
||||
}
|
||||
if (document.queryCommandState('justifyCenter')) {
|
||||
items.push('justifyCenter');
|
||||
}
|
||||
if (document.queryCommandState('justifyFull')) {
|
||||
items.push('justifyFull');
|
||||
}
|
||||
if (document.queryCommandState('justifyLeft')) {
|
||||
items.push('justifyLeft');
|
||||
}
|
||||
if (document.queryCommandState('justifyRight')) {
|
||||
items.push('justifyRight');
|
||||
}
|
||||
if (document.queryCommandState('insertHorizontalRule')) {
|
||||
items.push('horizontalRule');
|
||||
}
|
||||
var formatBlock = document.queryCommandValue('formatBlock');
|
||||
if (formatBlock.length > 0) {
|
||||
items.push(formatBlock);
|
||||
}
|
||||
|
||||
window.location.href = "re-state://" + encodeURI(items.join(','));
|
||||
}
|
||||
|
||||
RE.focus = function() {
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(RE.editor);
|
||||
range.collapse(false);
|
||||
var selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
RE.editor.focus();
|
||||
}
|
||||
|
||||
RE.blurFocus = function() {
|
||||
RE.editor.blur();
|
||||
}
|
||||
|
||||
RE.removeFormat = function() {
|
||||
document.execCommand('removeFormat', false, null);
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
RE.editor.addEventListener("input", RE.callback);
|
||||
RE.editor.addEventListener("keyup", function(e) {
|
||||
var KEY_LEFT = 37, KEY_RIGHT = 39;
|
||||
if (e.which == KEY_LEFT || e.which == KEY_RIGHT) {
|
||||
RE.enabledEditingItems(e);
|
||||
}
|
||||
});
|
||||
RE.editor.addEventListener("click", RE.enabledEditingItems);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
2
app/src/main/assets/zepto.min.js
vendored
2
app/src/main/assets/zepto.min.js
vendored
File diff suppressed because one or more lines are too long
699
app/src/main/java/android/support/v7/widget/AdapterHelper.java
Normal file
699
app/src/main/java/android/support/v7/widget/AdapterHelper.java
Normal file
@ -0,0 +1,699 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.support.v4.util.Pools;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
|
||||
/**
|
||||
* Helper class that can enqueue and process adapter update operations.
|
||||
* <p>
|
||||
* To support animations, RecyclerView presents an older version the Adapter to best represent
|
||||
* previous state of the layout. Sometimes, this is not trivial when items are removed that were
|
||||
* not laid out, in which case, RecyclerView has no way of providing that item's view for
|
||||
* animations.
|
||||
* <p>
|
||||
* AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
|
||||
* pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
|
||||
* and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
|
||||
* according to previously deferred operation and dispatch them before the first layout pass. It
|
||||
* also takes care of updating deferred UpdateOps since order of operations is changed by this
|
||||
* process.
|
||||
* <p>
|
||||
* Although operations may be forwarded to LayoutManager in different orders, resulting data set
|
||||
* is guaranteed to be the consistent.
|
||||
*/
|
||||
class AdapterHelper implements OpReorderer.Callback {
|
||||
|
||||
final static int POSITION_TYPE_INVISIBLE = 0;
|
||||
|
||||
final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String TAG = "AHT";
|
||||
|
||||
private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
|
||||
|
||||
final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
|
||||
|
||||
final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
Runnable mOnItemProcessedCallback;
|
||||
|
||||
final boolean mDisableRecycler;
|
||||
|
||||
final OpReorderer mOpReorderer;
|
||||
|
||||
AdapterHelper(Callback callback) {
|
||||
this(callback, false);
|
||||
}
|
||||
|
||||
AdapterHelper(Callback callback, boolean disableRecycler) {
|
||||
mCallback = callback;
|
||||
mDisableRecycler = disableRecycler;
|
||||
mOpReorderer = new OpReorderer(this);
|
||||
}
|
||||
|
||||
AdapterHelper addUpdateOp(UpdateOp... ops) {
|
||||
Collections.addAll(mPendingUpdates, ops);
|
||||
return this;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
recycleUpdateOpsAndClearList(mPendingUpdates);
|
||||
recycleUpdateOpsAndClearList(mPostponedList);
|
||||
}
|
||||
|
||||
void preProcess() {
|
||||
mOpReorderer.reorderOps(mPendingUpdates);
|
||||
final int count = mPendingUpdates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPendingUpdates.get(i);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
applyAdd(op);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
applyRemove(op);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
applyUpdate(op);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
applyMove(op);
|
||||
break;
|
||||
}
|
||||
if (mOnItemProcessedCallback != null) {
|
||||
mOnItemProcessedCallback.run();
|
||||
}
|
||||
}
|
||||
mPendingUpdates.clear();
|
||||
}
|
||||
|
||||
void consumePostponedUpdates() {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
mCallback.onDispatchSecondPass(mPostponedList.get(i));
|
||||
}
|
||||
recycleUpdateOpsAndClearList(mPostponedList);
|
||||
}
|
||||
|
||||
private void applyMove(UpdateOp op) {
|
||||
// MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
|
||||
// otherwise, it would be converted into a REMOVE operation
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
|
||||
private void applyRemove(UpdateOp op) {
|
||||
int tmpStart = op.positionStart;
|
||||
int tmpCount = 0;
|
||||
int tmpEnd = op.positionStart + op.itemCount;
|
||||
int type = -1;
|
||||
for (int position = op.positionStart; position < tmpEnd; position++) {
|
||||
boolean typeChanged = false;
|
||||
ViewHolder vh = mCallback.findViewHolder(position);
|
||||
if (vh != null || canFindInPreLayout(position)) {
|
||||
// If a ViewHolder exists or this is a newly added item, we can defer this update
|
||||
// to post layout stage.
|
||||
// * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
|
||||
// * For items that are added and removed in the same process cycle, they won't
|
||||
// have any effect in pre-layout since their add ops are already deferred to
|
||||
// post-layout pass.
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
// Looks like we have other updates that we cannot merge with this one.
|
||||
// Create an UpdateOp and dispatch it to LayoutManager.
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
dispatchAndUpdateViewHolders(newOp);
|
||||
typeChanged = true;
|
||||
}
|
||||
type = POSITION_TYPE_NEW_OR_LAID_OUT;
|
||||
} else {
|
||||
// This update cannot be recovered because we don't have a ViewHolder representing
|
||||
// this position. Instead, post it to LayoutManager immediately
|
||||
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
|
||||
// Looks like we have other updates that we cannot merge with this one.
|
||||
// Create UpdateOp op and dispatch it to LayoutManager.
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
postponeAndUpdateViewHolders(newOp);
|
||||
typeChanged = true;
|
||||
}
|
||||
type = POSITION_TYPE_INVISIBLE;
|
||||
}
|
||||
if (typeChanged) {
|
||||
position -= tmpCount; // also equal to tmpStart
|
||||
tmpEnd -= tmpCount;
|
||||
tmpCount = 1;
|
||||
} else {
|
||||
tmpCount++;
|
||||
}
|
||||
}
|
||||
if (tmpCount != op.itemCount) { // all 1 effect
|
||||
recycleUpdateOp(op);
|
||||
op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
}
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
dispatchAndUpdateViewHolders(op);
|
||||
} else {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyUpdate(UpdateOp op) {
|
||||
int tmpStart = op.positionStart;
|
||||
int tmpCount = 0;
|
||||
int tmpEnd = op.positionStart + op.itemCount;
|
||||
int type = -1;
|
||||
for (int position = op.positionStart; position < tmpEnd; position++) {
|
||||
ViewHolder vh = mCallback.findViewHolder(position);
|
||||
if (vh != null || canFindInPreLayout(position)) { // deferred
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
dispatchAndUpdateViewHolders(newOp);
|
||||
tmpCount = 0;
|
||||
tmpStart = position;
|
||||
}
|
||||
type = POSITION_TYPE_NEW_OR_LAID_OUT;
|
||||
} else { // applied
|
||||
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
postponeAndUpdateViewHolders(newOp);
|
||||
tmpCount = 0;
|
||||
tmpStart = position;
|
||||
}
|
||||
type = POSITION_TYPE_INVISIBLE;
|
||||
}
|
||||
tmpCount++;
|
||||
}
|
||||
if (tmpCount != op.itemCount) { // all 1 effect
|
||||
recycleUpdateOp(op);
|
||||
op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
}
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
dispatchAndUpdateViewHolders(op);
|
||||
} else {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchAndUpdateViewHolders(UpdateOp op) {
|
||||
// tricky part.
|
||||
// traverse all postpones and revert their changes on this op if necessary, apply updated
|
||||
// dispatch to them since now they are after this op.
|
||||
if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
|
||||
throw new IllegalArgumentException("should not dispatch add or move for pre layout");
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatch (pre)" + op);
|
||||
Log.d(TAG, "postponed state before:");
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
|
||||
// handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
|
||||
// TODO Since move ops are pushed to end, we should not need this anymore
|
||||
int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
|
||||
}
|
||||
int tmpCnt = 1;
|
||||
int offsetPositionForPartial = op.positionStart;
|
||||
final int positionMultiplier;
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.UPDATE:
|
||||
positionMultiplier = 1;
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
positionMultiplier = 0;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("op should be remove or update." + op);
|
||||
}
|
||||
for (int p = 1; p < op.itemCount; p++) {
|
||||
final int pos = op.positionStart + (positionMultiplier * p);
|
||||
int updatedPos = updatePositionWithPostponed(pos, op.cmd);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
|
||||
}
|
||||
boolean continuous = false;
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.UPDATE:
|
||||
continuous = updatedPos == tmpStart + 1;
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
continuous = updatedPos == tmpStart;
|
||||
break;
|
||||
}
|
||||
if (continuous) {
|
||||
tmpCnt++;
|
||||
} else {
|
||||
// need to dispatch this separately
|
||||
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "need to dispatch separately " + tmp);
|
||||
}
|
||||
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
|
||||
recycleUpdateOp(tmp);
|
||||
if (op.cmd == UpdateOp.UPDATE) {
|
||||
offsetPositionForPartial += tmpCnt;
|
||||
}
|
||||
tmpStart = updatedPos;// need to remove previously dispatched
|
||||
tmpCnt = 1;
|
||||
}
|
||||
}
|
||||
recycleUpdateOp(op);
|
||||
if (tmpCnt > 0) {
|
||||
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatching:" + tmp);
|
||||
}
|
||||
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
|
||||
recycleUpdateOp(tmp);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "post dispatch");
|
||||
Log.d(TAG, "postponed state after:");
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
|
||||
mCallback.onDispatchFirstPass(op);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.markViewHoldersUpdated(offsetStart, op.itemCount);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("only remove and update ops can be dispatched"
|
||||
+ " in first pass");
|
||||
}
|
||||
}
|
||||
|
||||
private int updatePositionWithPostponed(int pos, int cmd) {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
UpdateOp postponed = mPostponedList.get(i);
|
||||
if (postponed.cmd == UpdateOp.MOVE) {
|
||||
int start, end;
|
||||
if (postponed.positionStart < postponed.itemCount) {
|
||||
start = postponed.positionStart;
|
||||
end = postponed.itemCount;
|
||||
} else {
|
||||
start = postponed.itemCount;
|
||||
end = postponed.positionStart;
|
||||
}
|
||||
if (pos >= start && pos <= end) {
|
||||
//i'm affected
|
||||
if (start == postponed.positionStart) {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.itemCount++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.itemCount--;
|
||||
}
|
||||
// op moved to left, move it right to revert
|
||||
pos++;
|
||||
} else {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
}
|
||||
// op was moved right, move left to revert
|
||||
pos--;
|
||||
}
|
||||
} else if (pos < postponed.positionStart) {
|
||||
// postponed MV is outside the dispatched OP. if it is before, offset
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
postponed.itemCount++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
postponed.itemCount--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (postponed.positionStart <= pos) {
|
||||
if (postponed.cmd == UpdateOp.ADD) {
|
||||
pos -= postponed.itemCount;
|
||||
} else if (postponed.cmd == UpdateOp.REMOVE) {
|
||||
pos += postponed.itemCount;
|
||||
}
|
||||
} else {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispath (step" + i + ")");
|
||||
Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
}
|
||||
for (int i = mPostponedList.size() - 1; i >= 0; i--) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (op.itemCount == op.positionStart || op.itemCount < 0) {
|
||||
mPostponedList.remove(i);
|
||||
recycleUpdateOp(op);
|
||||
}
|
||||
} else if (op.itemCount <= 0) {
|
||||
mPostponedList.remove(i);
|
||||
recycleUpdateOp(op);
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private boolean canFindInPreLayout(int position) {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (findPositionOffset(op.itemCount, i + 1) == position) {
|
||||
return true;
|
||||
}
|
||||
} else if (op.cmd == UpdateOp.ADD) {
|
||||
// TODO optimize.
|
||||
final int end = op.positionStart + op.itemCount;
|
||||
for (int pos = op.positionStart; pos < end; pos++) {
|
||||
if (findPositionOffset(pos, i + 1) == position) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void applyAdd(UpdateOp op) {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
|
||||
private void postponeAndUpdateViewHolders(UpdateOp op) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "postponing " + op);
|
||||
}
|
||||
// Utils.log("add UpdateOp to PostponedList");
|
||||
mPostponedList.add(op);
|
||||
// Utils.log("op" + op.positionStart + "=" + op.itemCount);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
|
||||
op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown update op type for " + op);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasPendingUpdates() {
|
||||
return mPendingUpdates.size() > 0;
|
||||
}
|
||||
|
||||
int findPositionOffset(int position) {
|
||||
return findPositionOffset(position, 0);
|
||||
}
|
||||
|
||||
int findPositionOffset(int position, int firstPostponedItem) {
|
||||
int count = mPostponedList.size();
|
||||
for (int i = firstPostponedItem; i < count; ++i) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (op.positionStart == position) {
|
||||
position = op.itemCount;
|
||||
} else {
|
||||
if (op.positionStart < position) {
|
||||
position--; // like a remove
|
||||
}
|
||||
if (op.itemCount <= position) {
|
||||
position++; // like an add
|
||||
}
|
||||
}
|
||||
} else if (op.positionStart <= position) {
|
||||
if (op.cmd == UpdateOp.REMOVE) {
|
||||
if (position < op.positionStart + op.itemCount) {
|
||||
return -1;
|
||||
}
|
||||
position -= op.itemCount;
|
||||
} else if (op.cmd == UpdateOp.ADD) {
|
||||
position += op.itemCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeChanged(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeInserted(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeMoved(int from, int to, int itemCount) {
|
||||
if (from == to) {
|
||||
return false;//no-op
|
||||
}
|
||||
if (itemCount != 1) {
|
||||
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
|
||||
}
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips pre-processing and applies all updates in one pass.
|
||||
*/
|
||||
void consumeUpdatesInOnePass() {
|
||||
// we still consume postponed updates (if there is) in case there was a pre-process call
|
||||
// w/o a matching consumePostponedUpdates.
|
||||
consumePostponedUpdates();
|
||||
final int count = mPendingUpdates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPendingUpdates.get(i);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
|
||||
break;
|
||||
}
|
||||
if (mOnItemProcessedCallback != null) {
|
||||
mOnItemProcessedCallback.run();
|
||||
}
|
||||
}
|
||||
recycleUpdateOpsAndClearList(mPendingUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queued operation to happen when child views are updated.
|
||||
*/
|
||||
static class UpdateOp {
|
||||
|
||||
static final int ADD = 0;
|
||||
|
||||
static final int REMOVE = 1;
|
||||
|
||||
static final int UPDATE = 2;
|
||||
|
||||
static final int MOVE = 3;
|
||||
|
||||
static final int POOL_SIZE = 30;
|
||||
|
||||
int cmd;
|
||||
|
||||
int positionStart;
|
||||
|
||||
// holds the target position if this is a MOVE
|
||||
int itemCount;
|
||||
|
||||
UpdateOp(int cmd, int positionStart, int itemCount) {
|
||||
this.cmd = cmd;
|
||||
this.positionStart = positionStart;
|
||||
this.itemCount = itemCount;
|
||||
}
|
||||
|
||||
String cmdToString() {
|
||||
switch (cmd) {
|
||||
case ADD:
|
||||
return "add";
|
||||
case REMOVE:
|
||||
return "rm";
|
||||
case UPDATE:
|
||||
return "up";
|
||||
case MOVE:
|
||||
return "mv";
|
||||
}
|
||||
return "??";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateOp op = (UpdateOp) o;
|
||||
|
||||
if (cmd != op.cmd) {
|
||||
return false;
|
||||
}
|
||||
if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
|
||||
// reverse of this is also true
|
||||
if (itemCount == op.positionStart && positionStart == op.itemCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (itemCount != op.itemCount) {
|
||||
return false;
|
||||
}
|
||||
if (positionStart != op.positionStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = cmd;
|
||||
result = 31 * result + positionStart;
|
||||
result = 31 * result + itemCount;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
|
||||
UpdateOp op = mUpdateOpPool.acquire();
|
||||
if (op == null) {
|
||||
op = new UpdateOp(cmd, positionStart, itemCount);
|
||||
} else {
|
||||
op.cmd = cmd;
|
||||
op.positionStart = positionStart;
|
||||
op.itemCount = itemCount;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycleUpdateOp(UpdateOp op) {
|
||||
if (!mDisableRecycler) {
|
||||
mUpdateOpPool.release(op);
|
||||
}
|
||||
}
|
||||
|
||||
void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
|
||||
final int count = ops.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
recycleUpdateOp(ops.get(i));
|
||||
}
|
||||
ops.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract between AdapterHelper and RecyclerView.
|
||||
*/
|
||||
static interface Callback {
|
||||
|
||||
ViewHolder findViewHolder(int position);
|
||||
|
||||
void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
|
||||
|
||||
void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
|
||||
|
||||
void markViewHoldersUpdated(int positionStart, int itemCount);
|
||||
|
||||
void onDispatchFirstPass(UpdateOp updateOp);
|
||||
|
||||
void onDispatchSecondPass(UpdateOp updateOp);
|
||||
|
||||
void offsetPositionsForAdd(int positionStart, int itemCount);
|
||||
|
||||
void offsetPositionsForMove(int from, int to);
|
||||
}
|
||||
}
|
||||
484
app/src/main/java/android/support/v7/widget/ChildHelper.java
Normal file
484
app/src/main/java/android/support/v7/widget/ChildHelper.java
Normal file
@ -0,0 +1,484 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class to manage children.
|
||||
* <p>
|
||||
* It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
|
||||
* provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
|
||||
* like getChildAt, getChildCount etc. These methods ignore hidden children.
|
||||
* <p>
|
||||
* When RecyclerView needs direct access to the view group children, it can call unfiltered
|
||||
* methods like get getUnfilteredChildCount or getUnfilteredChildAt.
|
||||
*/
|
||||
class ChildHelper {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String TAG = "ChildrenHelper";
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
final Bucket mBucket;
|
||||
|
||||
final List<View> mHiddenViews;
|
||||
|
||||
ChildHelper(Callback callback) {
|
||||
mCallback = callback;
|
||||
mBucket = new Bucket();
|
||||
mHiddenViews = new ArrayList<View>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a view to the ViewGroup
|
||||
*
|
||||
* @param child View to add.
|
||||
* @param hidden If set to true, this item will be invisible from regular methods.
|
||||
*/
|
||||
void addView(View child, boolean hidden) {
|
||||
addView(child, -1, hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a view to the ViewGroup at an index
|
||||
*
|
||||
* @param child View to add.
|
||||
* @param index Index of the child from the regular perspective (excluding hidden views).
|
||||
* ChildHelper offsets this index to actual ViewGroup index.
|
||||
* @param hidden If set to true, this item will be invisible from regular methods.
|
||||
*/
|
||||
void addView(View child, int index, boolean hidden) {
|
||||
final int offset;
|
||||
if (index < 0) {
|
||||
offset = mCallback.getChildCount();
|
||||
} else {
|
||||
offset = getOffset(index);
|
||||
}
|
||||
mCallback.addView(child, offset);
|
||||
mBucket.insert(offset, hidden);
|
||||
if (hidden) {
|
||||
mHiddenViews.add(child);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
private int getOffset(int index) {
|
||||
if (index < 0) {
|
||||
return -1; //anything below 0 won't work as diff will be undefined.
|
||||
}
|
||||
final int limit = mCallback.getChildCount();
|
||||
int offset = index;
|
||||
while (offset < limit) {
|
||||
final int removedBefore = mBucket.countOnesBefore(offset);
|
||||
final int diff = index - (offset - removedBefore);
|
||||
if (diff == 0) {
|
||||
while (mBucket.get(offset)) { // ensure this offset is not hidden
|
||||
offset ++;
|
||||
}
|
||||
return offset;
|
||||
} else {
|
||||
offset += diff;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided View from underlying RecyclerView.
|
||||
*
|
||||
* @param view The view to remove.
|
||||
*/
|
||||
void removeView(View view) {
|
||||
int index = mCallback.indexOfChild(view);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
mCallback.removeViewAt(index);
|
||||
if (mBucket.remove(index)) {
|
||||
mHiddenViews.remove(view);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "remove View off:" + index + "," + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the view at the provided index from RecyclerView.
|
||||
*
|
||||
* @param index Index of the child from the regular perspective (excluding hidden views).
|
||||
* ChildHelper offsets this index to actual ViewGroup index.
|
||||
*/
|
||||
void removeViewAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
final View view = mCallback.getChildAt(offset);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
mCallback.removeViewAt(offset);
|
||||
if (mBucket.remove(offset)) {
|
||||
mHiddenViews.remove(view);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child at provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
View getChildAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
return mCallback.getChildAt(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all views from the ViewGroup including the hidden ones.
|
||||
*/
|
||||
void removeAllViewsUnfiltered() {
|
||||
mCallback.removeAllViews();
|
||||
mBucket.reset();
|
||||
mHiddenViews.clear();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeAllViewsUnfiltered");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be used to find a disappearing view by position.
|
||||
*
|
||||
* @param position The adapter position of the item.
|
||||
* @param type View type, can be {@link RecyclerView#INVALID_TYPE}.
|
||||
* @return A hidden view with a valid ViewHolder that matches the position and type.
|
||||
*/
|
||||
View findHiddenNonRemovedView(int position, int type) {
|
||||
final int count = mHiddenViews.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View view = mHiddenViews.get(i);
|
||||
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
|
||||
if (holder.getPosition() == position && !holder.isInvalid() &&
|
||||
(type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the provided view to the underlying ViewGroup.
|
||||
*
|
||||
* @param child Child to attach.
|
||||
* @param index Index of the child to attach in regular perspective.
|
||||
* @param layoutParams LayoutParams for the child.
|
||||
* @param hidden If set to true, this item will be invisible to the regular methods.
|
||||
*/
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
|
||||
boolean hidden) {
|
||||
final int offset;
|
||||
if (index < 0) {
|
||||
offset = mCallback.getChildCount();
|
||||
} else {
|
||||
offset = getOffset(index);
|
||||
}
|
||||
mCallback.attachViewToParent(child, offset, layoutParams);
|
||||
mBucket.insert(offset, hidden);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," +
|
||||
"h:" + hidden + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of children that are not hidden.
|
||||
*
|
||||
* @return Number of children that are not hidden.
|
||||
* @see #getChildAt(int)
|
||||
*/
|
||||
int getChildCount() {
|
||||
return mCallback.getChildCount() - mHiddenViews.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of children.
|
||||
*
|
||||
* @return The total number of children including the hidden views.
|
||||
* @see #getUnfilteredChildAt(int)
|
||||
*/
|
||||
int getUnfilteredChildCount() {
|
||||
return mCallback.getChildCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a child by ViewGroup offset. ChildHelper won't offset this index.
|
||||
*
|
||||
* @param index ViewGroup index of the child to return.
|
||||
* @return The view in the provided index.
|
||||
*/
|
||||
View getUnfilteredChildAt(int index) {
|
||||
return mCallback.getChildAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the view at the provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
void detachViewFromParent(int index) {
|
||||
final int offset = getOffset(index);
|
||||
mCallback.detachViewFromParent(offset);
|
||||
mBucket.remove(offset);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the child in regular perspective.
|
||||
*
|
||||
* @param child The child whose index will be returned.
|
||||
* @return The regular perspective index of the child or -1 if it does not exists.
|
||||
*/
|
||||
int indexOfChild(View child) {
|
||||
final int index = mCallback.indexOfChild(child);
|
||||
if (index == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
if (DEBUG) {
|
||||
throw new IllegalArgumentException("cannot get index of a hidden child");
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// reverse the index
|
||||
return index - mBucket.countOnesBefore(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a View is visible to LayoutManager or not.
|
||||
*
|
||||
* @param view The child view to check. Should be a child of the Callback.
|
||||
* @return True if the View is not visible to LayoutManager
|
||||
*/
|
||||
boolean isHidden(View view) {
|
||||
return mHiddenViews.contains(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a child view as hidden.
|
||||
*
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
void hide(View view) {
|
||||
final int offset = mCallback.indexOfChild(view);
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
|
||||
}
|
||||
if (DEBUG && mBucket.get(offset)) {
|
||||
throw new RuntimeException("trying to hide same view twice, how come ? " + view);
|
||||
}
|
||||
mBucket.set(offset);
|
||||
mHiddenViews.add(view);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mBucket.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a view from the ViewGroup if it is hidden.
|
||||
*
|
||||
* @param view The view to remove.
|
||||
* @return True if the View is found and it is hidden. False otherwise.
|
||||
*/
|
||||
boolean removeViewIfHidden(View view) {
|
||||
final int index = mCallback.indexOfChild(view);
|
||||
if (index == -1) {
|
||||
if (mHiddenViews.remove(view) && DEBUG) {
|
||||
throw new IllegalStateException("view is in hidden list but not in view group");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
mBucket.remove(index);
|
||||
mCallback.removeViewAt(index);
|
||||
if (!mHiddenViews.remove(view) && DEBUG) {
|
||||
throw new IllegalStateException(
|
||||
"removed a hidden view but it is not in hidden views list");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitset implementation that provides methods to offset indices.
|
||||
*/
|
||||
static class Bucket {
|
||||
|
||||
final static int BITS_PER_WORD = Long.SIZE;
|
||||
|
||||
final static long LAST_BIT = 1L << (Long.SIZE - 1);
|
||||
|
||||
long mData = 0;
|
||||
|
||||
Bucket next;
|
||||
|
||||
void set(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
next.set(index - BITS_PER_WORD);
|
||||
} else {
|
||||
mData |= 1L << index;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNext() {
|
||||
if (next == null) {
|
||||
next = new Bucket();
|
||||
}
|
||||
}
|
||||
|
||||
void clear(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
if (next != null) {
|
||||
next.clear(index - BITS_PER_WORD);
|
||||
}
|
||||
} else {
|
||||
mData &= ~(1L << index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean get(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return next.get(index - BITS_PER_WORD);
|
||||
} else {
|
||||
return (mData & (1L << index)) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
mData = 0;
|
||||
if (next != null) {
|
||||
next.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void insert(int index, boolean value) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
next.insert(index - BITS_PER_WORD, value);
|
||||
} else {
|
||||
final boolean lastBit = (mData & LAST_BIT) != 0;
|
||||
long mask = (1L << index) - 1;
|
||||
final long before = mData & mask;
|
||||
final long after = ((mData & ~mask)) << 1;
|
||||
mData = before | after;
|
||||
if (value) {
|
||||
set(index);
|
||||
} else {
|
||||
clear(index);
|
||||
}
|
||||
if (lastBit || next != null) {
|
||||
ensureNext();
|
||||
next.insert(0, lastBit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return next.remove(index - BITS_PER_WORD);
|
||||
} else {
|
||||
long mask = (1L << index);
|
||||
final boolean value = (mData & mask) != 0;
|
||||
mData &= ~mask;
|
||||
mask = mask - 1;
|
||||
final long before = mData & mask;
|
||||
// cannot use >> because it adds one.
|
||||
final long after = Long.rotateRight(mData & ~mask, 1);
|
||||
mData = before | after;
|
||||
if (next != null) {
|
||||
if (next.get(0)) {
|
||||
set(BITS_PER_WORD - 1);
|
||||
}
|
||||
next.remove(0);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int countOnesBefore(int index) {
|
||||
if (next == null) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
return Long.bitCount(mData);
|
||||
}
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
}
|
||||
if (index < BITS_PER_WORD) {
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
} else {
|
||||
return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return next == null ? Long.toBinaryString(mData)
|
||||
: next.toString() + "xx" + Long.toBinaryString(mData);
|
||||
}
|
||||
}
|
||||
|
||||
static interface Callback {
|
||||
|
||||
int getChildCount();
|
||||
|
||||
void addView(View child, int index);
|
||||
|
||||
int indexOfChild(View view);
|
||||
|
||||
void removeViewAt(int index);
|
||||
|
||||
View getChildAt(int offset);
|
||||
|
||||
void removeAllViews();
|
||||
|
||||
RecyclerView.ViewHolder getChildViewHolder(View view);
|
||||
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
|
||||
|
||||
void detachViewFromParent(int offset);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,628 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorListener;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
|
||||
* animations on remove, add, and move events that happen to the items in
|
||||
* a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
|
||||
*
|
||||
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||
*/
|
||||
public class DefaultItemAnimator extends RecyclerView.ItemAnimator {
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>();
|
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
|
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>();
|
||||
|
||||
private ArrayList<ArrayList<ViewHolder>> mAdditionsList =
|
||||
new ArrayList<ArrayList<ViewHolder>>();
|
||||
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>();
|
||||
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>();
|
||||
|
||||
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<ViewHolder>();
|
||||
|
||||
private static class MoveInfo {
|
||||
public ViewHolder holder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
this.holder = holder;
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChangeInfo {
|
||||
public ViewHolder oldHolder, newHolder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
|
||||
this.oldHolder = oldHolder;
|
||||
this.newHolder = newHolder;
|
||||
}
|
||||
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
this(oldHolder, newHolder);
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangeInfo{" +
|
||||
"oldHolder=" + oldHolder +
|
||||
", newHolder=" + newHolder +
|
||||
", fromX=" + fromX +
|
||||
", fromY=" + fromY +
|
||||
", toX=" + toX +
|
||||
", toY=" + toY +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runPendingAnimations() {
|
||||
boolean removalsPending = !mPendingRemovals.isEmpty();
|
||||
boolean movesPending = !mPendingMoves.isEmpty();
|
||||
boolean changesPending = !mPendingChanges.isEmpty();
|
||||
boolean additionsPending = !mPendingAdditions.isEmpty();
|
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||
// nothing to animate
|
||||
return;
|
||||
}
|
||||
// First, remove stuff
|
||||
for (ViewHolder holder : mPendingRemovals) {
|
||||
animateRemoveImpl(holder);
|
||||
}
|
||||
mPendingRemovals.clear();
|
||||
// Next, move stuff
|
||||
if (movesPending) {
|
||||
final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>();
|
||||
moves.addAll(mPendingMoves);
|
||||
mMovesList.add(moves);
|
||||
mPendingMoves.clear();
|
||||
Runnable mover = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (MoveInfo moveInfo : moves) {
|
||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
||||
moveInfo.toX, moveInfo.toY);
|
||||
}
|
||||
moves.clear();
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
View view = moves.get(0).holder.itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
|
||||
} else {
|
||||
mover.run();
|
||||
}
|
||||
}
|
||||
// Next, change stuff, to run in parallel with move animations
|
||||
if (changesPending) {
|
||||
final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>();
|
||||
changes.addAll(mPendingChanges);
|
||||
mChangesList.add(changes);
|
||||
mPendingChanges.clear();
|
||||
Runnable changer = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (ChangeInfo change : changes) {
|
||||
animateChangeImpl(change);
|
||||
}
|
||||
changes.clear();
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
ViewHolder holder = changes.get(0).oldHolder;
|
||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
|
||||
} else {
|
||||
changer.run();
|
||||
}
|
||||
}
|
||||
// Next, add stuff
|
||||
if (additionsPending) {
|
||||
final ArrayList<ViewHolder> additions = new ArrayList<ViewHolder>();
|
||||
additions.addAll(mPendingAdditions);
|
||||
mAdditionsList.add(additions);
|
||||
mPendingAdditions.clear();
|
||||
Runnable adder = new Runnable() {
|
||||
public void run() {
|
||||
for (ViewHolder holder : additions) {
|
||||
animateAddImpl(holder);
|
||||
}
|
||||
additions.clear();
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
};
|
||||
if (removalsPending || movesPending || changesPending) {
|
||||
long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
||||
long moveDuration = movesPending ? getMoveDuration() : 0;
|
||||
long changeDuration = changesPending ? getChangeDuration() : 0;
|
||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
||||
View view = additions.get(0).itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
|
||||
} else {
|
||||
adder.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateRemove(final ViewHolder holder) {
|
||||
endAnimation(holder);
|
||||
mPendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateRemoveImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
animation.setDuration(getRemoveDuration())
|
||||
.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchRemoveStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchRemoveFinished(holder);
|
||||
mRemoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
mRemoveAnimations.add(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final ViewHolder holder) {
|
||||
endAnimation(holder);
|
||||
ViewCompat.setAlpha(holder.itemView, 0);
|
||||
mPendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateAddImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
mAddAnimations.add(holder);
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
animation.alpha(1).setDuration(getAddDuration()).
|
||||
setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchAddStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchAddFinished(holder);
|
||||
mAddAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
|
||||
int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
fromX += ViewCompat.getTranslationX(holder.itemView);
|
||||
fromY += ViewCompat.getTranslationY(holder.itemView);
|
||||
endAnimation(holder);
|
||||
int deltaX = toX - fromX;
|
||||
int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, -deltaX);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, -deltaY);
|
||||
}
|
||||
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.animate(view).translationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.animate(view).translationY(0);
|
||||
}
|
||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
mMoveAnimations.add(holder);
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchMoveStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchMoveFinished(holder);
|
||||
mMoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
|
||||
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
|
||||
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
|
||||
endAnimation(oldHolder);
|
||||
int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||
int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||
// recover prev translation state after ending animation
|
||||
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
|
||||
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
|
||||
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
|
||||
if (newHolder != null && newHolder.itemView != null) {
|
||||
// carry over translation values
|
||||
endAnimation(newHolder);
|
||||
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
|
||||
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
|
||||
ViewCompat.setAlpha(newHolder.itemView, 0);
|
||||
}
|
||||
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||
final ViewHolder holder = changeInfo.oldHolder;
|
||||
final View view = holder.itemView;
|
||||
final ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
|
||||
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
|
||||
getChangeDuration());
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
oldViewAnim.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||
mChangeAnimations.remove(changeInfo.oldHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
if (newView != null) {
|
||||
mChangeAnimations.add(changeInfo.newHolder);
|
||||
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
|
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
|
||||
alpha(1).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
newViewAnimation.setListener(null);
|
||||
ViewCompat.setAlpha(newView, 1);
|
||||
ViewCompat.setTranslationX(newView, 0);
|
||||
ViewCompat.setTranslationY(newView, 0);
|
||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||
mChangeAnimations.remove(changeInfo.newHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
|
||||
for (int i = infoList.size() - 1; i >= 0; i--) {
|
||||
ChangeInfo changeInfo = infoList.get(i);
|
||||
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||
infoList.remove(changeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
|
||||
if (changeInfo.oldHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
||||
}
|
||||
if (changeInfo.newHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
||||
}
|
||||
}
|
||||
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
|
||||
boolean oldItem = false;
|
||||
if (changeInfo.newHolder == item) {
|
||||
changeInfo.newHolder = null;
|
||||
} else if (changeInfo.oldHolder == item) {
|
||||
changeInfo.oldHolder = null;
|
||||
oldItem = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
ViewCompat.setAlpha(item.itemView, 1);
|
||||
ViewCompat.setTranslationX(item.itemView, 0);
|
||||
ViewCompat.setTranslationY(item.itemView, 0);
|
||||
dispatchChangeFinished(item, oldItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimation(ViewHolder item) {
|
||||
final View view = item.itemView;
|
||||
// this will trigger end callback which should set properties to their target values.
|
||||
ViewCompat.animate(view).cancel();
|
||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
||||
MoveInfo moveInfo = mPendingMoves.get(i);
|
||||
if (moveInfo.holder == item) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
mPendingMoves.remove(item);
|
||||
}
|
||||
}
|
||||
endChangeAnimation(mPendingChanges, item);
|
||||
if (mPendingRemovals.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchRemoveFinished(item);
|
||||
}
|
||||
if (mPendingAdditions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
}
|
||||
|
||||
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
endChangeAnimation(changes, item);
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
}
|
||||
for (int i = mMovesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
for (int j = moves.size() - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
if (moveInfo.holder == item) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
if (additions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animations should be ended by the cancel above.
|
||||
if (mRemoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mRemoveAnimations list");
|
||||
}
|
||||
|
||||
if (mAddAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mAddAnimations list");
|
||||
}
|
||||
|
||||
if (mChangeAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mChangeAnimations list");
|
||||
}
|
||||
|
||||
if (mMoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mMoveAnimations list");
|
||||
}
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (!mPendingAdditions.isEmpty() ||
|
||||
!mPendingChanges.isEmpty() ||
|
||||
!mPendingMoves.isEmpty() ||
|
||||
!mPendingRemovals.isEmpty() ||
|
||||
!mMoveAnimations.isEmpty() ||
|
||||
!mRemoveAnimations.isEmpty() ||
|
||||
!mAddAnimations.isEmpty() ||
|
||||
!mChangeAnimations.isEmpty() ||
|
||||
!mMovesList.isEmpty() ||
|
||||
!mAdditionsList.isEmpty() ||
|
||||
!mChangesList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of currently pending and running animations. If there are none
|
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||
* listeners.
|
||||
*/
|
||||
private void dispatchFinishedWhenDone() {
|
||||
if (!isRunning()) {
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimations() {
|
||||
int count = mPendingMoves.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
MoveInfo item = mPendingMoves.get(i);
|
||||
View view = item.holder.itemView;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item.holder);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
count = mPendingRemovals.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingRemovals.get(i);
|
||||
dispatchRemoveFinished(item);
|
||||
mPendingRemovals.remove(i);
|
||||
}
|
||||
count = mPendingAdditions.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingAdditions.get(i);
|
||||
View view = item.itemView;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
mPendingAdditions.remove(i);
|
||||
}
|
||||
count = mPendingChanges.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
||||
}
|
||||
mPendingChanges.clear();
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int listCount = mMovesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
count = moves.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
ViewHolder item = moveInfo.holder;
|
||||
View view = item.itemView;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(moveInfo.holder);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mAdditionsList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
count = additions.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
ViewHolder item = additions.get(j);
|
||||
View view = item.itemView;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
additions.remove(j);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mChangesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
count = changes.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
endChangeAnimationIfNecessary(changes.get(j));
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelAll(mRemoveAnimations);
|
||||
cancelAll(mMoveAnimations);
|
||||
cancelAll(mAddAnimations);
|
||||
cancelAll(mChangeAnimations);
|
||||
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
|
||||
void cancelAll(List<ViewHolder> viewHolders) {
|
||||
for (int i = viewHolders.size() - 1; i >= 0; i--) {
|
||||
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,816 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* 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 languag`e governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
|
||||
* <p>
|
||||
* By default, each item occupies 1 span. You can change it by providing a custom
|
||||
* {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
|
||||
*/
|
||||
public class GridLayoutManager extends LinearLayoutManager {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "GridLayoutManager";
|
||||
public static final int DEFAULT_SPAN_COUNT = -1;
|
||||
/**
|
||||
* The measure spec for the scroll direction.
|
||||
*/
|
||||
static final int MAIN_DIR_SPEC =
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
int mSpanCount = DEFAULT_SPAN_COUNT;
|
||||
/**
|
||||
* The size of each span
|
||||
*/
|
||||
int mSizePerSpan;
|
||||
/**
|
||||
* Temporary array to keep views in layoutChunk method
|
||||
*/
|
||||
View[] mSet;
|
||||
final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
|
||||
final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
|
||||
SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
|
||||
// re-used variable to acquire decor insets from RecyclerView
|
||||
final Rect mDecorInsets = new Rect();
|
||||
|
||||
/**
|
||||
* Creates a vertical GridLayoutManager
|
||||
*
|
||||
* @param context Current context, will be used to access resources.
|
||||
* @param spanCount The number of columns in the grid
|
||||
*/
|
||||
public GridLayoutManager(Context context, int spanCount) {
|
||||
super(context);
|
||||
setSpanCount(spanCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context Current context, will be used to access resources.
|
||||
* @param spanCount The number of columns or rows in the grid
|
||||
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
|
||||
* #VERTICAL}.
|
||||
* @param reverseLayout When set to true, layouts from end to start.
|
||||
*/
|
||||
public GridLayoutManager(Context context, int spanCount, int orientation,
|
||||
boolean reverseLayout) {
|
||||
super(context, orientation, reverseLayout);
|
||||
setSpanCount(spanCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* stackFromEnd is not supported by GridLayoutManager. Consider using
|
||||
* {@link #setReverseLayout(boolean)}.
|
||||
*/
|
||||
@Override
|
||||
public void setStackFromEnd(boolean stackFromEnd) {
|
||||
if (stackFromEnd) {
|
||||
throw new UnsupportedOperationException(
|
||||
"GridLayoutManager does not support stack from end."
|
||||
+ " Consider using reverse layout");
|
||||
}
|
||||
super.setStackFromEnd(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
|
||||
RecyclerView.State state) {
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
return mSpanCount;
|
||||
}
|
||||
if (state.getItemCount() < 1) {
|
||||
return 0;
|
||||
}
|
||||
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
|
||||
RecyclerView.State state) {
|
||||
if (mOrientation == VERTICAL) {
|
||||
return mSpanCount;
|
||||
}
|
||||
if (state.getItemCount() < 1) {
|
||||
return 0;
|
||||
}
|
||||
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
|
||||
RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
|
||||
ViewGroup.LayoutParams lp = host.getLayoutParams();
|
||||
if (!(lp instanceof LayoutParams)) {
|
||||
super.onInitializeAccessibilityNodeInfoForItem(host, info);
|
||||
return;
|
||||
}
|
||||
LayoutParams glp = (LayoutParams) lp;
|
||||
int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewPosition());
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
|
||||
glp.getSpanIndex(), glp.getSpanSize(),
|
||||
spanGroupIndex, 1,
|
||||
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
|
||||
} else { // VERTICAL
|
||||
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
|
||||
spanGroupIndex , 1,
|
||||
glp.getSpanIndex(), glp.getSpanSize(),
|
||||
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
if (state.isPreLayout()) {
|
||||
cachePreLayoutSpanMapping();
|
||||
}
|
||||
super.onLayoutChildren(recycler, state);
|
||||
if (DEBUG) {
|
||||
validateChildOrder();
|
||||
}
|
||||
clearPreLayoutSpanMappingCache();
|
||||
}
|
||||
|
||||
private void clearPreLayoutSpanMappingCache() {
|
||||
mPreLayoutSpanSizeCache.clear();
|
||||
mPreLayoutSpanIndexCache.clear();
|
||||
}
|
||||
|
||||
private void cachePreLayoutSpanMapping() {
|
||||
final int childCount = getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
|
||||
final int viewPosition = lp.getViewPosition();
|
||||
mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
|
||||
mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsChanged(RecyclerView recyclerView) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
|
||||
return new LayoutParams(c, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
|
||||
if (lp instanceof ViewGroup.MarginLayoutParams) {
|
||||
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
|
||||
} else {
|
||||
return new LayoutParams(lp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
|
||||
return lp instanceof LayoutParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source to get the number of spans occupied by each item in the adapter.
|
||||
*
|
||||
* @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
|
||||
* occupied by each item
|
||||
*/
|
||||
public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
|
||||
mSpanSizeLookup = spanSizeLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
|
||||
*
|
||||
* @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
|
||||
*/
|
||||
public SpanSizeLookup getSpanSizeLookup() {
|
||||
return mSpanSizeLookup;
|
||||
}
|
||||
|
||||
private void updateMeasurements() {
|
||||
int totalSpace;
|
||||
if (getOrientation() == VERTICAL) {
|
||||
totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
|
||||
} else {
|
||||
totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||
}
|
||||
mSizePerSpan = totalSpace / mSpanCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo) {
|
||||
super.onAnchorReady(state, anchorInfo);
|
||||
updateMeasurements();
|
||||
if (state.getItemCount() > 0 && !state.isPreLayout()) {
|
||||
ensureAnchorIsInFirstSpan(anchorInfo);
|
||||
}
|
||||
if (mSet == null || mSet.length != mSpanCount) {
|
||||
mSet = new View[mSpanCount];
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo) {
|
||||
int span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
|
||||
while (span > 0 && anchorInfo.mPosition > 0) {
|
||||
anchorInfo.mPosition--;
|
||||
span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
|
||||
}
|
||||
}
|
||||
|
||||
private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
|
||||
int viewPosition) {
|
||||
if (!state.isPreLayout()) {
|
||||
return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
|
||||
}
|
||||
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
|
||||
if (adapterPosition == -1) {
|
||||
if (DEBUG) {
|
||||
throw new RuntimeException("Cannot find span group index for position "
|
||||
+ viewPosition);
|
||||
}
|
||||
Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
|
||||
return 0;
|
||||
}
|
||||
return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
|
||||
}
|
||||
|
||||
private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
|
||||
if (!state.isPreLayout()) {
|
||||
return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
|
||||
}
|
||||
final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
|
||||
if (cached != -1) {
|
||||
return cached;
|
||||
}
|
||||
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
|
||||
if (adapterPosition == -1) {
|
||||
if (DEBUG) {
|
||||
throw new RuntimeException("Cannot find span index for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
}
|
||||
Log.w(TAG, "Cannot find span size for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
return 0;
|
||||
}
|
||||
return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
|
||||
}
|
||||
|
||||
private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
|
||||
if (!state.isPreLayout()) {
|
||||
return mSpanSizeLookup.getSpanSize(pos);
|
||||
}
|
||||
final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
|
||||
if (cached != -1) {
|
||||
return cached;
|
||||
}
|
||||
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
|
||||
if (adapterPosition == -1) {
|
||||
if (DEBUG) {
|
||||
throw new RuntimeException("Cannot find span size for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
}
|
||||
Log.w(TAG, "Cannot find span size for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
return 1;
|
||||
}
|
||||
return mSpanSizeLookup.getSpanSize(adapterPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
|
||||
LayoutState layoutState, LayoutChunkResult result) {
|
||||
final boolean layingOutInPrimaryDirection =
|
||||
layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
|
||||
int count = 0;
|
||||
int consumedSpanCount = 0;
|
||||
int remainingSpan = mSpanCount;
|
||||
if (!layingOutInPrimaryDirection) {
|
||||
int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
|
||||
int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
|
||||
remainingSpan = itemSpanIndex + itemSpanSize;
|
||||
}
|
||||
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
|
||||
int pos = layoutState.mCurrentPosition;
|
||||
final int spanSize = getSpanSize(recycler, state, pos);
|
||||
if (spanSize > mSpanCount) {
|
||||
throw new IllegalArgumentException("Item at position " + pos + " requires " +
|
||||
spanSize + " spans but GridLayoutManager has only " + mSpanCount
|
||||
+ " spans.");
|
||||
}
|
||||
remainingSpan -= spanSize;
|
||||
if (remainingSpan < 0) {
|
||||
break; // item did not fit into this row or column
|
||||
}
|
||||
View view = layoutState.next(recycler);
|
||||
if (view == null) {
|
||||
break;
|
||||
}
|
||||
consumedSpanCount += spanSize;
|
||||
mSet[count] = view;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
result.mFinished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int maxSize = 0;
|
||||
|
||||
// we should assign spans before item decor offsets are calculated
|
||||
assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
|
||||
for (int i = 0; i < count; i++) {
|
||||
View view = mSet[i];
|
||||
if (layoutState.mScrapList == null) {
|
||||
if (layingOutInPrimaryDirection) {
|
||||
addView(view);
|
||||
} else {
|
||||
addView(view, 0);
|
||||
}
|
||||
} else {
|
||||
if (layingOutInPrimaryDirection) {
|
||||
addDisappearingView(view);
|
||||
} else {
|
||||
addDisappearingView(view, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int spanSize = getSpanSize(recycler, state, getPosition(view));
|
||||
final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
|
||||
View.MeasureSpec.EXACTLY);
|
||||
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
||||
if (mOrientation == VERTICAL) {
|
||||
measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height));
|
||||
} else {
|
||||
measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec);
|
||||
}
|
||||
final int size = mOrientationHelper.getDecoratedMeasurement(view);
|
||||
if (size > maxSize) {
|
||||
maxSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
// views that did not measure the maxSize has to be re-measured
|
||||
final int maxMeasureSpec = getMainDirSpec(maxSize);
|
||||
for (int i = 0; i < count; i ++) {
|
||||
final View view = mSet[i];
|
||||
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
|
||||
int spanSize = getSpanSize(recycler, state, getPosition(view));
|
||||
final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
|
||||
View.MeasureSpec.EXACTLY);
|
||||
if (mOrientation == VERTICAL) {
|
||||
measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec);
|
||||
} else {
|
||||
measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.mConsumed = maxSize;
|
||||
|
||||
int left = 0, right = 0, top = 0, bottom = 0;
|
||||
if (mOrientation == VERTICAL) {
|
||||
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
|
||||
bottom = layoutState.mOffset;
|
||||
top = bottom - maxSize;
|
||||
} else {
|
||||
top = layoutState.mOffset;
|
||||
bottom = top + maxSize;
|
||||
}
|
||||
} else {
|
||||
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
|
||||
right = layoutState.mOffset;
|
||||
left = right - maxSize;
|
||||
} else {
|
||||
left = layoutState.mOffset;
|
||||
right = left + maxSize;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
View view = mSet[i];
|
||||
LayoutParams params = (LayoutParams) view.getLayoutParams();
|
||||
if (mOrientation == VERTICAL) {
|
||||
left = getPaddingLeft() + mSizePerSpan * params.mSpanIndex;
|
||||
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
|
||||
} else {
|
||||
top = getPaddingTop() + mSizePerSpan * params.mSpanIndex;
|
||||
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
|
||||
}
|
||||
// We calculate everything with View's bounding box (which includes decor and margins)
|
||||
// To calculate correct layout position, we subtract margins.
|
||||
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
|
||||
right - params.rightMargin, bottom - params.bottomMargin);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
|
||||
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
|
||||
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
|
||||
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
|
||||
}
|
||||
// Consume the available space if the view is not removed OR changed
|
||||
if (params.isItemRemoved() || params.isItemChanged()) {
|
||||
result.mIgnoreConsumed = true;
|
||||
}
|
||||
result.mFocusable |= view.isFocusable();
|
||||
}
|
||||
Arrays.fill(mSet, null);
|
||||
}
|
||||
|
||||
private int getMainDirSpec(int dim) {
|
||||
if (dim < 0) {
|
||||
return MAIN_DIR_SPEC;
|
||||
} else {
|
||||
return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
|
||||
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
|
||||
calculateItemDecorationsForChild(child, mDecorInsets);
|
||||
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
|
||||
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
|
||||
lp.rightMargin + mDecorInsets.right);
|
||||
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
|
||||
lp.bottomMargin + mDecorInsets.bottom);
|
||||
child.measure(widthSpec, heightSpec);
|
||||
}
|
||||
|
||||
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
|
||||
if (startInset == 0 && endInset == 0) {
|
||||
return spec;
|
||||
}
|
||||
final int mode = View.MeasureSpec.getMode(spec);
|
||||
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
|
||||
return View.MeasureSpec.makeMeasureSpec(
|
||||
View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
|
||||
int consumedSpanCount, boolean layingOutInPrimaryDirection) {
|
||||
int span, spanDiff, start, end, diff;
|
||||
// make sure we traverse from min position to max position
|
||||
if (layingOutInPrimaryDirection) {
|
||||
start = 0;
|
||||
end = count;
|
||||
diff = 1;
|
||||
} else {
|
||||
start = count - 1;
|
||||
end = -1;
|
||||
diff = -1;
|
||||
}
|
||||
if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
|
||||
span = consumedSpanCount - 1;
|
||||
spanDiff = -1;
|
||||
} else {
|
||||
span = 0;
|
||||
spanDiff = 1;
|
||||
}
|
||||
for (int i = start; i != end; i += diff) {
|
||||
View view = mSet[i];
|
||||
LayoutParams params = (LayoutParams) view.getLayoutParams();
|
||||
params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
|
||||
if (spanDiff == -1 && params.mSpanSize > 1) {
|
||||
params.mSpanIndex = span - (params.mSpanSize - 1);
|
||||
} else {
|
||||
params.mSpanIndex = span;
|
||||
}
|
||||
span += spanDiff * params.mSpanSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of spans laid out by this grid.
|
||||
*
|
||||
* @return The number of spans
|
||||
* @see #setSpanCount(int)
|
||||
*/
|
||||
public int getSpanCount() {
|
||||
return mSpanCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of spans to be laid out.
|
||||
* <p>
|
||||
* If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
|
||||
* If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
|
||||
*
|
||||
* @param spanCount The total number of spans in the grid
|
||||
* @see #getSpanCount()
|
||||
*/
|
||||
public void setSpanCount(int spanCount) {
|
||||
if (spanCount == mSpanCount) {
|
||||
return;
|
||||
}
|
||||
if (spanCount < 1) {
|
||||
throw new IllegalArgumentException("Span count should be at least 1. Provided "
|
||||
+ spanCount);
|
||||
}
|
||||
mSpanCount = spanCount;
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class to provide the number of spans each item occupies.
|
||||
* <p>
|
||||
* Default implementation sets each item to occupy exactly 1 span.
|
||||
*
|
||||
* @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
|
||||
*/
|
||||
public static abstract class SpanSizeLookup {
|
||||
|
||||
final SparseIntArray mSpanIndexCache = new SparseIntArray();
|
||||
|
||||
private boolean mCacheSpanIndices = false;
|
||||
|
||||
/**
|
||||
* Returns the number of span occupied by the item at <code>position</code>.
|
||||
*
|
||||
* @param position The adapter position of the item
|
||||
* @return The number of spans occupied by the item at the provided position
|
||||
*/
|
||||
abstract public int getSpanSize(int position);
|
||||
|
||||
/**
|
||||
* Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
|
||||
* not. By default these values are not cached. If you are not overriding
|
||||
* {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
|
||||
*
|
||||
* @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
|
||||
*/
|
||||
public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
|
||||
mCacheSpanIndices = cacheSpanIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the span index cache. GridLayoutManager automatically calls this method when
|
||||
* adapter changes occur.
|
||||
*/
|
||||
public void invalidateSpanIndexCache() {
|
||||
mSpanIndexCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
|
||||
*
|
||||
* @return True if results of {@link #getSpanIndex(int, int)} are cached.
|
||||
*/
|
||||
public boolean isSpanIndexCacheEnabled() {
|
||||
return mCacheSpanIndices;
|
||||
}
|
||||
|
||||
int getCachedSpanIndex(int position, int spanCount) {
|
||||
if (!mCacheSpanIndices) {
|
||||
return getSpanIndex(position, spanCount);
|
||||
}
|
||||
final int existing = mSpanIndexCache.get(position, -1);
|
||||
if (existing != -1) {
|
||||
return existing;
|
||||
}
|
||||
final int value = getSpanIndex(position, spanCount);
|
||||
mSpanIndexCache.put(position, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final span index of the provided position.
|
||||
* <p>
|
||||
* If you have a faster way to calculate span index for your items, you should override
|
||||
* this method. Otherwise, you should enable span index cache
|
||||
* ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
|
||||
* disabled, default implementation traverses all items from 0 to
|
||||
* <code>position</code>. When caching is enabled, it calculates from the closest cached
|
||||
* value before the <code>position</code>.
|
||||
* <p>
|
||||
* If you override this method, you need to make sure it is consistent with
|
||||
* {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
|
||||
* each item. It is called only for the reference item and rest of the items
|
||||
* are assigned to spans based on the reference item. For example, you cannot assign a
|
||||
* position to span 2 while span 1 is empty.
|
||||
* <p>
|
||||
* Note that span offsets always start with 0 and are not affected by RTL.
|
||||
*
|
||||
* @param position The position of the item
|
||||
* @param spanCount The total number of spans in the grid
|
||||
* @return The final span position of the item. Should be between 0 (inclusive) and
|
||||
* <code>spanCount</code>(exclusive)
|
||||
*/
|
||||
public int getSpanIndex(int position, int spanCount) {
|
||||
int positionSpanSize = getSpanSize(position);
|
||||
if (positionSpanSize == spanCount) {
|
||||
return 0; // quick return for full-span items
|
||||
}
|
||||
int span = 0;
|
||||
int startPos = 0;
|
||||
// If caching is enabled, try to jump
|
||||
if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
|
||||
int prevKey = findReferenceIndexFromCache(position);
|
||||
if (prevKey >= 0) {
|
||||
span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
|
||||
startPos = prevKey + 1;
|
||||
}
|
||||
}
|
||||
for (int i = startPos; i < position; i++) {
|
||||
int size = getSpanSize(i);
|
||||
span += size;
|
||||
if (span == spanCount) {
|
||||
span = 0;
|
||||
} else if (span > spanCount) {
|
||||
// did not fit, moving to next row / column
|
||||
span = size;
|
||||
}
|
||||
}
|
||||
if (span + positionSpanSize <= spanCount) {
|
||||
return span;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int findReferenceIndexFromCache(int position) {
|
||||
int lo = 0;
|
||||
int hi = mSpanIndexCache.size() - 1;
|
||||
|
||||
while (lo <= hi) {
|
||||
final int mid = (lo + hi) >>> 1;
|
||||
final int midVal = mSpanIndexCache.keyAt(mid);
|
||||
if (midVal < position) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
int index = lo - 1;
|
||||
if (index >= 0 && index < mSpanIndexCache.size()) {
|
||||
return mSpanIndexCache.keyAt(index);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the group this position belongs.
|
||||
* <p>
|
||||
* For example, if grid has 3 columns and each item occupies 1 span, span group index
|
||||
* for item 1 will be 0, item 5 will be 1.
|
||||
*
|
||||
* @param adapterPosition The position in adapter
|
||||
* @param spanCount The total number of spans in the grid
|
||||
* @return The index of the span group including the item at the given adapter position
|
||||
*/
|
||||
public int getSpanGroupIndex(int adapterPosition, int spanCount) {
|
||||
int span = 0;
|
||||
int group = 0;
|
||||
int positionSpanSize = getSpanSize(adapterPosition);
|
||||
for (int i = 0; i < adapterPosition; i++) {
|
||||
int size = getSpanSize(i);
|
||||
span += size;
|
||||
if (span == spanCount) {
|
||||
span = 0;
|
||||
group++;
|
||||
} else if (span > spanCount) {
|
||||
// did not fit, moving to next row / column
|
||||
span = size;
|
||||
group++;
|
||||
}
|
||||
}
|
||||
if (span + positionSpanSize > spanCount) {
|
||||
group++;
|
||||
}
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPredictiveItemAnimations() {
|
||||
return mPendingSavedState == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
|
||||
*/
|
||||
public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
|
||||
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpanIndex(int position, int spanCount) {
|
||||
return position % spanCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LayoutParams used by GridLayoutManager.
|
||||
*/
|
||||
public static class LayoutParams extends RecyclerView.LayoutParams {
|
||||
|
||||
/**
|
||||
* Span Id for Views that are not laid out yet.
|
||||
*/
|
||||
public static final int INVALID_SPAN_ID = -1;
|
||||
|
||||
private int mSpanIndex = INVALID_SPAN_ID;
|
||||
|
||||
private int mSpanSize = 0;
|
||||
|
||||
public LayoutParams(Context c, AttributeSet attrs) {
|
||||
super(c, attrs);
|
||||
}
|
||||
|
||||
public LayoutParams(int width, int height) {
|
||||
super(width, height);
|
||||
}
|
||||
|
||||
public LayoutParams(ViewGroup.MarginLayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
public LayoutParams(ViewGroup.LayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
public LayoutParams(RecyclerView.LayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current span index of this View. If the View is not laid out yet, the return
|
||||
* value is <code>undefined</code>.
|
||||
* <p>
|
||||
* Note that span index may change by whether the RecyclerView is RTL or not. For
|
||||
* example, if the number of spans is 3 and layout is RTL, the rightmost item will have
|
||||
* span index of 2. If the layout changes back to LTR, span index for this view will be 0.
|
||||
* If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
|
||||
* <p>
|
||||
* If the View occupies multiple spans, span with the minimum index is returned.
|
||||
*
|
||||
* @return The span index of the View.
|
||||
*/
|
||||
public int getSpanIndex() {
|
||||
return mSpanIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of spans occupied by this View. If the View not laid out yet, the
|
||||
* return value is <code>undefined</code>.
|
||||
*
|
||||
* @return The number of spans occupied by this View.
|
||||
*/
|
||||
public int getSpanSize() {
|
||||
return mSpanSize;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
87
app/src/main/java/android/support/v7/widget/LayoutState.java
Normal file
87
app/src/main/java/android/support/v7/widget/LayoutState.java
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* 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 languag`e governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
|
||||
* space.
|
||||
*/
|
||||
class LayoutState {
|
||||
|
||||
final static String TAG = "LayoutState";
|
||||
|
||||
final static int LAYOUT_START = -1;
|
||||
|
||||
final static int LAYOUT_END = 1;
|
||||
|
||||
final static int INVALID_LAYOUT = Integer.MIN_VALUE;
|
||||
|
||||
final static int ITEM_DIRECTION_HEAD = -1;
|
||||
|
||||
final static int ITEM_DIRECTION_TAIL = 1;
|
||||
|
||||
final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
|
||||
|
||||
/**
|
||||
* Number of pixels that we should fill, in the layout direction.
|
||||
*/
|
||||
int mAvailable;
|
||||
|
||||
/**
|
||||
* Current position on the adapter to get the next item.
|
||||
*/
|
||||
int mCurrentPosition;
|
||||
|
||||
/**
|
||||
* Defines the direction in which the data adapter is traversed.
|
||||
* Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
|
||||
*/
|
||||
int mItemDirection;
|
||||
|
||||
/**
|
||||
* Defines the direction in which the layout is filled.
|
||||
* Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
|
||||
*/
|
||||
int mLayoutDirection;
|
||||
|
||||
/**
|
||||
* Used if you want to pre-layout items that are not yet visible.
|
||||
* The difference with {@link #mAvailable} is that, when recycling, distance rendered for
|
||||
* {@link #mExtra} is not considered not to recycle visible children.
|
||||
*/
|
||||
int mExtra = 0;
|
||||
|
||||
/**
|
||||
* @return true if there are more items in the data adapter
|
||||
*/
|
||||
boolean hasMore(RecyclerView.State state) {
|
||||
return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the view for the next element that we should render.
|
||||
* Also updates current item index to the next item, based on {@link #mItemDirection}
|
||||
*
|
||||
* @return The next element that we should render.
|
||||
*/
|
||||
View next(RecyclerView.Recycler recycler) {
|
||||
final View view = recycler.getViewForPosition(mCurrentPosition);
|
||||
mCurrentPosition += mItemDirection;
|
||||
return view;
|
||||
}
|
||||
}
|
||||
1981
app/src/main/java/android/support/v7/widget/LinearLayoutManager.java
Normal file
1981
app/src/main/java/android/support/v7/widget/LinearLayoutManager.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
/**
|
||||
* {@link RecyclerView.SmoothScroller} implementation which uses
|
||||
* {@link LinearInterpolator} until the target position becames a child of
|
||||
* the RecyclerView and then uses
|
||||
* {@link DecelerateInterpolator} to slowly approach to target position.
|
||||
*/
|
||||
abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
|
||||
|
||||
private static final String TAG = "LinearSmoothScroller";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final float MILLISECONDS_PER_INCH = 25f;
|
||||
|
||||
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
|
||||
|
||||
/**
|
||||
* Align child view's left or top with parent view's left or top
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(View, int)
|
||||
* @see #calculateDyToMakeVisible(View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_START = -1;
|
||||
|
||||
/**
|
||||
* Align child view's right or bottom with parent view's right or bottom
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(View, int)
|
||||
* @see #calculateDyToMakeVisible(View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_END = 1;
|
||||
|
||||
/**
|
||||
* <p>Decides if the child should be snapped from start or end, depending on where it
|
||||
* currently is in relation to its parent.</p>
|
||||
* <p>For instance, if the view is virtually on the left of RecyclerView, using
|
||||
* {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(View, int)
|
||||
* @see #calculateDyToMakeVisible(View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_ANY = 0;
|
||||
|
||||
// Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
|
||||
// view is not laid out until interim target position is reached, we can detect the case before
|
||||
// scrolling slows down and reschedule another interim target scroll
|
||||
private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
|
||||
|
||||
protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
|
||||
|
||||
protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
|
||||
|
||||
protected PointF mTargetVector;
|
||||
|
||||
private final float MILLISECONDS_PER_PX;
|
||||
|
||||
// Temporary variables to keep track of the interim scroll target. These values do not
|
||||
// point to a real item position, rather point to an estimated location pixels.
|
||||
protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
|
||||
|
||||
public LinearSmoothScroller(Context context) {
|
||||
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onStart() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
|
||||
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
|
||||
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
|
||||
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
|
||||
final int time = calculateTimeForDeceleration(distance);
|
||||
if (time > 0) {
|
||||
action.update(-dx, -dy, time, mDecelerateInterpolator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
|
||||
if (getChildCount() == 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (DEBUG && mTargetVector != null
|
||||
&& ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
|
||||
throw new IllegalStateException("Scroll happened in the opposite direction"
|
||||
+ " of the target. Some calculations are wrong");
|
||||
}
|
||||
mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
|
||||
mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
|
||||
|
||||
if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
|
||||
updateActionForInterimTarget(action);
|
||||
} // everything is valid, keep going
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onStop() {
|
||||
mInterimTargetDx = mInterimTargetDy = 0;
|
||||
mTargetVector = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scroll speed.
|
||||
*
|
||||
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
|
||||
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
|
||||
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
|
||||
*/
|
||||
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
||||
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Calculates the time for deceleration so that transition from LinearInterpolator to
|
||||
* DecelerateInterpolator looks smooth.</p>
|
||||
*
|
||||
* @param dx Distance to scroll
|
||||
* @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
|
||||
* from LinearInterpolation
|
||||
*/
|
||||
protected int calculateTimeForDeceleration(int dx) {
|
||||
// we want to cover same area with the linear interpolator for the first 10% of the
|
||||
// interpolation. After that, deceleration will take control.
|
||||
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
|
||||
// which gives 0.100028 when x = .3356
|
||||
// this is why we divide linear scrolling time with .3356
|
||||
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time it should take to scroll the given distance (in pixels)
|
||||
*
|
||||
* @param dx Distance in pixels that we want to scroll
|
||||
* @return Time in milliseconds
|
||||
* @see #calculateSpeedPerPixel(DisplayMetrics)
|
||||
*/
|
||||
protected int calculateTimeForScrolling(int dx) {
|
||||
// In a case where dx is very small, rounding may return 0 although dx > 0.
|
||||
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
|
||||
// time.
|
||||
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling towards a child view, this method defines whether we should align the left
|
||||
* or the right edge of the child with the parent RecyclerView.
|
||||
*
|
||||
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
|
||||
* @see #SNAP_TO_START
|
||||
* @see #SNAP_TO_END
|
||||
* @see #SNAP_TO_ANY
|
||||
*/
|
||||
protected int getHorizontalSnapPreference() {
|
||||
return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
|
||||
mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling towards a child view, this method defines whether we should align the top
|
||||
* or the bottom edge of the child with the parent RecyclerView.
|
||||
*
|
||||
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
|
||||
* @see #SNAP_TO_START
|
||||
* @see #SNAP_TO_END
|
||||
* @see #SNAP_TO_ANY
|
||||
*/
|
||||
protected int getVerticalSnapPreference() {
|
||||
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
|
||||
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the target scroll position is not a child of the RecyclerView, this method calculates
|
||||
* a direction vector towards that child and triggers a smooth scroll.
|
||||
*
|
||||
* @see #computeScrollVectorForPosition(int)
|
||||
*/
|
||||
protected void updateActionForInterimTarget(Action action) {
|
||||
// find an interim target position
|
||||
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
|
||||
if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
|
||||
Log.e(TAG, "To support smooth scrolling, you should override \n"
|
||||
+ "LayoutManager#computeScrollVectorForPosition.\n"
|
||||
+ "Falling back to instant scroll");
|
||||
final int target = getTargetPosition();
|
||||
stop();
|
||||
instantScrollToPosition(target);
|
||||
return;
|
||||
}
|
||||
normalize(scrollVector);
|
||||
mTargetVector = scrollVector;
|
||||
|
||||
mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
|
||||
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
|
||||
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
|
||||
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
|
||||
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
|
||||
// won't actually scroll more than what we need.
|
||||
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
|
||||
, (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
|
||||
, (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
|
||||
}
|
||||
|
||||
private int clampApplyScroll(int tmpDt, int dt) {
|
||||
final int before = tmpDt;
|
||||
tmpDt -= dt;
|
||||
if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
|
||||
return 0;
|
||||
}
|
||||
return tmpDt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for {@link #calculateDxToMakeVisible(View, int)} and
|
||||
* {@link #calculateDyToMakeVisible(View, int)}
|
||||
*/
|
||||
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
|
||||
snapPreference) {
|
||||
switch (snapPreference) {
|
||||
case SNAP_TO_START:
|
||||
return boxStart - viewStart;
|
||||
case SNAP_TO_END:
|
||||
return boxEnd - viewEnd;
|
||||
case SNAP_TO_ANY:
|
||||
final int dtStart = boxStart - viewStart;
|
||||
if (dtStart > 0) {
|
||||
return dtStart;
|
||||
}
|
||||
final int dtEnd = boxEnd - viewEnd;
|
||||
if (dtEnd < 0) {
|
||||
return dtEnd;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("snap preference should be one of the"
|
||||
+ " constants defined in SmoothScroller, starting with SNAP_");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the vertical scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_END}.
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDyToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (!layoutManager.canScrollVertically()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
|
||||
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
|
||||
final int start = layoutManager.getPaddingTop();
|
||||
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
|
||||
return calculateDtToFit(top, bottom, start, end, snapPreference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the horizontal scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_END}
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDxToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (!layoutManager.canScrollHorizontally()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
|
||||
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
|
||||
final int start = layoutManager.getPaddingLeft();
|
||||
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
|
||||
return calculateDtToFit(left, right, start, end, snapPreference);
|
||||
}
|
||||
|
||||
abstract public PointF computeScrollVectorForPosition(int targetPosition);
|
||||
}
|
||||
238
app/src/main/java/android/support/v7/widget/OpReorderer.java
Normal file
238
app/src/main/java/android/support/v7/widget/OpReorderer.java
Normal file
@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.support.v7.widget.AdapterHelper.UpdateOp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.ADD;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.MOVE;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.REMOVE;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.UPDATE;
|
||||
|
||||
class OpReorderer {
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
public OpReorderer(Callback callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
void reorderOps(List<UpdateOp> ops) {
|
||||
// since move operations breaks continuity, their effects on ADD/RM are hard to handle.
|
||||
// we push them to the end of the list so that they can be handled easily.
|
||||
int badMove;
|
||||
while ((badMove = getLastMoveOutOfOrder(ops)) != -1) {
|
||||
swapMoveOp(ops, badMove, badMove + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void swapMoveOp(List<UpdateOp> list, int badMove, int next) {
|
||||
final UpdateOp moveOp = list.get(badMove);
|
||||
final UpdateOp nextOp = list.get(next);
|
||||
switch (nextOp.cmd) {
|
||||
case REMOVE:
|
||||
swapMoveRemove(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
case ADD:
|
||||
swapMoveAdd(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
case UPDATE:
|
||||
swapMoveUpdate(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp,
|
||||
int removePos, UpdateOp removeOp) {
|
||||
UpdateOp extraRm = null;
|
||||
// check if move is nulled out by remove
|
||||
boolean revertedMove = false;
|
||||
final boolean moveIsBackwards;
|
||||
|
||||
if (moveOp.positionStart < moveOp.itemCount) {
|
||||
moveIsBackwards = false;
|
||||
if (removeOp.positionStart == moveOp.positionStart
|
||||
&& removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
|
||||
revertedMove = true;
|
||||
}
|
||||
} else {
|
||||
moveIsBackwards = true;
|
||||
if (removeOp.positionStart == moveOp.itemCount + 1 &&
|
||||
removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
|
||||
revertedMove = true;
|
||||
}
|
||||
}
|
||||
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < removeOp.positionStart) {
|
||||
removeOp.positionStart--;
|
||||
} else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
|
||||
// move is removed.
|
||||
removeOp.itemCount --;
|
||||
moveOp.cmd = REMOVE;
|
||||
moveOp.itemCount = 1;
|
||||
if (removeOp.itemCount == 0) {
|
||||
list.remove(removePos);
|
||||
mCallback.recycleUpdateOp(removeOp);
|
||||
}
|
||||
// no need to swap, it is already a remove
|
||||
return;
|
||||
}
|
||||
|
||||
// now affect of add is consumed. now apply effect of first remove
|
||||
if (moveOp.positionStart <= removeOp.positionStart) {
|
||||
removeOp.positionStart++;
|
||||
} else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
|
||||
final int remaining = removeOp.positionStart + removeOp.itemCount
|
||||
- moveOp.positionStart;
|
||||
extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining);
|
||||
removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
|
||||
}
|
||||
|
||||
// if effects of move is reverted by remove, we are done.
|
||||
if (revertedMove) {
|
||||
list.set(movePos, removeOp);
|
||||
list.remove(removePos);
|
||||
mCallback.recycleUpdateOp(moveOp);
|
||||
return;
|
||||
}
|
||||
|
||||
// now find out the new locations for move actions
|
||||
if (moveIsBackwards) {
|
||||
if (extraRm != null) {
|
||||
if (moveOp.positionStart > extraRm.positionStart) {
|
||||
moveOp.positionStart -= extraRm.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount > extraRm.positionStart) {
|
||||
moveOp.itemCount -= extraRm.itemCount;
|
||||
}
|
||||
}
|
||||
if (moveOp.positionStart > removeOp.positionStart) {
|
||||
moveOp.positionStart -= removeOp.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount > removeOp.positionStart) {
|
||||
moveOp.itemCount -= removeOp.itemCount;
|
||||
}
|
||||
} else {
|
||||
if (extraRm != null) {
|
||||
if (moveOp.positionStart >= extraRm.positionStart) {
|
||||
moveOp.positionStart -= extraRm.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount >= extraRm.positionStart) {
|
||||
moveOp.itemCount -= extraRm.itemCount;
|
||||
}
|
||||
}
|
||||
if (moveOp.positionStart >= removeOp.positionStart) {
|
||||
moveOp.positionStart -= removeOp.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount >= removeOp.positionStart) {
|
||||
moveOp.itemCount -= removeOp.itemCount;
|
||||
}
|
||||
}
|
||||
|
||||
list.set(movePos, removeOp);
|
||||
if (moveOp.positionStart != moveOp.itemCount) {
|
||||
list.set(removePos, moveOp);
|
||||
} else {
|
||||
list.remove(removePos);
|
||||
}
|
||||
if (extraRm != null) {
|
||||
list.add(movePos, extraRm);
|
||||
}
|
||||
}
|
||||
|
||||
private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add,
|
||||
UpdateOp addOp) {
|
||||
int offset = 0;
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < addOp.positionStart) {
|
||||
offset--;
|
||||
}
|
||||
if (moveOp.positionStart < addOp.positionStart) {
|
||||
offset++;
|
||||
}
|
||||
if (addOp.positionStart <= moveOp.positionStart) {
|
||||
moveOp.positionStart += addOp.itemCount;
|
||||
}
|
||||
if (addOp.positionStart <= moveOp.itemCount) {
|
||||
moveOp.itemCount += addOp.itemCount;
|
||||
}
|
||||
addOp.positionStart += offset;
|
||||
list.set(move, addOp);
|
||||
list.set(add, moveOp);
|
||||
}
|
||||
|
||||
void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update,
|
||||
UpdateOp updateOp) {
|
||||
UpdateOp extraUp1 = null;
|
||||
UpdateOp extraUp2 = null;
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < updateOp.positionStart) {
|
||||
updateOp.positionStart--;
|
||||
} else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
|
||||
// moved item is updated. add an update for it
|
||||
updateOp.itemCount--;
|
||||
extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1);
|
||||
}
|
||||
// now affect of add is consumed. now apply effect of first remove
|
||||
if (moveOp.positionStart <= updateOp.positionStart) {
|
||||
updateOp.positionStart++;
|
||||
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
|
||||
final int remaining = updateOp.positionStart + updateOp.itemCount
|
||||
- moveOp.positionStart;
|
||||
extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining);
|
||||
updateOp.itemCount -= remaining;
|
||||
}
|
||||
list.set(update, moveOp);
|
||||
if (updateOp.itemCount > 0) {
|
||||
list.set(move, updateOp);
|
||||
} else {
|
||||
list.remove(move);
|
||||
mCallback.recycleUpdateOp(updateOp);
|
||||
}
|
||||
if (extraUp1 != null) {
|
||||
list.add(move, extraUp1);
|
||||
}
|
||||
if (extraUp2 != null) {
|
||||
list.add(move, extraUp2);
|
||||
}
|
||||
}
|
||||
|
||||
private int getLastMoveOutOfOrder(List<UpdateOp> list) {
|
||||
boolean foundNonMove = false;
|
||||
for (int i = list.size() - 1; i >= 0; i--) {
|
||||
final UpdateOp op1 = list.get(i);
|
||||
if (op1.cmd == MOVE) {
|
||||
if (foundNonMove) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
foundNonMove = true;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static interface Callback {
|
||||
|
||||
UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount);
|
||||
|
||||
void recycleUpdateOp(UpdateOp op);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
/**
|
||||
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
|
||||
* <p>
|
||||
* It is developed to easily support vertical and horizontal orientations in a LayoutManager but
|
||||
* can also be used to abstract calls around view bounds and child measurements with margins and
|
||||
* decorations.
|
||||
*
|
||||
* @see #createHorizontalHelper(RecyclerView.LayoutManager)
|
||||
* @see #createVerticalHelper(RecyclerView.LayoutManager)
|
||||
*/
|
||||
public abstract class OrientationHelper {
|
||||
|
||||
private static final int INVALID_SIZE = Integer.MIN_VALUE;
|
||||
|
||||
protected final RecyclerView.LayoutManager mLayoutManager;
|
||||
|
||||
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
|
||||
|
||||
public static final int VERTICAL = LinearLayout.VERTICAL;
|
||||
|
||||
private int mLastTotalSpace = INVALID_SIZE;
|
||||
|
||||
private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
mLayoutManager = layoutManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method after onLayout method is complete if state is NOT pre-layout.
|
||||
* This method records information like layout bounds that might be useful in the next layout
|
||||
* calculations.
|
||||
*/
|
||||
public void onLayoutComplete() {
|
||||
mLastTotalSpace = getTotalSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the layout space change between the previous layout pass and current layout pass.
|
||||
* <p>
|
||||
* Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's
|
||||
* {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler,
|
||||
* RecyclerView.State)} method.
|
||||
*
|
||||
* @return The difference between the current total space and previous layout's total space.
|
||||
* @see #onLayoutComplete()
|
||||
*/
|
||||
public int getTotalSpaceChange() {
|
||||
return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start of the view including its decoration and margin.
|
||||
* <p>
|
||||
* For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left
|
||||
* decoration and 3px left margin, returned value will be 15px.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return The first pixel of the element
|
||||
* @see #getDecoratedEnd(View)
|
||||
*/
|
||||
public abstract int getDecoratedStart(View view);
|
||||
|
||||
/**
|
||||
* Returns the end of the view including its decoration and margin.
|
||||
* <p>
|
||||
* For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
|
||||
* decoration and 3px right margin, returned value will be 205.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return The last pixel of the element
|
||||
* @see #getDecoratedStart(View)
|
||||
*/
|
||||
public abstract int getDecoratedEnd(View view);
|
||||
|
||||
/**
|
||||
* Returns the space occupied by this View in the current orientation including decorations and
|
||||
* margins.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return Total space occupied by this view
|
||||
* @see #getDecoratedMeasurementInOther(View)
|
||||
*/
|
||||
public abstract int getDecoratedMeasurement(View view);
|
||||
|
||||
/**
|
||||
* Returns the space occupied by this View in the perpendicular orientation including
|
||||
* decorations and margins.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return Total space occupied by this view in the perpendicular orientation to current one
|
||||
* @see #getDecoratedMeasurement(View)
|
||||
*/
|
||||
public abstract int getDecoratedMeasurementInOther(View view);
|
||||
|
||||
/**
|
||||
* Returns the start position of the layout after the start padding is added.
|
||||
*
|
||||
* @return The very first pixel we can draw.
|
||||
*/
|
||||
public abstract int getStartAfterPadding();
|
||||
|
||||
/**
|
||||
* Returns the end position of the layout after the end padding is removed.
|
||||
*
|
||||
* @return The end boundary for this layout.
|
||||
*/
|
||||
public abstract int getEndAfterPadding();
|
||||
|
||||
/**
|
||||
* Returns the end position of the layout without taking padding into account.
|
||||
*
|
||||
* @return The end boundary for this layout without considering padding.
|
||||
*/
|
||||
public abstract int getEnd();
|
||||
|
||||
/**
|
||||
* Offsets all children's positions by the given amount.
|
||||
*
|
||||
* @param amount Value to add to each child's layout parameters
|
||||
*/
|
||||
public abstract void offsetChildren(int amount);
|
||||
|
||||
/**
|
||||
* Returns the total space to layout. This number is the difference between
|
||||
* {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
|
||||
*
|
||||
* @return Total space to layout children
|
||||
*/
|
||||
public abstract int getTotalSpace();
|
||||
|
||||
/**
|
||||
* Offsets the child in this orientation.
|
||||
*
|
||||
* @param view View to offset
|
||||
* @param offset offset amount
|
||||
*/
|
||||
public abstract void offsetChild(View view, int offset);
|
||||
|
||||
/**
|
||||
* Returns the padding at the end of the layout. For horizontal helper, this is the right
|
||||
* padding and for vertical helper, this is the bottom padding. This method does not check
|
||||
* whether the layout is RTL or not.
|
||||
*
|
||||
* @return The padding at the end of the layout.
|
||||
*/
|
||||
public abstract int getEndPadding();
|
||||
|
||||
/**
|
||||
* Creates an OrientationHelper for the given LayoutManager and orientation.
|
||||
*
|
||||
* @param layoutManager LayoutManager to attach to
|
||||
* @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createOrientationHelper(
|
||||
RecyclerView.LayoutManager layoutManager, int orientation) {
|
||||
switch (orientation) {
|
||||
case HORIZONTAL:
|
||||
return createHorizontalHelper(layoutManager);
|
||||
case VERTICAL:
|
||||
return createVerticalHelper(layoutManager);
|
||||
}
|
||||
throw new IllegalArgumentException("invalid orientation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a horizontal OrientationHelper for the given LayoutManager.
|
||||
*
|
||||
* @param layoutManager The LayoutManager to attach to.
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createHorizontalHelper(
|
||||
RecyclerView.LayoutManager layoutManager) {
|
||||
return new OrientationHelper(layoutManager) {
|
||||
@Override
|
||||
public int getEndAfterPadding() {
|
||||
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnd() {
|
||||
return mLayoutManager.getWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChildren(int amount) {
|
||||
mLayoutManager.offsetChildrenHorizontal(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartAfterPadding() {
|
||||
return mLayoutManager.getPaddingLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurement(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
|
||||
+ params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurementInOther(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
|
||||
+ params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalSpace() {
|
||||
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
|
||||
- mLayoutManager.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChild(View view, int offset) {
|
||||
view.offsetLeftAndRight(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndPadding() {
|
||||
return mLayoutManager.getPaddingRight();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a vertical OrientationHelper for the given LayoutManager.
|
||||
*
|
||||
* @param layoutManager The LayoutManager to attach to.
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
return new OrientationHelper(layoutManager) {
|
||||
@Override
|
||||
public int getEndAfterPadding() {
|
||||
return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnd() {
|
||||
return mLayoutManager.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChildren(int amount) {
|
||||
mLayoutManager.offsetChildrenVertical(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartAfterPadding() {
|
||||
return mLayoutManager.getPaddingTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurement(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
|
||||
+ params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurementInOther(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
|
||||
+ params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalSpace() {
|
||||
return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
|
||||
- mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChild(View view, int offset) {
|
||||
view.offsetTopAndBottom(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndPadding() {
|
||||
return mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
460
app/src/main/java/android/support/v7/widget/PositionMap.java
Normal file
460
app/src/main/java/android/support/v7/widget/PositionMap.java
Normal file
@ -0,0 +1,460 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Like a SparseArray, but with the ability to offset key ranges for bulk insertions/deletions.
|
||||
*/
|
||||
class PositionMap<E> implements Cloneable {
|
||||
private static final Object DELETED = new Object();
|
||||
private boolean mGarbage = false;
|
||||
|
||||
private int[] mKeys;
|
||||
private Object[] mValues;
|
||||
private int mSize;
|
||||
|
||||
/**
|
||||
* Creates a new SparseArray containing no mappings.
|
||||
*/
|
||||
public PositionMap() {
|
||||
this(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new PositionMap containing no mappings that will not
|
||||
* require any additional memory allocation to store the specified
|
||||
* number of mappings. If you supply an initial capacity of 0, the
|
||||
* sparse array will be initialized with a light-weight representation
|
||||
* not requiring any additional array allocations.
|
||||
*/
|
||||
public PositionMap(int initialCapacity) {
|
||||
if (initialCapacity == 0) {
|
||||
mKeys = ContainerHelpers.EMPTY_INTS;
|
||||
mValues = ContainerHelpers.EMPTY_OBJECTS;
|
||||
} else {
|
||||
initialCapacity = idealIntArraySize(initialCapacity);
|
||||
mKeys = new int[initialCapacity];
|
||||
mValues = new Object[initialCapacity];
|
||||
}
|
||||
mSize = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public PositionMap<E> clone() {
|
||||
PositionMap<E> clone = null;
|
||||
try {
|
||||
clone = (PositionMap<E>) super.clone();
|
||||
clone.mKeys = mKeys.clone();
|
||||
clone.mValues = mValues.clone();
|
||||
} catch (CloneNotSupportedException cnse) {
|
||||
/* ignore */
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Object mapped from the specified key, or <code>null</code>
|
||||
* if no such mapping has been made.
|
||||
*/
|
||||
public E get(int key) {
|
||||
return get(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Object mapped from the specified key, or the specified Object
|
||||
* if no such mapping has been made.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public E get(int key, E valueIfKeyNotFound) {
|
||||
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
|
||||
if (i < 0 || mValues[i] == DELETED) {
|
||||
return valueIfKeyNotFound;
|
||||
} else {
|
||||
return (E) mValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the mapping from the specified key, if there was any.
|
||||
*/
|
||||
public void delete(int key) {
|
||||
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
|
||||
if (i >= 0) {
|
||||
if (mValues[i] != DELETED) {
|
||||
mValues[i] = DELETED;
|
||||
mGarbage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for {@link #delete(int)}.
|
||||
*/
|
||||
public void remove(int key) {
|
||||
delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the mapping at the specified index.
|
||||
*/
|
||||
public void removeAt(int index) {
|
||||
if (mValues[index] != DELETED) {
|
||||
mValues[index] = DELETED;
|
||||
mGarbage = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a range of mappings as a batch.
|
||||
*
|
||||
* @param index Index to begin at
|
||||
* @param size Number of mappings to remove
|
||||
*/
|
||||
public void removeAtRange(int index, int size) {
|
||||
final int end = Math.min(mSize, index + size);
|
||||
for (int i = index; i < end; i++) {
|
||||
removeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void insertKeyRange(int keyStart, int count) {
|
||||
|
||||
}
|
||||
|
||||
public void removeKeyRange(ArrayList<E> removedItems, int keyStart, int count) {
|
||||
|
||||
}
|
||||
|
||||
private void gc() {
|
||||
// Log.e("SparseArray", "gc start with " + mSize);
|
||||
|
||||
int n = mSize;
|
||||
int o = 0;
|
||||
int[] keys = mKeys;
|
||||
Object[] values = mValues;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object val = values[i];
|
||||
|
||||
if (val != DELETED) {
|
||||
if (i != o) {
|
||||
keys[o] = keys[i];
|
||||
values[o] = val;
|
||||
values[i] = null;
|
||||
}
|
||||
|
||||
o++;
|
||||
}
|
||||
}
|
||||
|
||||
mGarbage = false;
|
||||
mSize = o;
|
||||
|
||||
// Log.e("SparseArray", "gc end with " + mSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping from the specified key to the specified value,
|
||||
* replacing the previous mapping from the specified key if there
|
||||
* was one.
|
||||
*/
|
||||
public void put(int key, E value) {
|
||||
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
|
||||
if (i >= 0) {
|
||||
mValues[i] = value;
|
||||
} else {
|
||||
i = ~i;
|
||||
|
||||
if (i < mSize && mValues[i] == DELETED) {
|
||||
mKeys[i] = key;
|
||||
mValues[i] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mGarbage && mSize >= mKeys.length) {
|
||||
gc();
|
||||
|
||||
// Search again because indices may have changed.
|
||||
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
}
|
||||
|
||||
if (mSize >= mKeys.length) {
|
||||
int n = idealIntArraySize(mSize + 1);
|
||||
|
||||
int[] nkeys = new int[n];
|
||||
Object[] nvalues = new Object[n];
|
||||
|
||||
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
|
||||
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
|
||||
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
|
||||
|
||||
mKeys = nkeys;
|
||||
mValues = nvalues;
|
||||
}
|
||||
|
||||
if (mSize - i != 0) {
|
||||
// Log.e("SparseArray", "move " + (mSize - i));
|
||||
System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
|
||||
System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
|
||||
}
|
||||
|
||||
mKeys[i] = key;
|
||||
mValues[i] = value;
|
||||
mSize++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of key-value mappings that this SparseArray
|
||||
* currently stores.
|
||||
*/
|
||||
public int size() {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return mSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index in the range <code>0...size()-1</code>, returns
|
||||
* the key from the <code>index</code>th key-value mapping that this
|
||||
* SparseArray stores.
|
||||
*/
|
||||
public int keyAt(int index) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return mKeys[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index in the range <code>0...size()-1</code>, returns
|
||||
* the value from the <code>index</code>th key-value mapping that this
|
||||
* SparseArray stores.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public E valueAt(int index) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return (E) mValues[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index in the range <code>0...size()-1</code>, sets a new
|
||||
* value for the <code>index</code>th key-value mapping that this
|
||||
* SparseArray stores.
|
||||
*/
|
||||
public void setValueAt(int index, E value) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
mValues[index] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index for which {@link #keyAt} would return the
|
||||
* specified key, or a negative number if the specified
|
||||
* key is not mapped.
|
||||
*/
|
||||
public int indexOfKey(int key) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an index for which {@link #valueAt} would return the
|
||||
* specified key, or a negative number if no keys map to the
|
||||
* specified value.
|
||||
* <p>Beware that this is a linear search, unlike lookups by key,
|
||||
* and that multiple keys can map to the same value and this will
|
||||
* find only one of them.
|
||||
* <p>Note also that unlike most collections' {@code indexOf} methods,
|
||||
* this method compares values using {@code ==} rather than {@code equals}.
|
||||
*/
|
||||
public int indexOfValue(E value) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
for (int i = 0; i < mSize; i++)
|
||||
if (mValues[i] == value)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all key-value mappings from this SparseArray.
|
||||
*/
|
||||
public void clear() {
|
||||
int n = mSize;
|
||||
Object[] values = mValues;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
values[i] = null;
|
||||
}
|
||||
|
||||
mSize = 0;
|
||||
mGarbage = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a key/value pair into the array, optimizing for the case where
|
||||
* the key is greater than all existing keys in the array.
|
||||
*/
|
||||
public void append(int key, E value) {
|
||||
if (mSize != 0 && key <= mKeys[mSize - 1]) {
|
||||
put(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mGarbage && mSize >= mKeys.length) {
|
||||
gc();
|
||||
}
|
||||
|
||||
int pos = mSize;
|
||||
if (pos >= mKeys.length) {
|
||||
int n = idealIntArraySize(pos + 1);
|
||||
|
||||
int[] nkeys = new int[n];
|
||||
Object[] nvalues = new Object[n];
|
||||
|
||||
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
|
||||
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
|
||||
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
|
||||
|
||||
mKeys = nkeys;
|
||||
mValues = nvalues;
|
||||
}
|
||||
|
||||
mKeys[pos] = key;
|
||||
mValues[pos] = value;
|
||||
mSize = pos + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>This implementation composes a string by iterating over its mappings. If
|
||||
* this map contains itself as a value, the string "(this Map)"
|
||||
* will appear in its place.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (size() <= 0) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
StringBuilder buffer = new StringBuilder(mSize * 28);
|
||||
buffer.append('{');
|
||||
for (int i=0; i<mSize; i++) {
|
||||
if (i > 0) {
|
||||
buffer.append(", ");
|
||||
}
|
||||
int key = keyAt(i);
|
||||
buffer.append(key);
|
||||
buffer.append('=');
|
||||
Object value = valueAt(i);
|
||||
if (value != this) {
|
||||
buffer.append(value);
|
||||
} else {
|
||||
buffer.append("(this Map)");
|
||||
}
|
||||
}
|
||||
buffer.append('}');
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
static int idealByteArraySize(int need) {
|
||||
for (int i = 4; i < 32; i++)
|
||||
if (need <= (1 << i) - 12)
|
||||
return (1 << i) - 12;
|
||||
|
||||
return need;
|
||||
}
|
||||
|
||||
static int idealBooleanArraySize(int need) {
|
||||
return idealByteArraySize(need);
|
||||
}
|
||||
|
||||
static int idealShortArraySize(int need) {
|
||||
return idealByteArraySize(need * 2) / 2;
|
||||
}
|
||||
|
||||
static int idealCharArraySize(int need) {
|
||||
return idealByteArraySize(need * 2) / 2;
|
||||
}
|
||||
|
||||
static int idealIntArraySize(int need) {
|
||||
return idealByteArraySize(need * 4) / 4;
|
||||
}
|
||||
|
||||
static int idealFloatArraySize(int need) {
|
||||
return idealByteArraySize(need * 4) / 4;
|
||||
}
|
||||
|
||||
static int idealObjectArraySize(int need) {
|
||||
return idealByteArraySize(need * 4) / 4;
|
||||
}
|
||||
|
||||
static int idealLongArraySize(int need) {
|
||||
return idealByteArraySize(need * 8) / 8;
|
||||
}
|
||||
|
||||
static class ContainerHelpers {
|
||||
static final boolean[] EMPTY_BOOLEANS = new boolean[0];
|
||||
static final int[] EMPTY_INTS = new int[0];
|
||||
static final long[] EMPTY_LONGS = new long[0];
|
||||
static final Object[] EMPTY_OBJECTS = new Object[0];
|
||||
|
||||
// This is Arrays.binarySearch(), but doesn't do any argument validation.
|
||||
static int binarySearch(int[] array, int size, int value) {
|
||||
int lo = 0;
|
||||
int hi = size - 1;
|
||||
|
||||
while (lo <= hi) {
|
||||
final int mid = (lo + hi) >>> 1;
|
||||
final int midVal = array[mid];
|
||||
|
||||
if (midVal < value) {
|
||||
lo = mid + 1;
|
||||
} else if (midVal > value) {
|
||||
hi = mid - 1;
|
||||
} else {
|
||||
return mid; // value found
|
||||
}
|
||||
}
|
||||
return ~lo; // value not present
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
8303
app/src/main/java/android/support/v7/widget/RecyclerView.java
Normal file
8303
app/src/main/java/android/support/v7/widget/RecyclerView.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.view.AccessibilityDelegateCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
/**
|
||||
* The AccessibilityDelegate used by RecyclerView.
|
||||
* <p>
|
||||
* This class handles basic accessibility actions and delegates them to LayoutManager.
|
||||
*/
|
||||
public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
final RecyclerView mRecyclerView;
|
||||
|
||||
|
||||
public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName(RecyclerView.class.getName());
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(host, event);
|
||||
event.setClassName(RecyclerView.class.getName());
|
||||
if (host instanceof RecyclerView) {
|
||||
RecyclerView rv = (RecyclerView) host;
|
||||
if (rv.getLayoutManager() != null) {
|
||||
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccessibilityDelegateCompat getItemDelegate() {
|
||||
return mItemDelegate;
|
||||
}
|
||||
|
||||
final AccessibilityDelegateCompat mItemDelegate = new AccessibilityDelegateCompat() {
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager().
|
||||
onInitializeAccessibilityNodeInfoForItem(host, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager().
|
||||
performAccessibilityActionForItem(host, action, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A helper class to do scroll offset calculations.
|
||||
*/
|
||||
class ScrollbarHelper {
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled, boolean reverseLayout) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
final int minPosition = Math.min(lm.getPosition(startChild), lm.getPosition(endChild));
|
||||
final int maxPosition = Math.max(lm.getPosition(startChild), lm.getPosition(endChild));
|
||||
final int itemsBefore = reverseLayout
|
||||
? Math.max(0, state.getItemCount() - maxPosition - 1)
|
||||
: Math.max(0, minPosition - 1);
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return itemsBefore;
|
||||
}
|
||||
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) -
|
||||
orientation.getDecoratedStart(startChild));
|
||||
final int itemRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
|
||||
final float avgSizePerRow = (float) laidOutArea / itemRange;
|
||||
|
||||
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
|
||||
- orientation.getDecoratedStart(startChild)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
|
||||
}
|
||||
final int extend = orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild);
|
||||
return Math.min(orientation.getTotalSpace(), extend);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return state.getItemCount();
|
||||
}
|
||||
// smooth scrollbar enabled. try to estimate better.
|
||||
final int laidOutArea = orientation.getDecoratedEnd(endChild) -
|
||||
orientation.getDecoratedStart(startChild);
|
||||
final int laidOutRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild))
|
||||
+ 1;
|
||||
// estimate a size for full list.
|
||||
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
58
app/src/main/java/com/android/volley/AuthFailureError.java
Normal file
58
app/src/main/java/com/android/volley/AuthFailureError.java
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Error indicating that there was an authentication failure when performing a Request.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class AuthFailureError extends VolleyError {
|
||||
/** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
|
||||
private Intent mResolutionIntent;
|
||||
|
||||
public AuthFailureError() { }
|
||||
|
||||
public AuthFailureError(Intent intent) {
|
||||
mResolutionIntent = intent;
|
||||
}
|
||||
|
||||
public AuthFailureError(NetworkResponse response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public AuthFailureError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthFailureError(String message, Exception reason) {
|
||||
super(message, reason);
|
||||
}
|
||||
|
||||
public Intent getResolutionIntent() {
|
||||
return mResolutionIntent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
if (mResolutionIntent != null) {
|
||||
return "User needs to (re)enter credentials.";
|
||||
}
|
||||
return super.getMessage();
|
||||
}
|
||||
}
|
||||
97
app/src/main/java/com/android/volley/Cache.java
Normal file
97
app/src/main/java/com/android/volley/Cache.java
Normal file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An interface for a cache keyed by a String with a byte array as data.
|
||||
*/
|
||||
public interface Cache {
|
||||
/**
|
||||
* Retrieves an entry from the cache.
|
||||
* @param key Cache key
|
||||
* @return An {@link Entry} or null in the event of a cache miss
|
||||
*/
|
||||
public Entry get(String key);
|
||||
|
||||
/**
|
||||
* Adds or replaces an entry to the cache.
|
||||
* @param key Cache key
|
||||
* @param entry Data to store and metadata for cache coherency, TTL, etc.
|
||||
*/
|
||||
public void put(String key, Entry entry);
|
||||
|
||||
/**
|
||||
* Performs any potentially long-running actions needed to initialize the cache;
|
||||
* will be called from a worker thread.
|
||||
*/
|
||||
public void initialize();
|
||||
|
||||
/**
|
||||
* Invalidates an entry in the cache.
|
||||
* @param key Cache key
|
||||
* @param fullExpire True to fully expire the entry, false to soft expire
|
||||
*/
|
||||
public void invalidate(String key, boolean fullExpire);
|
||||
|
||||
/**
|
||||
* Removes an entry from the cache.
|
||||
* @param key Cache key
|
||||
*/
|
||||
public void remove(String key);
|
||||
|
||||
/**
|
||||
* Empties the cache.
|
||||
*/
|
||||
public void clear();
|
||||
|
||||
/**
|
||||
* Data and metadata for an entry returned by the cache.
|
||||
*/
|
||||
public static class Entry {
|
||||
/** The data returned from cache. */
|
||||
public byte[] data;
|
||||
|
||||
/** ETag for cache coherency. */
|
||||
public String etag;
|
||||
|
||||
/** Date of this response as reported by the server. */
|
||||
public long serverDate;
|
||||
|
||||
/** TTL for this record. */
|
||||
public long ttl;
|
||||
|
||||
/** Soft TTL for this record. */
|
||||
public long softTtl;
|
||||
|
||||
/** Immutable response headers as received from server; must be non-null. */
|
||||
public Map<String, String> responseHeaders = Collections.emptyMap();
|
||||
|
||||
/** True if the entry is expired. */
|
||||
public boolean isExpired() {
|
||||
return this.ttl < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** True if a refresh is needed from the original data source. */
|
||||
public boolean refreshNeeded() {
|
||||
return this.softTtl < System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
159
app/src/main/java/com/android/volley/CacheDispatcher.java
Normal file
159
app/src/main/java/com/android/volley/CacheDispatcher.java
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* Provides a thread for performing cache triage on a queue of requests.
|
||||
*
|
||||
* Requests added to the specified cache queue are resolved from cache.
|
||||
* Any deliverable response is posted back to the caller via a
|
||||
* {@link ResponseDelivery}. Cache misses and responses that require
|
||||
* refresh are enqueued on the specified network queue for processing
|
||||
* by a {@link NetworkDispatcher}.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class CacheDispatcher extends Thread {
|
||||
|
||||
private static final boolean DEBUG = VolleyLog.DEBUG;
|
||||
|
||||
/** The queue of requests coming in for triage. */
|
||||
private final BlockingQueue<Request> mCacheQueue;
|
||||
|
||||
/** The queue of requests going out to the network. */
|
||||
private final BlockingQueue<Request> mNetworkQueue;
|
||||
|
||||
/** The cache to read from. */
|
||||
private final Cache mCache;
|
||||
|
||||
/** For posting responses. */
|
||||
private final ResponseDelivery mDelivery;
|
||||
|
||||
/** Used for telling us to die. */
|
||||
private volatile boolean mQuit = false;
|
||||
|
||||
/**
|
||||
* Creates a new cache triage dispatcher thread. You must call {@link #start()}
|
||||
* in order to begin processing.
|
||||
*
|
||||
* @param cacheQueue Queue of incoming requests for triage
|
||||
* @param networkQueue Queue to post requests that require network to
|
||||
* @param cache Cache interface to use for resolution
|
||||
* @param delivery Delivery interface to use for posting responses
|
||||
*/
|
||||
public CacheDispatcher(
|
||||
BlockingQueue<Request> cacheQueue, BlockingQueue<Request> networkQueue,
|
||||
Cache cache, ResponseDelivery delivery) {
|
||||
mCacheQueue = cacheQueue;
|
||||
mNetworkQueue = networkQueue;
|
||||
mCache = cache;
|
||||
mDelivery = delivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces this dispatcher to quit immediately. If any requests are still in
|
||||
* the queue, they are not guaranteed to be processed.
|
||||
*/
|
||||
public void quit() {
|
||||
mQuit = true;
|
||||
interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (DEBUG) VolleyLog.v("start new dispatcher");
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
|
||||
// Make a blocking call to initialize the cache.
|
||||
mCache.initialize();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
// Get a request from the cache triage queue, blocking until
|
||||
// at least one is available.
|
||||
final Request request = mCacheQueue.take();
|
||||
request.addMarker("cache-queue-take");
|
||||
|
||||
// If the request has been canceled, don't bother dispatching it.
|
||||
if (request.isCanceled()) {
|
||||
request.finish("cache-discard-canceled");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt to retrieve this item from cache.
|
||||
Cache.Entry entry = mCache.get(request.getCacheKey());
|
||||
if (entry == null) {
|
||||
request.addMarker("cache-miss");
|
||||
// Cache miss; send off to the network dispatcher.
|
||||
mNetworkQueue.put(request);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it is completely expired, just send it to the network.
|
||||
if (entry.isExpired()) {
|
||||
request.addMarker("cache-hit-expired");
|
||||
request.setCacheEntry(entry);
|
||||
mNetworkQueue.put(request);
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have a cache hit; parse its data for delivery back to the request.
|
||||
request.addMarker("cache-hit");
|
||||
Response<?> response = request.parseNetworkResponse(
|
||||
new NetworkResponse(entry.data, entry.responseHeaders));
|
||||
request.addMarker("cache-hit-parsed");
|
||||
|
||||
if (!entry.refreshNeeded()) {
|
||||
// Completely unexpired cache hit. Just deliver the response.
|
||||
mDelivery.postResponse(request, response);
|
||||
} else {
|
||||
// Soft-expired cache hit. We can deliver the cached response,
|
||||
// but we need to also send the request to the network for
|
||||
// refreshing.
|
||||
request.addMarker("cache-hit-refresh-needed");
|
||||
request.setCacheEntry(entry);
|
||||
|
||||
// Mark the response as intermediate.
|
||||
response.intermediate = true;
|
||||
|
||||
// Post the intermediate response back to the user and have
|
||||
// the delivery then forward the request along to the network.
|
||||
mDelivery.postResponse(request, response, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
mNetworkQueue.put(request);
|
||||
} catch (InterruptedException e) {
|
||||
// Not much we can do about this.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// We may have been interrupted because it was time to quit.
|
||||
if (mQuit) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
app/src/main/java/com/android/volley/DefaultRetryPolicy.java
Normal file
98
app/src/main/java/com/android/volley/DefaultRetryPolicy.java
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Default retry policy for requests.
|
||||
*/
|
||||
public class DefaultRetryPolicy implements RetryPolicy {
|
||||
/** The current timeout in milliseconds. */
|
||||
private int mCurrentTimeoutMs;
|
||||
|
||||
/** The current retry count. */
|
||||
private int mCurrentRetryCount;
|
||||
|
||||
/** The maximum number of attempts. */
|
||||
private final int mMaxNumRetries;
|
||||
|
||||
/** The backoff multiplier for for the policy. */
|
||||
private final float mBackoffMultiplier;
|
||||
|
||||
/** The default socket timeout in milliseconds */
|
||||
public static final int DEFAULT_TIMEOUT_MS = 2500;
|
||||
|
||||
/** The default number of retries */
|
||||
public static final int DEFAULT_MAX_RETRIES = 1;
|
||||
|
||||
/** The default backoff multiplier */
|
||||
public static final float DEFAULT_BACKOFF_MULT = 1f;
|
||||
|
||||
/**
|
||||
* Constructs a new retry policy using the default timeouts.
|
||||
*/
|
||||
public DefaultRetryPolicy() {
|
||||
this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new retry policy.
|
||||
* @param initialTimeoutMs The initial timeout for the policy.
|
||||
* @param maxNumRetries The maximum number of retries.
|
||||
* @param backoffMultiplier Backoff multiplier for the policy.
|
||||
*/
|
||||
public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
|
||||
mCurrentTimeoutMs = initialTimeoutMs;
|
||||
mMaxNumRetries = maxNumRetries;
|
||||
mBackoffMultiplier = backoffMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current timeout.
|
||||
*/
|
||||
@Override
|
||||
public int getCurrentTimeout() {
|
||||
return mCurrentTimeoutMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current retry count.
|
||||
*/
|
||||
@Override
|
||||
public int getCurrentRetryCount() {
|
||||
return mCurrentRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares for the next retry by applying a backoff to the timeout.
|
||||
* @param error The error code of the last attempt.
|
||||
*/
|
||||
@Override
|
||||
public void retry(VolleyError error) throws VolleyError {
|
||||
mCurrentRetryCount++;
|
||||
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
|
||||
if (!hasAttemptRemaining()) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this policy has attempts remaining, false otherwise.
|
||||
*/
|
||||
protected boolean hasAttemptRemaining() {
|
||||
return mCurrentRetryCount <= mMaxNumRetries;
|
||||
}
|
||||
}
|
||||
118
app/src/main/java/com/android/volley/ExecutorDelivery.java
Normal file
118
app/src/main/java/com/android/volley/ExecutorDelivery.java
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Delivers responses and errors.
|
||||
*/
|
||||
public class ExecutorDelivery implements ResponseDelivery {
|
||||
/** Used for posting responses, typically to the main thread. */
|
||||
private final Executor mResponsePoster;
|
||||
|
||||
/**
|
||||
* Creates a new response delivery interface.
|
||||
* @param handler {@link Handler} to post responses on
|
||||
*/
|
||||
public ExecutorDelivery(final Handler handler) {
|
||||
// Make an Executor that just wraps the handler.
|
||||
mResponsePoster = new Executor() {
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
handler.post(command);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new response delivery interface, mockable version
|
||||
* for testing.
|
||||
* @param executor For running delivery tasks
|
||||
*/
|
||||
public ExecutorDelivery(Executor executor) {
|
||||
mResponsePoster = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postResponse(Request<?> request, Response<?> response) {
|
||||
postResponse(request, response, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
|
||||
request.markDelivered();
|
||||
request.addMarker("post-response");
|
||||
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postError(Request<?> request, VolleyError error) {
|
||||
request.addMarker("post-error");
|
||||
Response<?> response = Response.error(error);
|
||||
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* A Runnable used for delivering network responses to a listener on the
|
||||
* main thread.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private class ResponseDeliveryRunnable implements Runnable {
|
||||
private final Request mRequest;
|
||||
private final Response mResponse;
|
||||
private final Runnable mRunnable;
|
||||
|
||||
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
|
||||
mRequest = request;
|
||||
mResponse = response;
|
||||
mRunnable = runnable;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void run() {
|
||||
// If this request has canceled, finish it and don't deliver.
|
||||
if (mRequest.isCanceled()) {
|
||||
mRequest.finish("canceled-at-delivery");
|
||||
return;
|
||||
}
|
||||
|
||||
// Deliver a normal response or error, depending.
|
||||
if (mResponse.isSuccess()) {
|
||||
mRequest.deliverResponse(mResponse.result);
|
||||
} else {
|
||||
mRequest.deliverError(mResponse.error);
|
||||
}
|
||||
|
||||
// If this is an intermediate response, add a marker, otherwise we're done
|
||||
// and the request can be finished.
|
||||
if (mResponse.intermediate) {
|
||||
mRequest.addMarker("intermediate-response");
|
||||
} else {
|
||||
mRequest.finish("done");
|
||||
}
|
||||
|
||||
// If we have been provided a post-delivery runnable, run it.
|
||||
if (mRunnable != null) {
|
||||
mRunnable.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/com/android/volley/Network.java
Normal file
30
app/src/main/java/com/android/volley/Network.java
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* An interface for performing requests.
|
||||
*/
|
||||
public interface Network {
|
||||
/**
|
||||
* Performs the specified request.
|
||||
* @param request Request to process
|
||||
* @return A {@link NetworkResponse} with data and caching metadata; will never be null
|
||||
* @throws VolleyError on errors
|
||||
*/
|
||||
public NetworkResponse performRequest(Request<?> request) throws VolleyError;
|
||||
}
|
||||
142
app/src/main/java/com/android/volley/NetworkDispatcher.java
Normal file
142
app/src/main/java/com/android/volley/NetworkDispatcher.java
Normal file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.net.TrafficStats;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* Provides a thread for performing network dispatch from a queue of requests.
|
||||
*
|
||||
* Requests added to the specified queue are processed from the network via a
|
||||
* specified {@link Network} interface. Responses are committed to cache, if
|
||||
* eligible, using a specified {@link Cache} interface. Valid responses and
|
||||
* errors are posted back to the caller via a {@link ResponseDelivery}.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class NetworkDispatcher extends Thread {
|
||||
/** The queue of requests to service. */
|
||||
private final BlockingQueue<Request> mQueue;
|
||||
/** The network interface for processing requests. */
|
||||
private final Network mNetwork;
|
||||
/** The cache to write to. */
|
||||
private final Cache mCache;
|
||||
/** For posting responses and errors. */
|
||||
private final ResponseDelivery mDelivery;
|
||||
/** Used for telling us to die. */
|
||||
private volatile boolean mQuit = false;
|
||||
|
||||
/**
|
||||
* Creates a new network dispatcher thread. You must call {@link #start()}
|
||||
* in order to begin processing.
|
||||
*
|
||||
* @param queue Queue of incoming requests for triage
|
||||
* @param network Network interface to use for performing requests
|
||||
* @param cache Cache interface to use for writing responses to cache
|
||||
* @param delivery Delivery interface to use for posting responses
|
||||
*/
|
||||
public NetworkDispatcher(BlockingQueue<Request> queue,
|
||||
Network network, Cache cache,
|
||||
ResponseDelivery delivery) {
|
||||
mQueue = queue;
|
||||
mNetwork = network;
|
||||
mCache = cache;
|
||||
mDelivery = delivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces this dispatcher to quit immediately. If any requests are still in
|
||||
* the queue, they are not guaranteed to be processed.
|
||||
*/
|
||||
public void quit() {
|
||||
mQuit = true;
|
||||
interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
Request request;
|
||||
while (true) {
|
||||
try {
|
||||
// Take a request from the queue.
|
||||
request = mQueue.take();
|
||||
} catch (InterruptedException e) {
|
||||
// We may have been interrupted because it was time to quit.
|
||||
if (mQuit) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
request.addMarker("network-queue-take");
|
||||
|
||||
// If the request was cancelled already, do not perform the
|
||||
// network request.
|
||||
if (request.isCanceled()) {
|
||||
request.finish("network-discard-cancelled");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tag the request (if API >= 14)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
|
||||
}
|
||||
|
||||
// Perform the network request.
|
||||
NetworkResponse networkResponse = mNetwork.performRequest(request);
|
||||
request.addMarker("network-http-complete");
|
||||
|
||||
// If the server returned 304 AND we delivered a response already,
|
||||
// we're done -- don't deliver a second identical response.
|
||||
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
|
||||
request.finish("not-modified");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the response here on the worker thread.
|
||||
Response<?> response = request.parseNetworkResponse(networkResponse);
|
||||
request.addMarker("network-parse-complete");
|
||||
|
||||
// Write to cache if applicable.
|
||||
// TODO: Only update cache metadata instead of entire record for 304s.
|
||||
if (request.shouldCache() && response.cacheEntry != null) {
|
||||
mCache.put(request.getCacheKey(), response.cacheEntry);
|
||||
request.addMarker("network-cache-written");
|
||||
}
|
||||
|
||||
// Post the response back.
|
||||
request.markDelivered();
|
||||
mDelivery.postResponse(request, response);
|
||||
} catch (VolleyError volleyError) {
|
||||
parseAndDeliverNetworkError(request, volleyError);
|
||||
} catch (Exception e) {
|
||||
VolleyLog.e(e, "Unhandled exception %s", e.toString());
|
||||
mDelivery.postError(request, new VolleyError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
|
||||
error = request.parseNetworkError(error);
|
||||
mDelivery.postError(request, error);
|
||||
}
|
||||
}
|
||||
36
app/src/main/java/com/android/volley/NetworkError.java
Normal file
36
app/src/main/java/com/android/volley/NetworkError.java
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that there was a network error when performing a Volley request.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class NetworkError extends VolleyError {
|
||||
public NetworkError() {
|
||||
super();
|
||||
}
|
||||
|
||||
public NetworkError(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public NetworkError(NetworkResponse networkResponse) {
|
||||
super(networkResponse);
|
||||
}
|
||||
}
|
||||
62
app/src/main/java/com/android/volley/NetworkResponse.java
Normal file
62
app/src/main/java/com/android/volley/NetworkResponse.java
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Data and headers returned from {@link Network#performRequest(Request)}.
|
||||
*/
|
||||
public class NetworkResponse {
|
||||
/**
|
||||
* Creates a new network response.
|
||||
* @param statusCode the HTTP status code
|
||||
* @param data Response body
|
||||
* @param headers Headers returned with this response, or null for none
|
||||
* @param notModified True if the server returned a 304 and the data was already in cache
|
||||
*/
|
||||
public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
|
||||
boolean notModified) {
|
||||
this.statusCode = statusCode;
|
||||
this.data = data;
|
||||
this.headers = headers;
|
||||
this.notModified = notModified;
|
||||
}
|
||||
|
||||
public NetworkResponse(byte[] data) {
|
||||
this(HttpStatus.SC_OK, data, Collections.<String, String>emptyMap(), false);
|
||||
}
|
||||
|
||||
public NetworkResponse(byte[] data, Map<String, String> headers) {
|
||||
this(HttpStatus.SC_OK, data, headers, false);
|
||||
}
|
||||
|
||||
/** The HTTP status code. */
|
||||
public final int statusCode;
|
||||
|
||||
/** Raw data from this response. */
|
||||
public final byte[] data;
|
||||
|
||||
/** Response headers. */
|
||||
public final Map<String, String> headers;
|
||||
|
||||
/** True if the server returned a 304 (Not Modified). */
|
||||
public final boolean notModified;
|
||||
}
|
||||
31
app/src/main/java/com/android/volley/NoConnectionError.java
Normal file
31
app/src/main/java/com/android/volley/NoConnectionError.java
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Error indicating that no connection could be established when performing a Volley request.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class NoConnectionError extends NetworkError {
|
||||
public NoConnectionError() {
|
||||
super();
|
||||
}
|
||||
|
||||
public NoConnectionError(Throwable reason) {
|
||||
super(reason);
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (C) 2017 Wasabeef
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* 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
|
||||
* 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,
|
||||
@ -14,30 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@charset "UTF-8";
|
||||
package com.android.volley;
|
||||
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
/**
|
||||
* Indicates that the server's response could not be parsed.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ParseError extends VolleyError {
|
||||
public ParseError() { }
|
||||
|
||||
public ParseError(NetworkResponse networkResponse) {
|
||||
super(networkResponse);
|
||||
}
|
||||
|
||||
public ParseError(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: scroll;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
min-height:100%;
|
||||
}
|
||||
|
||||
#editor {
|
||||
display: table-cell;
|
||||
outline: 0px solid transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#editor[placeholder]:empty:not(:focus):before {
|
||||
content: attr(placeholder);
|
||||
opacity: .5;
|
||||
}}
|
||||
543
app/src/main/java/com/android/volley/Request.java
Normal file
543
app/src/main/java/com/android/volley/Request.java
Normal file
@ -0,0 +1,543 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.net.TrafficStats;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.volley.VolleyLog.MarkerLog;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base class for all network requests.
|
||||
*
|
||||
* @param <T> The type of parsed response this request expects.
|
||||
*/
|
||||
public abstract class Request<T> implements Comparable<Request<T>> {
|
||||
|
||||
/**
|
||||
* Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}.
|
||||
*/
|
||||
private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
|
||||
|
||||
/**
|
||||
* Supported request methods.
|
||||
*/
|
||||
public interface Method {
|
||||
int DEPRECATED_GET_OR_POST = -1;
|
||||
int GET = 0;
|
||||
int POST = 1;
|
||||
int PUT = 2;
|
||||
int DELETE = 3;
|
||||
}
|
||||
|
||||
/** An event log tracing the lifetime of this request; for debugging. */
|
||||
private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
|
||||
|
||||
/** Request method of this request. Currently supports GET, POST, PUT, and DELETE. */
|
||||
private final int mMethod;
|
||||
|
||||
/** URL of this request. */
|
||||
private final String mUrl;
|
||||
|
||||
/** Default tag for {@link TrafficStats}. */
|
||||
private final int mDefaultTrafficStatsTag;
|
||||
|
||||
/** Listener interface for errors. */
|
||||
private final Response.ErrorListener mErrorListener;
|
||||
|
||||
/** Sequence number of this request, used to enforce FIFO ordering. */
|
||||
private Integer mSequence;
|
||||
|
||||
/** The request queue this request is associated with. */
|
||||
private RequestQueue mRequestQueue;
|
||||
|
||||
/** Whether or not responses to this request should be cached. */
|
||||
private boolean mShouldCache = true;
|
||||
|
||||
/** Whether or not this request has been canceled. */
|
||||
private boolean mCanceled = false;
|
||||
|
||||
/** Whether or not a response has been delivered for this request yet. */
|
||||
private boolean mResponseDelivered = false;
|
||||
|
||||
// A cheap variant of request tracing used to dump slow requests.
|
||||
private long mRequestBirthTime = 0;
|
||||
|
||||
/** Threshold at which we should log the request (even when debug logging is not enabled). */
|
||||
private static final long SLOW_REQUEST_THRESHOLD_MS = 3000;
|
||||
|
||||
/** The retry policy for this request. */
|
||||
private RetryPolicy mRetryPolicy;
|
||||
|
||||
/**
|
||||
* When a request can be retrieved from cache but must be refreshed from
|
||||
* the network, the cache entry will be stored here so that in the event of
|
||||
* a "Not Modified" response, we can be sure it hasn't been evicted from cache.
|
||||
*/
|
||||
private Cache.Entry mCacheEntry = null;
|
||||
|
||||
/** An opaque token tagging this request; used for bulk cancellation. */
|
||||
private Object mTag;
|
||||
|
||||
/**
|
||||
* Creates a new request with the given URL and error listener. Note that
|
||||
* the normal response listener is not provided here as delivery of responses
|
||||
* is provided by subclasses, who have a better idea of how to deliver an
|
||||
* already-parsed response.
|
||||
*
|
||||
* @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}.
|
||||
*/
|
||||
public Request(String url, Response.ErrorListener listener) {
|
||||
this(Method.DEPRECATED_GET_OR_POST, url, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new request with the given method (one of the values from {@link Method}),
|
||||
* URL, and error listener. Note that the normal response listener is not provided here as
|
||||
* delivery of responses is provided by subclasses, who have a better idea of how to deliver
|
||||
* an already-parsed response.
|
||||
*/
|
||||
public Request(int method, String url, Response.ErrorListener listener) {
|
||||
mMethod = method;
|
||||
mUrl = url;
|
||||
mErrorListener = listener;
|
||||
setRetryPolicy(new DefaultRetryPolicy());
|
||||
|
||||
mDefaultTrafficStatsTag = TextUtils.isEmpty(url) ? 0: Uri.parse(url).getHost().hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method for this request. Can be one of the values in {@link Method}.
|
||||
*/
|
||||
public int getMethod() {
|
||||
return mMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a tag on this request. Can be used to cancel all requests with this
|
||||
* tag by {@link RequestQueue#cancelAll(Object)}.
|
||||
*/
|
||||
public void setTag(Object tag) {
|
||||
mTag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this request's tag.
|
||||
* @see Request#setTag(Object)
|
||||
*/
|
||||
public Object getTag() {
|
||||
return mTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)}
|
||||
*/
|
||||
public int getTrafficStatsTag() {
|
||||
return mDefaultTrafficStatsTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the retry policy for this request.
|
||||
*/
|
||||
public void setRetryPolicy(RetryPolicy retryPolicy) {
|
||||
mRetryPolicy = retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event to this request's event log; for debugging.
|
||||
*/
|
||||
public void addMarker(String tag) {
|
||||
if (MarkerLog.ENABLED) {
|
||||
mEventLog.add(tag, Thread.currentThread().getId());
|
||||
} else if (mRequestBirthTime == 0) {
|
||||
mRequestBirthTime = SystemClock.elapsedRealtime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the request queue that this request has finished (successfully or with error).
|
||||
*
|
||||
* <p>Also dumps all events from this request's event log; for debugging.</p>
|
||||
*/
|
||||
void finish(final String tag) {
|
||||
if (mRequestQueue != null) {
|
||||
mRequestQueue.finish(this);
|
||||
}
|
||||
if (MarkerLog.ENABLED) {
|
||||
final long threadId = Thread.currentThread().getId();
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
// If we finish marking off of the main thread, we need to
|
||||
// actually do it on the main thread to ensure correct ordering.
|
||||
Handler mainThread = new Handler(Looper.getMainLooper());
|
||||
mainThread.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mEventLog.add(tag, threadId);
|
||||
mEventLog.finish(this.toString());
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
mEventLog.add(tag, threadId);
|
||||
mEventLog.finish(this.toString());
|
||||
} else {
|
||||
long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
|
||||
if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
|
||||
VolleyLog.d("%d ms: %s", requestTime, this.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates this request with the given queue. The request queue will be notified when this
|
||||
* request has finished.
|
||||
*/
|
||||
public void setRequestQueue(RequestQueue requestQueue) {
|
||||
mRequestQueue = requestQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sequence number of this request. Used by {@link RequestQueue}.
|
||||
*/
|
||||
public final void setSequence(int sequence) {
|
||||
mSequence = sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sequence number of this request.
|
||||
*/
|
||||
public final int getSequence() {
|
||||
if (mSequence == null) {
|
||||
throw new IllegalStateException("getSequence called before setSequence");
|
||||
}
|
||||
return mSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of this request.
|
||||
*/
|
||||
public String getUrl() {
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache key for this request. By default, this is the URL.
|
||||
*/
|
||||
public String getCacheKey() {
|
||||
return getUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotates this request with an entry retrieved for it from cache.
|
||||
* Used for cache coherency support.
|
||||
*/
|
||||
public void setCacheEntry(Cache.Entry entry) {
|
||||
mCacheEntry = entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the annotated cache entry, or null if there isn't one.
|
||||
*/
|
||||
public Cache.Entry getCacheEntry() {
|
||||
return mCacheEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this request as canceled. No callback will be delivered.
|
||||
*/
|
||||
public void cancel() {
|
||||
mCanceled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this request has been canceled.
|
||||
*/
|
||||
public boolean isCanceled() {
|
||||
return mCanceled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of extra HTTP headers to go along with this request. Can
|
||||
* throw {@link AuthFailureError} as authentication may be required to
|
||||
* provide these values.
|
||||
* @throws AuthFailureError In the event of auth failure
|
||||
*/
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Map of POST parameters to be used for this request, or null if
|
||||
* a simple GET should be used. Can throw {@link AuthFailureError} as
|
||||
* authentication may be required to provide these values.
|
||||
*
|
||||
* <p>Note that only one of getPostParams() and getPostBody() can return a non-null
|
||||
* value.</p>
|
||||
* @throws AuthFailureError In the event of auth failure
|
||||
*
|
||||
* @deprecated Use {@link #getParams()} instead.
|
||||
*/
|
||||
protected Map<String, String> getPostParams() throws AuthFailureError {
|
||||
return getParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which encoding should be used when converting POST parameters returned by
|
||||
* {@link #getPostParams()} into a raw POST body.
|
||||
*
|
||||
* <p>This controls both encodings:
|
||||
* <ol>
|
||||
* <li>The string encoding used when converting parameter names and values into bytes prior
|
||||
* to URL encoding them.</li>
|
||||
* <li>The string encoding used when converting the URL encoded parameters into a raw
|
||||
* byte array.</li>
|
||||
* </ol>
|
||||
*
|
||||
* @deprecated Use {@link #getParamsEncoding()} instead.
|
||||
*/
|
||||
protected String getPostParamsEncoding() {
|
||||
return getParamsEncoding();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getBodyContentType()} instead.
|
||||
*/
|
||||
public String getPostBodyContentType() {
|
||||
return getBodyContentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw POST body to be sent.
|
||||
*
|
||||
* @throws AuthFailureError In the event of auth failure
|
||||
*
|
||||
* @deprecated Use {@link #getBody()} instead.
|
||||
*/
|
||||
public byte[] getPostBody() throws AuthFailureError {
|
||||
// Note: For compatibility with legacy clients of volley, this implementation must remain
|
||||
// here instead of simply calling the getBody() function because this function must
|
||||
// call getPostParams() and getPostParamsEncoding() since legacy clients would have
|
||||
// overridden these two member functions for POST requests.
|
||||
Map<String, String> postParams = getPostParams();
|
||||
if (postParams != null && postParams.size() > 0) {
|
||||
return encodeParameters(postParams, getPostParamsEncoding());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Map of parameters to be used for a POST or PUT request. Can throw
|
||||
* {@link AuthFailureError} as authentication may be required to provide these values.
|
||||
*
|
||||
* <p>Note that you can directly override {@link #getBody()} for custom data.</p>
|
||||
*
|
||||
* @throws AuthFailureError in the event of auth failure
|
||||
*/
|
||||
protected Map<String, String> getParams() throws AuthFailureError {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which encoding should be used when converting POST or PUT parameters returned by
|
||||
* {@link #getParams()} into a raw POST or PUT body.
|
||||
*
|
||||
* <p>This controls both encodings:
|
||||
* <ol>
|
||||
* <li>The string encoding used when converting parameter names and values into bytes prior
|
||||
* to URL encoding them.</li>
|
||||
* <li>The string encoding used when converting the URL encoded parameters into a raw
|
||||
* byte array.</li>
|
||||
* </ol>
|
||||
*/
|
||||
protected String getParamsEncoding() {
|
||||
return DEFAULT_PARAMS_ENCODING;
|
||||
}
|
||||
|
||||
public String getBodyContentType() {
|
||||
return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw POST or PUT body to be sent.
|
||||
*
|
||||
* @throws AuthFailureError in the event of auth failure
|
||||
*/
|
||||
public byte[] getBody() throws AuthFailureError {
|
||||
Map<String, String> params = getParams();
|
||||
if (params != null && params.size() > 0) {
|
||||
return encodeParameters(params, getParamsEncoding());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts <code>params</code> into an application/x-www-form-urlencoded encoded string.
|
||||
*/
|
||||
private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
|
||||
StringBuilder encodedParams = new StringBuilder();
|
||||
try {
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
|
||||
encodedParams.append('=');
|
||||
encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
|
||||
encodedParams.append('&');
|
||||
}
|
||||
return encodedParams.toString().getBytes(paramsEncoding);
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not responses to this request should be cached.
|
||||
*/
|
||||
public final void setShouldCache(boolean shouldCache) {
|
||||
mShouldCache = shouldCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if responses to this request should be cached.
|
||||
*/
|
||||
public final boolean shouldCache() {
|
||||
return mShouldCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority values. Requests will be processed from higher priorities to
|
||||
* lower priorities, in FIFO order.
|
||||
*/
|
||||
public enum Priority {
|
||||
LOW,
|
||||
NORMAL,
|
||||
HIGH,
|
||||
IMMEDIATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default.
|
||||
*/
|
||||
public Priority getPriority() {
|
||||
return Priority.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the socket timeout in milliseconds per retry attempt. (This value can be changed
|
||||
* per retry attempt if a backoff is specified via backoffTimeout()). If there are no retry
|
||||
* attempts remaining, this will cause delivery of a {@link TimeoutError} error.
|
||||
*/
|
||||
public final int getTimeoutMs() {
|
||||
return mRetryPolicy.getCurrentTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the retry policy that should be used for this request.
|
||||
*/
|
||||
public RetryPolicy getRetryPolicy() {
|
||||
return mRetryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this request as having a response delivered on it. This can be used
|
||||
* later in the request's lifetime for suppressing identical responses.
|
||||
*/
|
||||
public void markDelivered() {
|
||||
mResponseDelivered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this request has had a response delivered for it.
|
||||
*/
|
||||
public boolean hasHadResponseDelivered() {
|
||||
return mResponseDelivered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses must implement this to parse the raw network response
|
||||
* and return an appropriate response type. This method will be
|
||||
* called from a worker thread. The response will not be delivered
|
||||
* if you return null.
|
||||
* @param response Response from the network
|
||||
* @return The parsed response, or null in the case of an error
|
||||
*/
|
||||
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
|
||||
|
||||
/**
|
||||
* Subclasses can override this method to parse 'networkError' and return a more specific error.
|
||||
*
|
||||
* <p>The default implementation just returns the passed 'networkError'.</p>
|
||||
*
|
||||
* @param volleyError the error retrieved from the network
|
||||
* @return an NetworkError augmented with additional information
|
||||
*/
|
||||
protected VolleyError parseNetworkError(VolleyError volleyError) {
|
||||
return volleyError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses must implement this to perform delivery of the parsed
|
||||
* response to their listeners. The given response is guaranteed to
|
||||
* be non-null; responses that fail to parse are not delivered.
|
||||
* @param response The parsed response returned by
|
||||
* {@link #parseNetworkResponse(NetworkResponse)}
|
||||
*/
|
||||
abstract protected void deliverResponse(T response);
|
||||
|
||||
/**
|
||||
* Delivers error message to the ErrorListener that the Request was
|
||||
* initialized with.
|
||||
*
|
||||
* @param error Error details
|
||||
*/
|
||||
public void deliverError(VolleyError error) {
|
||||
if (mErrorListener != null) {
|
||||
mErrorListener.onErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Our comparator sorts from high to low priority, and secondarily by
|
||||
* sequence number to provide FIFO ordering.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Request<T> other) {
|
||||
Priority left = this.getPriority();
|
||||
Priority right = other.getPriority();
|
||||
|
||||
// High-priority requests are "lesser" so they are sorted to the front.
|
||||
// Equal priorities are sorted by sequence number to provide FIFO ordering.
|
||||
return left == right ?
|
||||
this.mSequence - other.mSequence :
|
||||
right.ordinal() - left.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag());
|
||||
return (mCanceled ? "[X] " : "[ ] ") + getUrl() + " " + trafficStatsTag + " "
|
||||
+ getPriority() + " " + mSequence;
|
||||
}
|
||||
}
|
||||
287
app/src/main/java/com/android/volley/RequestQueue.java
Normal file
287
app/src/main/java/com/android/volley/RequestQueue.java
Normal file
@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* A request dispatch queue with a thread pool of dispatchers.
|
||||
*
|
||||
* Calling {@link #add(Request)} will enqueue the given Request for dispatch,
|
||||
* resolving from either cache or network on a worker thread, and then delivering
|
||||
* a parsed response on the main thread.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class RequestQueue {
|
||||
|
||||
/** Used for generating monotonically-increasing sequence numbers for requests. */
|
||||
private AtomicInteger mSequenceGenerator = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* Staging area for requests that already have a duplicate request in flight.
|
||||
*
|
||||
* <ul>
|
||||
* <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
|
||||
* key.</li>
|
||||
* <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
|
||||
* is <em>not</em> contained in that list. Is null if no requests are staged.</li>
|
||||
* </ul>
|
||||
*/
|
||||
private final Map<String, Queue<Request>> mWaitingRequests =
|
||||
new HashMap<String, Queue<Request>>();
|
||||
|
||||
/**
|
||||
* The set of all requests currently being processed by this RequestQueue. A Request
|
||||
* will be in this set if it is waiting in any queue or currently being processed by
|
||||
* any dispatcher.
|
||||
*/
|
||||
private final Set<Request> mCurrentRequests = new HashSet<Request>();
|
||||
|
||||
/** The cache triage queue. */
|
||||
private final PriorityBlockingQueue<Request> mCacheQueue =
|
||||
new PriorityBlockingQueue<Request>();
|
||||
|
||||
/** The queue of requests that are actually going out to the network. */
|
||||
private final PriorityBlockingQueue<Request> mNetworkQueue =
|
||||
new PriorityBlockingQueue<Request>();
|
||||
|
||||
/** Number of network request dispatcher threads to start. */
|
||||
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
|
||||
|
||||
/** Cache interface for retrieving and storing respones. */
|
||||
private final Cache mCache;
|
||||
|
||||
/** Network interface for performing requests. */
|
||||
private final Network mNetwork;
|
||||
|
||||
/** Response delivery mechanism. */
|
||||
private final ResponseDelivery mDelivery;
|
||||
|
||||
/** The network dispatchers. */
|
||||
private NetworkDispatcher[] mDispatchers;
|
||||
|
||||
/** The cache dispatcher. */
|
||||
private CacheDispatcher mCacheDispatcher;
|
||||
|
||||
/**
|
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
|
||||
*
|
||||
* @param cache A Cache to use for persisting responses to disk
|
||||
* @param network A Network interface for performing HTTP requests
|
||||
* @param threadPoolSize Number of network dispatcher threads to create
|
||||
* @param delivery A ResponseDelivery interface for posting responses and errors
|
||||
*/
|
||||
public RequestQueue(Cache cache, Network network, int threadPoolSize,
|
||||
ResponseDelivery delivery) {
|
||||
mCache = cache;
|
||||
mNetwork = network;
|
||||
mDispatchers = new NetworkDispatcher[threadPoolSize];
|
||||
mDelivery = delivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
|
||||
*
|
||||
* @param cache A Cache to use for persisting responses to disk
|
||||
* @param network A Network interface for performing HTTP requests
|
||||
* @param threadPoolSize Number of network dispatcher threads to create
|
||||
*/
|
||||
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
|
||||
this(cache, network, threadPoolSize,
|
||||
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
|
||||
*
|
||||
* @param cache A Cache to use for persisting responses to disk
|
||||
* @param network A Network interface for performing HTTP requests
|
||||
*/
|
||||
public RequestQueue(Cache cache, Network network) {
|
||||
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the dispatchers in this queue.
|
||||
*/
|
||||
public void start() {
|
||||
stop(); // Make sure any currently running dispatchers are stopped.
|
||||
// Create the cache dispatcher and start it.
|
||||
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
|
||||
mCacheDispatcher.start();
|
||||
|
||||
// Create network dispatchers (and corresponding threads) up to the pool size.
|
||||
for (int i = 0; i < mDispatchers.length; i++) {
|
||||
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
|
||||
mCache, mDelivery);
|
||||
mDispatchers[i] = networkDispatcher;
|
||||
networkDispatcher.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the cache and network dispatchers.
|
||||
*/
|
||||
public void stop() {
|
||||
if (mCacheDispatcher != null) {
|
||||
mCacheDispatcher.quit();
|
||||
}
|
||||
for (int i = 0; i < mDispatchers.length; i++) {
|
||||
if (mDispatchers[i] != null) {
|
||||
mDispatchers[i].quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a sequence number.
|
||||
*/
|
||||
public int getSequenceNumber() {
|
||||
return mSequenceGenerator.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Cache} instance being used.
|
||||
*/
|
||||
public Cache getCache() {
|
||||
return mCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple predicate or filter interface for Requests, for use by
|
||||
* {@link RequestQueue#cancelAll(RequestFilter)}.
|
||||
*/
|
||||
public interface RequestFilter {
|
||||
public boolean apply(Request<?> request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all requests in this queue for which the given filter applies.
|
||||
* @param filter The filtering function to use
|
||||
*/
|
||||
public void cancelAll(RequestFilter filter) {
|
||||
synchronized (mCurrentRequests) {
|
||||
for (Request<?> request : mCurrentRequests) {
|
||||
if (filter.apply(request)) {
|
||||
request.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all requests in this queue with the given tag. Tag must be non-null
|
||||
* and equality is by identity.
|
||||
*/
|
||||
public void cancelAll(final Object tag) {
|
||||
if (tag == null) {
|
||||
throw new IllegalArgumentException("Cannot cancelAll with a null tag");
|
||||
}
|
||||
cancelAll(new RequestFilter() {
|
||||
@Override
|
||||
public boolean apply(Request<?> request) {
|
||||
return request.getTag() == tag;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Request to the dispatch queue.
|
||||
* @param request The request to service
|
||||
* @return The passed-in request
|
||||
*/
|
||||
public Request add(Request request) {
|
||||
// Tag the request as belonging to this queue and add it to the set of current requests.
|
||||
request.setRequestQueue(this);
|
||||
synchronized (mCurrentRequests) {
|
||||
mCurrentRequests.add(request);
|
||||
}
|
||||
|
||||
// Process requests in the order they are added.
|
||||
request.setSequence(getSequenceNumber());
|
||||
request.addMarker("add-to-queue");
|
||||
|
||||
// If the request is uncacheable, skip the cache queue and go straight to the network.
|
||||
if (!request.shouldCache()) {
|
||||
mNetworkQueue.add(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
// Insert request into stage if there's already a request with the same cache key in flight.
|
||||
synchronized (mWaitingRequests) {
|
||||
String cacheKey = request.getCacheKey();
|
||||
if (mWaitingRequests.containsKey(cacheKey)) {
|
||||
// There is already a request in flight. Queue up.
|
||||
Queue<Request> stagedRequests = mWaitingRequests.get(cacheKey);
|
||||
if (stagedRequests == null) {
|
||||
stagedRequests = new LinkedList<Request>();
|
||||
}
|
||||
stagedRequests.add(request);
|
||||
mWaitingRequests.put(cacheKey, stagedRequests);
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
|
||||
}
|
||||
} else {
|
||||
// Insert 'null' queue for this cacheKey, indicating there is now a request in
|
||||
// flight.
|
||||
mWaitingRequests.put(cacheKey, null);
|
||||
mCacheQueue.add(request);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link Request#finish(String)}, indicating that processing of the given request
|
||||
* has finished.
|
||||
*
|
||||
* <p>Releases waiting requests for <code>request.getCacheKey()</code> if
|
||||
* <code>request.shouldCache()</code>.</p>
|
||||
*/
|
||||
void finish(Request request) {
|
||||
// Remove from the set of requests currently being processed.
|
||||
synchronized (mCurrentRequests) {
|
||||
mCurrentRequests.remove(request);
|
||||
}
|
||||
|
||||
if (request.shouldCache()) {
|
||||
synchronized (mWaitingRequests) {
|
||||
String cacheKey = request.getCacheKey();
|
||||
Queue<Request> waitingRequests = mWaitingRequests.remove(cacheKey);
|
||||
if (waitingRequests != null) {
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
|
||||
waitingRequests.size(), cacheKey);
|
||||
}
|
||||
// Process all queued up requests. They won't be considered as in flight, but
|
||||
// that's not a problem as the cache has been primed by 'request'.
|
||||
mCacheQueue.addAll(waitingRequests);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
app/src/main/java/com/android/volley/Response.java
Normal file
85
app/src/main/java/com/android/volley/Response.java
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Encapsulates a parsed response for delivery.
|
||||
*
|
||||
* @param <T> Parsed type of this response
|
||||
*/
|
||||
public class Response<T> {
|
||||
|
||||
/** Callback interface for delivering parsed responses. */
|
||||
public interface Listener<T> {
|
||||
/** Called when a response is received. */
|
||||
public void onResponse(T response);
|
||||
}
|
||||
|
||||
/** Callback interface for delivering error responses. */
|
||||
public interface ErrorListener {
|
||||
/**
|
||||
* Callback method that an error has been occurred with the
|
||||
* provided error code and optional user-readable message.
|
||||
*/
|
||||
public void onErrorResponse(VolleyError error);
|
||||
}
|
||||
|
||||
/** Returns a successful response containing the parsed result. */
|
||||
public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
|
||||
return new Response<T>(result, cacheEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a failed response containing the given error code and an optional
|
||||
* localized message displayed to the user.
|
||||
*/
|
||||
public static <T> Response<T> error(VolleyError error) {
|
||||
return new Response<T>(error);
|
||||
}
|
||||
|
||||
/** Parsed response, or null in the case of error. */
|
||||
public final T result;
|
||||
|
||||
/** Cache metadata for this response, or null in the case of error. */
|
||||
public final Cache.Entry cacheEntry;
|
||||
|
||||
/** Detailed error information if <code>errorCode != OK</code>. */
|
||||
public final VolleyError error;
|
||||
|
||||
/** True if this response was a soft-expired one and a second one MAY be coming. */
|
||||
public boolean intermediate = false;
|
||||
|
||||
/**
|
||||
* Returns whether this response is considered successful.
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return error == null;
|
||||
}
|
||||
|
||||
|
||||
private Response(T result, Cache.Entry cacheEntry) {
|
||||
this.result = result;
|
||||
this.cacheEntry = cacheEntry;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
private Response(VolleyError error) {
|
||||
this.result = null;
|
||||
this.cacheEntry = null;
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/com/android/volley/ResponseDelivery.java
Normal file
35
app/src/main/java/com/android/volley/ResponseDelivery.java
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
public interface ResponseDelivery {
|
||||
/**
|
||||
* Parses a response from the network or cache and delivers it.
|
||||
*/
|
||||
public void postResponse(Request<?> request, Response<?> response);
|
||||
|
||||
/**
|
||||
* Parses a response from the network or cache and delivers it. The provided
|
||||
* Runnable will be executed after delivery.
|
||||
*/
|
||||
public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
|
||||
|
||||
/**
|
||||
* Posts an error for the given request.
|
||||
*/
|
||||
public void postError(Request<?> request, VolleyError error);
|
||||
}
|
||||
41
app/src/main/java/com/android/volley/RetryPolicy.java
Normal file
41
app/src/main/java/com/android/volley/RetryPolicy.java
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Retry policy for a request.
|
||||
*/
|
||||
public interface RetryPolicy {
|
||||
|
||||
/**
|
||||
* Returns the current timeout (used for logging).
|
||||
*/
|
||||
public int getCurrentTimeout();
|
||||
|
||||
/**
|
||||
* Returns the current retry count (used for logging).
|
||||
*/
|
||||
public int getCurrentRetryCount();
|
||||
|
||||
/**
|
||||
* Prepares for the next retry by applying a backoff to the timeout.
|
||||
* @param error The error code of the last attempt.
|
||||
* @throws VolleyError In the event that the retry could not be performed (for example if we
|
||||
* ran out of attempts), the passed in error is thrown.
|
||||
*/
|
||||
public void retry(VolleyError error) throws VolleyError;
|
||||
}
|
||||
32
app/src/main/java/com/android/volley/ServerError.java
Normal file
32
app/src/main/java/com/android/volley/ServerError.java
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that the error responded with an error response.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ServerError extends VolleyError {
|
||||
public ServerError(NetworkResponse networkResponse) {
|
||||
super(networkResponse);
|
||||
}
|
||||
|
||||
public ServerError() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
23
app/src/main/java/com/android/volley/TimeoutError.java
Normal file
23
app/src/main/java/com/android/volley/TimeoutError.java
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Indicates that the connection or the socket timed out.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class TimeoutError extends VolleyError { }
|
||||
48
app/src/main/java/com/android/volley/VolleyError.java
Normal file
48
app/src/main/java/com/android/volley/VolleyError.java
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Exception style class encapsulating Volley errors
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class VolleyError extends Exception {
|
||||
public final NetworkResponse networkResponse;
|
||||
|
||||
public VolleyError() {
|
||||
networkResponse = null;
|
||||
}
|
||||
|
||||
public VolleyError(NetworkResponse response) {
|
||||
networkResponse = response;
|
||||
}
|
||||
|
||||
public VolleyError(String exceptionMessage) {
|
||||
super(exceptionMessage);
|
||||
networkResponse = null;
|
||||
}
|
||||
|
||||
public VolleyError(String exceptionMessage, Throwable reason) {
|
||||
super(exceptionMessage, reason);
|
||||
networkResponse = null;
|
||||
}
|
||||
|
||||
public VolleyError(Throwable cause) {
|
||||
super(cause);
|
||||
networkResponse = null;
|
||||
}
|
||||
}
|
||||
176
app/src/main/java/com/android/volley/VolleyLog.java
Normal file
176
app/src/main/java/com/android/volley/VolleyLog.java
Normal file
@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Logging helper class. */
|
||||
public class VolleyLog {
|
||||
public static String TAG = "Volley";
|
||||
|
||||
public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
|
||||
|
||||
/**
|
||||
* Customize the log tag for your application, so that other apps
|
||||
* using Volley don't mix their logs with yours.
|
||||
* <br />
|
||||
* Enable the log property for your tag before starting your app:
|
||||
* <br />
|
||||
* {@code adb shell setprop log.tag.<tag>}
|
||||
*/
|
||||
public static void setTag(String tag) {
|
||||
d("Changing log tag to %s", tag);
|
||||
TAG = tag;
|
||||
|
||||
// Reinitialize the DEBUG "constant"
|
||||
DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
|
||||
}
|
||||
|
||||
public static void v(String format, Object... args) {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, buildMessage(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void d(String format, Object... args) {
|
||||
Log.d(TAG, buildMessage(format, args));
|
||||
}
|
||||
|
||||
public static void e(String format, Object... args) {
|
||||
Log.e(TAG, buildMessage(format, args));
|
||||
}
|
||||
|
||||
public static void e(Throwable tr, String format, Object... args) {
|
||||
Log.e(TAG, buildMessage(format, args), tr);
|
||||
}
|
||||
|
||||
public static void wtf(String format, Object... args) {
|
||||
Log.wtf(TAG, buildMessage(format, args));
|
||||
}
|
||||
|
||||
public static void wtf(Throwable tr, String format, Object... args) {
|
||||
Log.wtf(TAG, buildMessage(format, args), tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the caller's provided message and prepends useful info like
|
||||
* calling thread ID and method name.
|
||||
*/
|
||||
private static String buildMessage(String format, Object... args) {
|
||||
String msg = (args == null) ? format : String.format(Locale.US, format, args);
|
||||
StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();
|
||||
|
||||
String caller = "<unknown>";
|
||||
// Walk up the stack looking for the first caller outside of VolleyLog.
|
||||
// It will be at least two frames up, so start there.
|
||||
for (int i = 2; i < trace.length; i++) {
|
||||
Class<?> clazz = trace[i].getClass();
|
||||
if (!clazz.equals(VolleyLog.class)) {
|
||||
String callingClass = trace[i].getClassName();
|
||||
callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1);
|
||||
callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1);
|
||||
|
||||
caller = callingClass + "." + trace[i].getMethodName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return String.format(Locale.US, "[%d] %s: %s",
|
||||
Thread.currentThread().getId(), caller, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple event log with records containing a name, thread ID, and timestamp.
|
||||
*/
|
||||
static class MarkerLog {
|
||||
public static final boolean ENABLED = VolleyLog.DEBUG;
|
||||
|
||||
/** Minimum duration from first marker to last in an marker log to warrant logging. */
|
||||
private static final long MIN_DURATION_FOR_LOGGING_MS = 0;
|
||||
|
||||
private static class Marker {
|
||||
public final String name;
|
||||
public final long thread;
|
||||
public final long time;
|
||||
|
||||
public Marker(String name, long thread, long time) {
|
||||
this.name = name;
|
||||
this.thread = thread;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Marker> mMarkers = new ArrayList<Marker>();
|
||||
private boolean mFinished = false;
|
||||
|
||||
/** Adds a marker to this log with the specified name. */
|
||||
public synchronized void add(String name, long threadId) {
|
||||
if (mFinished) {
|
||||
throw new IllegalStateException("Marker added to finished log");
|
||||
}
|
||||
|
||||
mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the log, dumping it to logcat if the time difference between
|
||||
* the first and last markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}.
|
||||
* @param header Header string to print above the marker log.
|
||||
*/
|
||||
public synchronized void finish(String header) {
|
||||
mFinished = true;
|
||||
|
||||
long duration = getTotalDuration();
|
||||
if (duration <= MIN_DURATION_FOR_LOGGING_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
long prevTime = mMarkers.get(0).time;
|
||||
d("(%-4d ms) %s", duration, header);
|
||||
for (Marker marker : mMarkers) {
|
||||
long thisTime = marker.time;
|
||||
d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name);
|
||||
prevTime = thisTime;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
// Catch requests that have been collected (and hence end-of-lifed)
|
||||
// but had no debugging output printed for them.
|
||||
if (!mFinished) {
|
||||
finish("Request on the loose");
|
||||
e("Marker log finalized without finish() - uncaught exit point for request");
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the time difference between the first and last events in this log. */
|
||||
private long getTotalDuration() {
|
||||
if (mMarkers.size() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long first = mMarkers.get(0).time;
|
||||
long last = mMarkers.get(mMarkers.size() - 1).time;
|
||||
return last - first;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AccountManagerFuture;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
|
||||
/**
|
||||
* An Authenticator that uses {@link AccountManager} to get auth
|
||||
* tokens of a specified type for a specified account.
|
||||
*/
|
||||
public class AndroidAuthenticator implements Authenticator {
|
||||
private final Context mContext;
|
||||
private final Account mAccount;
|
||||
private final String mAuthTokenType;
|
||||
private final boolean mNotifyAuthFailure;
|
||||
|
||||
/**
|
||||
* Creates a new authenticator.
|
||||
* @param context Context for accessing AccountManager
|
||||
* @param account Account to authenticate as
|
||||
* @param authTokenType Auth token type passed to AccountManager
|
||||
*/
|
||||
public AndroidAuthenticator(Context context, Account account, String authTokenType) {
|
||||
this(context, account, authTokenType, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new authenticator.
|
||||
* @param context Context for accessing AccountManager
|
||||
* @param account Account to authenticate as
|
||||
* @param authTokenType Auth token type passed to AccountManager
|
||||
* @param notifyAuthFailure Whether to raise a notification upon auth failure
|
||||
*/
|
||||
public AndroidAuthenticator(Context context, Account account, String authTokenType,
|
||||
boolean notifyAuthFailure) {
|
||||
mContext = context;
|
||||
mAccount = account;
|
||||
mAuthTokenType = authTokenType;
|
||||
mNotifyAuthFailure = notifyAuthFailure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Account being used by this authenticator.
|
||||
*/
|
||||
public Account getAccount() {
|
||||
return mAccount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthToken() throws AuthFailureError {
|
||||
final AccountManager accountManager = AccountManager.get(mContext);
|
||||
AccountManagerFuture<Bundle> future = accountManager.getAuthToken(mAccount,
|
||||
mAuthTokenType, mNotifyAuthFailure, null, null);
|
||||
Bundle result;
|
||||
try {
|
||||
result = future.getResult();
|
||||
} catch (Exception e) {
|
||||
throw new AuthFailureError("Error while retrieving auth token", e);
|
||||
}
|
||||
String authToken = null;
|
||||
if (future.isDone() && !future.isCancelled()) {
|
||||
if (result.containsKey(AccountManager.KEY_INTENT)) {
|
||||
Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
|
||||
throw new AuthFailureError(intent);
|
||||
}
|
||||
authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
|
||||
}
|
||||
if (authToken == null) {
|
||||
throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType);
|
||||
}
|
||||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateAuthToken(String authToken) {
|
||||
AccountManager.get(mContext).invalidateAuthToken(mAccount.type, authToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
|
||||
/**
|
||||
* An interface for interacting with auth tokens.
|
||||
*/
|
||||
public interface Authenticator {
|
||||
/**
|
||||
* Synchronously retrieves an auth token.
|
||||
*
|
||||
* @throws AuthFailureError If authentication did not succeed
|
||||
*/
|
||||
public String getAuthToken() throws AuthFailureError;
|
||||
|
||||
/**
|
||||
* Invalidates the provided auth token.
|
||||
*/
|
||||
public void invalidateAuthToken(String authToken);
|
||||
}
|
||||
303
app/src/main/java/com/android/volley/toolbox/BasicNetwork.java
Normal file
303
app/src/main/java/com/android/volley/toolbox/BasicNetwork.java
Normal file
@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.Network;
|
||||
import com.android.volley.NetworkError;
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.NoConnectionError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.RetryPolicy;
|
||||
import com.android.volley.ServerError;
|
||||
import com.android.volley.TimeoutError;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.VolleyLog;
|
||||
import com.gh.common.constant.Config;
|
||||
import com.gh.common.util.GzipUtils;
|
||||
import com.gh.gamecenter.volley.extended.JsonArrayExtendedRequest;
|
||||
import com.gh.gamecenter.volley.extended.JsonObjectExtendedRequest;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.conn.ConnectTimeoutException;
|
||||
import org.apache.http.impl.cookie.DateUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A network performing Volley requests over an {@link HttpStack}.
|
||||
*/
|
||||
public class BasicNetwork implements Network {
|
||||
protected static final boolean DEBUG = VolleyLog.DEBUG;
|
||||
|
||||
private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
|
||||
|
||||
private static int DEFAULT_POOL_SIZE = 4096;
|
||||
|
||||
protected final HttpStack mHttpStack;
|
||||
|
||||
protected final ByteArrayPool mPool;
|
||||
|
||||
/**
|
||||
* @param httpStack
|
||||
* HTTP stack to be used
|
||||
*/
|
||||
public BasicNetwork(HttpStack httpStack) {
|
||||
// If a pool isn't passed in, then build a small default pool that will
|
||||
// give us a lot of
|
||||
// benefit and not use too much memory.
|
||||
this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param httpStack
|
||||
* HTTP stack to be used
|
||||
* @param pool
|
||||
* a buffer pool that improves GC performance in copy operations
|
||||
*/
|
||||
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
|
||||
mHttpStack = httpStack;
|
||||
mPool = pool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkResponse performRequest(Request<?> request)
|
||||
throws VolleyError {
|
||||
long requestStart = SystemClock.elapsedRealtime();
|
||||
while (true) {
|
||||
HttpResponse httpResponse = null;
|
||||
byte[] responseContents = null;
|
||||
Map<String, String> responseHeaders = new HashMap<String, String>();
|
||||
try {
|
||||
// Gather headers.
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
addCacheHeaders(headers, request.getCacheEntry());
|
||||
httpResponse = mHttpStack.performRequest(request, headers);
|
||||
StatusLine statusLine = httpResponse.getStatusLine();
|
||||
int statusCode = statusLine.getStatusCode();
|
||||
|
||||
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
|
||||
// Handle cache validation.
|
||||
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
|
||||
if (request.getUrl().startsWith(Config.HOST + "support/upgrade")) {
|
||||
if (request.getCacheEntry() != null) {
|
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
|
||||
request.getCacheEntry().data, responseHeaders, true);
|
||||
} else {
|
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
|
||||
GzipUtils.compressBytes("{}".getBytes()), responseHeaders, true);
|
||||
}
|
||||
} else {
|
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
|
||||
request.getCacheEntry().data, responseHeaders, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Some responses such as 204s do not have content. We must
|
||||
// check.
|
||||
if (httpResponse.getEntity() != null) {
|
||||
responseContents = entityToBytes(httpResponse.getEntity());
|
||||
} else {
|
||||
// Add 0 byte response as a way of honestly representing a
|
||||
// no-content request.
|
||||
responseContents = new byte[0];
|
||||
}
|
||||
|
||||
// if the request is slow, log it.
|
||||
long requestLifetime = SystemClock.elapsedRealtime()
|
||||
- requestStart;
|
||||
logSlowRequests(requestLifetime, request, responseContents,
|
||||
statusLine);
|
||||
|
||||
if (statusCode != HttpStatus.SC_OK
|
||||
&& statusCode != HttpStatus.SC_NO_CONTENT) {
|
||||
throw new IOException();
|
||||
}
|
||||
return new NetworkResponse(statusCode, responseContents,
|
||||
responseHeaders, false);
|
||||
} catch (SocketTimeoutException e) {
|
||||
attemptRetryOnException("socket", request, new TimeoutError());
|
||||
} catch (ConnectTimeoutException e) {
|
||||
attemptRetryOnException("connection", request,
|
||||
new TimeoutError());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException("Bad URL " + request.getUrl(), e);
|
||||
} catch (IOException e) {
|
||||
int statusCode = 0;
|
||||
NetworkResponse networkResponse = null;
|
||||
if (httpResponse != null) {
|
||||
statusCode = httpResponse.getStatusLine().getStatusCode();
|
||||
} else {
|
||||
// If there is no network connection, judge whether the url is cached
|
||||
// If have cached, return cached
|
||||
Cache.Entry entry = request.getCacheEntry();
|
||||
if (entry != null) {
|
||||
return new NetworkResponse(HttpStatus.SC_OK,
|
||||
entry.data, entry.responseHeaders, false);
|
||||
}
|
||||
// else throw NoConnectionError
|
||||
throw new NoConnectionError(e);
|
||||
}
|
||||
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
|
||||
if (responseContents != null) {
|
||||
networkResponse = new NetworkResponse(statusCode,
|
||||
responseContents, responseHeaders, false);
|
||||
if (statusCode == HttpStatus.SC_UNAUTHORIZED
|
||||
|| statusCode == HttpStatus.SC_FORBIDDEN) {
|
||||
attemptRetryOnException("auth", request,
|
||||
new AuthFailureError(networkResponse));
|
||||
} else if (statusCode == HttpStatus.SC_NOT_FOUND) {
|
||||
if (request.getClass().equals(JsonObjectExtendedRequest.class)) {
|
||||
return new NetworkResponse(HttpStatus.SC_OK,
|
||||
GzipUtils.compressBytes("{}".getBytes()), responseHeaders, true);
|
||||
} else if (request.getClass().equals(JsonArrayExtendedRequest.class)) {
|
||||
return new NetworkResponse(HttpStatus.SC_OK,
|
||||
GzipUtils.compressBytes("[]".getBytes()), responseHeaders, true);
|
||||
} else {
|
||||
// TODO: Only throw ServerError for 5xx status codes.
|
||||
throw new ServerError(networkResponse);
|
||||
}
|
||||
} else {
|
||||
// TODO: Only throw ServerError for 5xx status codes.
|
||||
throw new ServerError(networkResponse);
|
||||
}
|
||||
} else {
|
||||
throw new NetworkError(networkResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
|
||||
*/
|
||||
private void logSlowRequests(long requestLifetime, Request<?> request,
|
||||
byte[] responseContents, StatusLine statusLine) {
|
||||
if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
|
||||
VolleyLog
|
||||
.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], "
|
||||
+ "[rc=%d], [retryCount=%s]", request,
|
||||
requestLifetime,
|
||||
responseContents != null ? responseContents.length
|
||||
: "null", statusLine.getStatusCode(),
|
||||
request.getRetryPolicy().getCurrentRetryCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to prepare the request for a retry. If there are no more
|
||||
* attempts remaining in the request's retry policy, a timeout exception is
|
||||
* thrown.
|
||||
*
|
||||
* @param request
|
||||
* The request to use.
|
||||
*/
|
||||
private static void attemptRetryOnException(String logPrefix,
|
||||
Request<?> request, VolleyError exception) throws VolleyError {
|
||||
RetryPolicy retryPolicy = request.getRetryPolicy();
|
||||
int oldTimeout = request.getTimeoutMs();
|
||||
|
||||
try {
|
||||
retryPolicy.retry(exception);
|
||||
} catch (VolleyError e) {
|
||||
request.addMarker(String.format("%s-timeout-giveup [timeout=%s]",
|
||||
logPrefix, oldTimeout));
|
||||
throw e;
|
||||
}
|
||||
request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix,
|
||||
oldTimeout));
|
||||
}
|
||||
|
||||
private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
|
||||
// If there's no cache entry, we're done.
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.etag != null) {
|
||||
headers.put("If-None-Match", entry.etag);
|
||||
}
|
||||
|
||||
if (entry.serverDate > 0) {
|
||||
Date refTime = new Date(entry.serverDate);
|
||||
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
|
||||
}
|
||||
}
|
||||
|
||||
protected void logError(String what, String url, long start) {
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start),
|
||||
url);
|
||||
}
|
||||
|
||||
/** Reads the contents of HttpEntity into a byte[]. */
|
||||
private byte[] entityToBytes(HttpEntity entity) throws IOException,
|
||||
ServerError {
|
||||
PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(
|
||||
mPool, (int) entity.getContentLength());
|
||||
byte[] buffer = null;
|
||||
try {
|
||||
InputStream in = entity.getContent();
|
||||
if (in == null) {
|
||||
throw new ServerError();
|
||||
}
|
||||
buffer = mPool.getBuf(1024);
|
||||
int count;
|
||||
while ((count = in.read(buffer)) != -1) {
|
||||
bytes.write(buffer, 0, count);
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
} finally {
|
||||
try {
|
||||
// Close the InputStream and release the resources by
|
||||
// "consuming the content".
|
||||
entity.consumeContent();
|
||||
} catch (IOException e) {
|
||||
// This can happen if there was an exception above that left the
|
||||
// entity in
|
||||
// an invalid state.
|
||||
VolleyLog.v("Error occured when calling consumingContent");
|
||||
}
|
||||
mPool.returnBuf(buffer);
|
||||
bytes.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Headers[] to Map<String, String>.
|
||||
*/
|
||||
private static Map<String, String> convertHeaders(Header[] headers) {
|
||||
Map<String, String> result = new HashMap<String, String>();
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
result.put(headers[i].getName(), headers[i].getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
135
app/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
Normal file
135
app/src/main/java/com/android/volley/toolbox/ByteArrayPool.java
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to
|
||||
* supply those buffers to consumers who need to use them for a short period of time and then
|
||||
* dispose of them. Simply creating and disposing such buffers in the conventional manner can
|
||||
* considerable heap churn and garbage collection delays on Android, which lacks good management of
|
||||
* short-lived heap objects. It may be advantageous to trade off some memory in the form of a
|
||||
* permanently allocated pool of buffers in order to gain heap performance improvements; that is
|
||||
* what this class does.
|
||||
* <p>
|
||||
* A good candidate user for this class is something like an I/O system that uses large temporary
|
||||
* <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants
|
||||
* the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks
|
||||
* off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into
|
||||
* account and also to maximize the odds of being able to reuse a recycled buffer, this class is
|
||||
* free to return buffers larger than the requested size. The caller needs to be able to gracefully
|
||||
* deal with getting buffers any size over the minimum.
|
||||
* <p>
|
||||
* If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this
|
||||
* class will allocate a new buffer and return it.
|
||||
* <p>
|
||||
* This class has no special ownership of buffers it creates; the caller is free to take a buffer
|
||||
* it receives from this pool, use it permanently, and never return it to the pool; additionally,
|
||||
* it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there
|
||||
* are no other lingering references to it.
|
||||
* <p>
|
||||
* This class ensures that the total size of the buffers in its recycling pool never exceeds a
|
||||
* certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,
|
||||
* least-recently-used buffers are disposed.
|
||||
*/
|
||||
public class ByteArrayPool {
|
||||
/** The buffer pool, arranged both by last use and by buffer size */
|
||||
private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
|
||||
private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);
|
||||
|
||||
/** The total size of the buffers in the pool */
|
||||
private int mCurrentSize = 0;
|
||||
|
||||
/**
|
||||
* The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
|
||||
* under this limit.
|
||||
*/
|
||||
private final int mSizeLimit;
|
||||
|
||||
/** Compares buffers by size */
|
||||
protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() {
|
||||
@Override
|
||||
public int compare(byte[] lhs, byte[] rhs) {
|
||||
return lhs.length - rhs.length;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param sizeLimit the maximum size of the pool, in bytes
|
||||
*/
|
||||
public ByteArrayPool(int sizeLimit) {
|
||||
mSizeLimit = sizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer from the pool if one is available in the requested size, or allocates a new
|
||||
* one if a pooled one is not available.
|
||||
*
|
||||
* @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
|
||||
* larger.
|
||||
* @return a byte[] buffer is always returned.
|
||||
*/
|
||||
public synchronized byte[] getBuf(int len) {
|
||||
for (int i = 0; i < mBuffersBySize.size(); i++) {
|
||||
byte[] buf = mBuffersBySize.get(i);
|
||||
if (buf.length >= len) {
|
||||
mCurrentSize -= buf.length;
|
||||
mBuffersBySize.remove(i);
|
||||
mBuffersByLastUse.remove(buf);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
return new byte[len];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
|
||||
* size.
|
||||
*
|
||||
* @param buf the buffer to return to the pool.
|
||||
*/
|
||||
public synchronized void returnBuf(byte[] buf) {
|
||||
if (buf == null || buf.length > mSizeLimit) {
|
||||
return;
|
||||
}
|
||||
mBuffersByLastUse.add(buf);
|
||||
int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
|
||||
if (pos < 0) {
|
||||
pos = -pos - 1;
|
||||
}
|
||||
mBuffersBySize.add(pos, buf);
|
||||
mCurrentSize += buf.length;
|
||||
trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes buffers from the pool until it is under its size limit.
|
||||
*/
|
||||
private synchronized void trim() {
|
||||
while (mCurrentSize > mSizeLimit) {
|
||||
byte[] buf = mBuffersByLastUse.remove(0);
|
||||
mBuffersBySize.remove(buf);
|
||||
mCurrentSize -= buf.length;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
|
||||
/**
|
||||
* A synthetic request used for clearing the cache.
|
||||
*/
|
||||
public class ClearCacheRequest extends Request<Object> {
|
||||
private final Cache mCache;
|
||||
private final Runnable mCallback;
|
||||
|
||||
/**
|
||||
* Creates a synthetic request for clearing the cache.
|
||||
* @param cache Cache to clear
|
||||
* @param callback Callback to make on the main thread once the cache is clear,
|
||||
* or null for none
|
||||
*/
|
||||
public ClearCacheRequest(Cache cache, Runnable callback) {
|
||||
super(Method.GET, null, null);
|
||||
mCache = cache;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCanceled() {
|
||||
// This is a little bit of a hack, but hey, why not.
|
||||
mCache.clear();
|
||||
if (mCallback != null) {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.postAtFrontOfQueue(mCallback);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Priority getPriority() {
|
||||
return Priority.IMMEDIATE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<Object> parseNetworkResponse(NetworkResponse response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deliverResponse(Object response) {
|
||||
}
|
||||
}
|
||||
648
app/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
Normal file
648
app/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
Normal file
@ -0,0 +1,648 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.VolleyLog;
|
||||
import com.gh.common.util.TimestampUtils;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Cache implementation that caches files directly onto the hard disk in the
|
||||
* specified directory. The default disk usage size is 5MB, but is configurable.
|
||||
*/
|
||||
public class DiskBasedCache implements Cache {
|
||||
|
||||
/** Map of the Key, CacheHeader pairs */
|
||||
private final Map<String, CacheHeader> mEntries = new LinkedHashMap<String, CacheHeader>(
|
||||
16, .75f, true);
|
||||
|
||||
/** Total amount of space currently used by the cache in bytes. */
|
||||
private long mTotalSize = 0;
|
||||
|
||||
/** The root directory to use for the cache. */
|
||||
private final File mRootDirectory;
|
||||
|
||||
/** The maximum size of the cache in bytes. */
|
||||
private final int mMaxCacheSizeInBytes;
|
||||
|
||||
/** Default maximum disk usage in bytes. */
|
||||
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
/** High water mark percentage for the cache */
|
||||
private static final float HYSTERESIS_FACTOR = 0.9f;
|
||||
|
||||
/** Magic number for current version of cache file format. */
|
||||
private static final int CACHE_MAGIC = 0x20120504;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the DiskBasedCache at the specified directory.
|
||||
*
|
||||
* @param rootDirectory
|
||||
* The root directory of the cache.
|
||||
* @param maxCacheSizeInBytes
|
||||
* The maximum size of the cache in bytes.
|
||||
*/
|
||||
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
|
||||
mRootDirectory = rootDirectory;
|
||||
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance of the DiskBasedCache at the specified directory
|
||||
* using the default maximum cache size of 5MB.
|
||||
*
|
||||
* @param rootDirectory
|
||||
* The root directory of the cache.
|
||||
*/
|
||||
public DiskBasedCache(File rootDirectory) {
|
||||
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache. Deletes all cached files from disk.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
File[] files = mRootDirectory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
mEntries.clear();
|
||||
mTotalSize = 0;
|
||||
VolleyLog.d("Cache cleared.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache entry with the specified key if it exists, null
|
||||
* otherwise.
|
||||
*/
|
||||
@Override
|
||||
public synchronized Entry get(String key) {
|
||||
CacheHeader entry = mEntries.get(key);
|
||||
// if the entry does not exist, return.
|
||||
if (entry == null) {
|
||||
if (key.contains("timestamp")) {
|
||||
Log.i("result", "get entrey is null");
|
||||
entry = mEntries.get(TimestampUtils.removeTimestamp(key));
|
||||
if (entry == null){
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File file = getFileForKey(key);
|
||||
if (!file.exists() && key.contains("timestamp")) {
|
||||
Log.i("result", "file = " + file.getName() + " not exists");
|
||||
file = getFileForKey(TimestampUtils.removeTimestamp(key));
|
||||
}
|
||||
Log.i("result", "key = " + key);
|
||||
Log.i("result", "name = " + file.getName());
|
||||
CountingInputStream cis = null;
|
||||
try {
|
||||
cis = new CountingInputStream(new FileInputStream(file));
|
||||
CacheHeader.readHeader(cis); // eat header
|
||||
byte[] data = streamToBytes(cis,
|
||||
(int) (file.length() - cis.bytesRead));
|
||||
return entry.toCacheEntry(data);
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
|
||||
remove(key);
|
||||
return null;
|
||||
} finally {
|
||||
if (cis != null) {
|
||||
try {
|
||||
cis.close();
|
||||
} catch (IOException ioe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized byte[] getData(String key) {
|
||||
File file = getFileForKey(key);
|
||||
CountingInputStream cis = null;
|
||||
try {
|
||||
cis = new CountingInputStream(new FileInputStream(file));
|
||||
CacheHeader.readHeader(cis); // eat header
|
||||
return streamToBytes(cis, (int) (file.length() - cis.bytesRead));
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
|
||||
return null;
|
||||
} finally {
|
||||
if (cis != null) {
|
||||
try {
|
||||
cis.close();
|
||||
} catch (IOException ioe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void modify(String key, byte[] data) {
|
||||
File file = getFileForKey(key);
|
||||
try {
|
||||
CountingInputStream cis = new CountingInputStream(new FileInputStream(file));
|
||||
CacheHeader e = CacheHeader.readHeader(cis); // eat header
|
||||
Entry entry = e.toCacheEntry(data);
|
||||
cis.close();
|
||||
put(key, entry);
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the DiskBasedCache by scanning for all files currently in the
|
||||
* specified root directory. Creates the root directory if necessary.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void initialize() {
|
||||
if (!mRootDirectory.exists()) {
|
||||
if (!mRootDirectory.mkdirs()) {
|
||||
VolleyLog.e("Unable to create cache dir %s",
|
||||
mRootDirectory.getAbsolutePath());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
File[] files = mRootDirectory.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(file);
|
||||
CacheHeader entry = CacheHeader.readHeader(fis);
|
||||
entry.size = file.length();
|
||||
putEntry(entry.key, entry);
|
||||
} catch (IOException e) {
|
||||
if (file != null) {
|
||||
file.delete();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates an entry in the cache.
|
||||
*
|
||||
* @param key
|
||||
* Cache key
|
||||
* @param fullExpire
|
||||
* True to fully expire the entry, false to soft expire
|
||||
*/
|
||||
@Override
|
||||
public synchronized void invalidate(String key, boolean fullExpire) {
|
||||
Entry entry = get(key);
|
||||
if (entry != null) {
|
||||
entry.softTtl = 0;
|
||||
if (fullExpire) {
|
||||
entry.ttl = 0;
|
||||
}
|
||||
put(key, entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the entry with the specified key into the cache.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void put(String key, Entry entry) {
|
||||
pruneIfNeeded(entry.data.length);
|
||||
File file = getFileForKey(key);
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
CacheHeader e = new CacheHeader(key, entry);
|
||||
e.writeHeader(fos);
|
||||
fos.write(entry.data);
|
||||
fos.close();
|
||||
putEntry(key, e);
|
||||
// 如果url包含timestamp参数,则去掉该参数再存一份缓存
|
||||
if (key.contains("timestamp")) {
|
||||
put(TimestampUtils.removeTimestamp(key), entry);
|
||||
}
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
}
|
||||
boolean deleted = file.delete();
|
||||
if (!deleted) {
|
||||
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified key from the cache if it exists.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void remove(String key) {
|
||||
boolean deleted = getFileForKey(key).delete();
|
||||
removeEntry(key);
|
||||
if (!deleted) {
|
||||
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
|
||||
key, getFilenameForKey(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pseudo-unique filename for the specified cache key.
|
||||
*
|
||||
* @param key
|
||||
* The key to generate a file name for.
|
||||
* @return A pseudo-unique filename.
|
||||
*/
|
||||
private String getFilenameForKey(String key) {
|
||||
int firstHalfLength = key.length() / 2;
|
||||
String localFilename = String.valueOf(key.substring(0, firstHalfLength)
|
||||
.hashCode());
|
||||
localFilename += String.valueOf(key.substring(firstHalfLength)
|
||||
.hashCode());
|
||||
return localFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a file object for the given cache key.
|
||||
*/
|
||||
public File getFileForKey(String key) {
|
||||
return new File(mRootDirectory, getFilenameForKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes the cache to fit the amount of bytes specified.
|
||||
*
|
||||
* @param neededSpace
|
||||
* The amount of bytes we are trying to fit into the cache.
|
||||
*/
|
||||
private void pruneIfNeeded(int neededSpace) {
|
||||
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
|
||||
return;
|
||||
}
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("Pruning old cache entries.");
|
||||
}
|
||||
|
||||
long before = mTotalSize;
|
||||
int prunedFiles = 0;
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
|
||||
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet()
|
||||
.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, CacheHeader> entry = iterator.next();
|
||||
CacheHeader e = entry.getValue();
|
||||
boolean deleted = getFileForKey(e.key).delete();
|
||||
if (deleted) {
|
||||
mTotalSize -= e.size;
|
||||
} else {
|
||||
VolleyLog.d(
|
||||
"Could not delete cache entry for key=%s, filename=%s",
|
||||
e.key, getFilenameForKey(e.key));
|
||||
}
|
||||
iterator.remove();
|
||||
prunedFiles++;
|
||||
|
||||
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes
|
||||
* HYSTERESIS_FACTOR) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("pruned %d files, %d bytes, %d ms", prunedFiles,
|
||||
(mTotalSize - before), SystemClock.elapsedRealtime()
|
||||
- startTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the entry with the specified key into the cache.
|
||||
*
|
||||
* @param key
|
||||
* The key to identify the entry by.
|
||||
* @param entry
|
||||
* The entry to cache.
|
||||
*/
|
||||
private void putEntry(String key, CacheHeader entry) {
|
||||
if (!mEntries.containsKey(key)) {
|
||||
mTotalSize += entry.size;
|
||||
} else {
|
||||
CacheHeader oldEntry = mEntries.get(key);
|
||||
mTotalSize += (entry.size - oldEntry.size);
|
||||
}
|
||||
mEntries.put(key, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the entry identified by 'key' from the cache.
|
||||
*/
|
||||
private void removeEntry(String key) {
|
||||
CacheHeader entry = mEntries.get(key);
|
||||
if (entry != null) {
|
||||
mTotalSize -= entry.size;
|
||||
mEntries.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of an InputStream into a byte[].
|
||||
* */
|
||||
private static byte[] streamToBytes(InputStream in, int length)
|
||||
throws IOException {
|
||||
byte[] bytes = new byte[length];
|
||||
int count;
|
||||
int pos = 0;
|
||||
while (pos < length
|
||||
&& ((count = in.read(bytes, pos, length - pos)) != -1)) {
|
||||
pos += count;
|
||||
}
|
||||
if (pos != length) {
|
||||
throw new IOException("Expected " + length + " bytes, read " + pos
|
||||
+ " bytes");
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles holding onto the cache headers for an entry.
|
||||
*/
|
||||
// Visible for testing.
|
||||
static class CacheHeader {
|
||||
/**
|
||||
* The size of the data identified by this CacheHeader. (This is not
|
||||
* serialized to disk.
|
||||
*/
|
||||
public long size;
|
||||
|
||||
/** The key that identifies the cache entry. */
|
||||
public String key;
|
||||
|
||||
/** ETag for cache coherence. */
|
||||
public String etag;
|
||||
|
||||
/** Date of this response as reported by the server. */
|
||||
public long serverDate;
|
||||
|
||||
/** TTL for this record. */
|
||||
public long ttl;
|
||||
|
||||
/** Soft TTL for this record. */
|
||||
public long softTtl;
|
||||
|
||||
/** Headers from the response resulting in this cache entry. */
|
||||
public Map<String, String> responseHeaders;
|
||||
|
||||
private CacheHeader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new CacheHeader object
|
||||
*
|
||||
* @param key
|
||||
* The key that identifies the cache entry
|
||||
* @param entry
|
||||
* The cache entry.
|
||||
*/
|
||||
public CacheHeader(String key, Entry entry) {
|
||||
this.key = key;
|
||||
this.size = entry.data.length;
|
||||
this.etag = entry.etag;
|
||||
this.serverDate = entry.serverDate;
|
||||
this.ttl = entry.ttl;
|
||||
this.softTtl = entry.softTtl;
|
||||
this.responseHeaders = entry.responseHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the header off of an InputStream and returns a CacheHeader
|
||||
* object.
|
||||
*
|
||||
* @param is
|
||||
* The InputStream to read from.
|
||||
* @throws IOException
|
||||
*/
|
||||
public static CacheHeader readHeader(InputStream is) throws IOException {
|
||||
CacheHeader entry = new CacheHeader();
|
||||
int magic = readInt(is);
|
||||
if (magic != CACHE_MAGIC) {
|
||||
// don't bother deleting, it'll get pruned eventually
|
||||
throw new IOException();
|
||||
}
|
||||
entry.key = readString(is);
|
||||
entry.etag = readString(is);
|
||||
if (entry.etag.equals("")) {
|
||||
entry.etag = null;
|
||||
}
|
||||
entry.serverDate = readLong(is);
|
||||
entry.ttl = readLong(is);
|
||||
entry.softTtl = readLong(is);
|
||||
entry.responseHeaders = readStringStringMap(is);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cache entry for the specified data.
|
||||
*/
|
||||
public Entry toCacheEntry(byte[] data) {
|
||||
Entry e = new Entry();
|
||||
e.data = data;
|
||||
e.etag = etag;
|
||||
e.serverDate = serverDate;
|
||||
e.ttl = ttl;
|
||||
e.softTtl = softTtl;
|
||||
e.responseHeaders = responseHeaders;
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of this CacheHeader to the specified
|
||||
* OutputStream.
|
||||
*/
|
||||
public boolean writeHeader(OutputStream os) {
|
||||
try {
|
||||
writeInt(os, CACHE_MAGIC);
|
||||
writeString(os, key);
|
||||
writeString(os, etag == null ? "" : etag);
|
||||
writeLong(os, serverDate);
|
||||
writeLong(os, ttl);
|
||||
writeLong(os, softTtl);
|
||||
writeStringStringMap(responseHeaders, os);
|
||||
os.flush();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s", e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class CountingInputStream extends FilterInputStream {
|
||||
private int bytesRead = 0;
|
||||
|
||||
private CountingInputStream(InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int result = super.read();
|
||||
if (result != -1) {
|
||||
bytesRead++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count)
|
||||
throws IOException {
|
||||
int result = super.read(buffer, offset, count);
|
||||
if (result != -1) {
|
||||
bytesRead += result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Homebrewed simple serialization system used for reading and writing cache
|
||||
* headers on disk. Once upon a time, this used the standard Java
|
||||
* Object{Input,Output}Stream, but the default implementation relies heavily
|
||||
* on reflection (even for standard types) and generates a ton of garbage.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Simple wrapper around {@link InputStream#read()} that throws EOFException
|
||||
* instead of returning -1.
|
||||
*/
|
||||
private static int read(InputStream is) throws IOException {
|
||||
int b = is.read();
|
||||
if (b == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
static void writeInt(OutputStream os, int n) throws IOException {
|
||||
os.write((n >> 0) & 0xff);
|
||||
os.write((n >> 8) & 0xff);
|
||||
os.write((n >> 16) & 0xff);
|
||||
os.write((n >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static int readInt(InputStream is) throws IOException {
|
||||
int n = 0;
|
||||
n |= (read(is) << 0);
|
||||
n |= (read(is) << 8);
|
||||
n |= (read(is) << 16);
|
||||
n |= (read(is) << 24);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void writeLong(OutputStream os, long n) throws IOException {
|
||||
os.write((byte) (n >>> 0));
|
||||
os.write((byte) (n >>> 8));
|
||||
os.write((byte) (n >>> 16));
|
||||
os.write((byte) (n >>> 24));
|
||||
os.write((byte) (n >>> 32));
|
||||
os.write((byte) (n >>> 40));
|
||||
os.write((byte) (n >>> 48));
|
||||
os.write((byte) (n >>> 56));
|
||||
}
|
||||
|
||||
static long readLong(InputStream is) throws IOException {
|
||||
long n = 0;
|
||||
n |= ((read(is) & 0xFFL) << 0);
|
||||
n |= ((read(is) & 0xFFL) << 8);
|
||||
n |= ((read(is) & 0xFFL) << 16);
|
||||
n |= ((read(is) & 0xFFL) << 24);
|
||||
n |= ((read(is) & 0xFFL) << 32);
|
||||
n |= ((read(is) & 0xFFL) << 40);
|
||||
n |= ((read(is) & 0xFFL) << 48);
|
||||
n |= ((read(is) & 0xFFL) << 56);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void writeString(OutputStream os, String s) throws IOException {
|
||||
byte[] b = s.getBytes("UTF-8");
|
||||
writeLong(os, b.length);
|
||||
os.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
static String readString(InputStream is) throws IOException {
|
||||
int n = (int) readLong(is);
|
||||
byte[] b = streamToBytes(is, n);
|
||||
return new String(b, "UTF-8");
|
||||
}
|
||||
|
||||
static void writeStringStringMap(Map<String, String> map, OutputStream os)
|
||||
throws IOException {
|
||||
if (map != null) {
|
||||
writeInt(os, map.size());
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
writeString(os, entry.getKey());
|
||||
writeString(os, entry.getValue());
|
||||
}
|
||||
} else {
|
||||
writeInt(os, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, String> readStringStringMap(InputStream is)
|
||||
throws IOException {
|
||||
int size = readInt(is);
|
||||
Map<String, String> result = (size == 0) ? Collections
|
||||
.<String, String> emptyMap()
|
||||
: new HashMap<String, String>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
String key = readString(is).intern();
|
||||
String value = readString(is).intern();
|
||||
result.put(key, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Request.Method;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpDelete;
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpPut;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An HttpStack that performs request over an {@link HttpClient}.
|
||||
*/
|
||||
public class HttpClientStack implements HttpStack {
|
||||
protected final HttpClient mClient;
|
||||
|
||||
private final static String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
|
||||
public HttpClientStack(HttpClient client) {
|
||||
mClient = client;
|
||||
}
|
||||
|
||||
private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
|
||||
for (String key : headers.keySet()) {
|
||||
httpRequest.setHeader(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) {
|
||||
List<NameValuePair> result = new ArrayList<NameValuePair>(postParams.size());
|
||||
for (String key : postParams.keySet()) {
|
||||
result.add(new BasicNameValuePair(key, postParams.get(key)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
|
||||
throws IOException, AuthFailureError {
|
||||
HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
|
||||
addHeaders(httpRequest, additionalHeaders);
|
||||
addHeaders(httpRequest, request.getHeaders());
|
||||
onPrepareRequest(httpRequest);
|
||||
HttpParams httpParams = httpRequest.getParams();
|
||||
int timeoutMs = request.getTimeoutMs();
|
||||
// TODO: Reevaluate this connection timeout based on more wide-scale
|
||||
// data collection and possibly different for wifi vs. 3G.
|
||||
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
|
||||
HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
|
||||
return mClient.execute(httpRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the appropriate subclass of HttpUriRequest for passed in request.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
/* protected */ static HttpUriRequest createHttpRequest(Request<?> request,
|
||||
Map<String, String> additionalHeaders) throws AuthFailureError {
|
||||
switch (request.getMethod()) {
|
||||
case Method.DEPRECATED_GET_OR_POST: {
|
||||
// This is the deprecated way that needs to be handled for backwards compatibility.
|
||||
// If the request's post body is null, then the assumption is that the request is
|
||||
// GET. Otherwise, it is assumed that the request is a POST.
|
||||
byte[] postBody = request.getPostBody();
|
||||
if (postBody != null) {
|
||||
HttpPost postRequest = new HttpPost(request.getUrl());
|
||||
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType());
|
||||
HttpEntity entity;
|
||||
entity = new ByteArrayEntity(postBody);
|
||||
postRequest.setEntity(entity);
|
||||
return postRequest;
|
||||
} else {
|
||||
return new HttpGet(request.getUrl());
|
||||
}
|
||||
}
|
||||
case Method.GET:
|
||||
return new HttpGet(request.getUrl());
|
||||
case Method.DELETE:
|
||||
return new HttpDelete(request.getUrl());
|
||||
case Method.POST: {
|
||||
HttpPost postRequest = new HttpPost(request.getUrl());
|
||||
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
|
||||
setEntityIfNonEmptyBody(postRequest, request);
|
||||
return postRequest;
|
||||
}
|
||||
case Method.PUT: {
|
||||
HttpPut putRequest = new HttpPut(request.getUrl());
|
||||
putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
|
||||
setEntityIfNonEmptyBody(putRequest, request);
|
||||
return putRequest;
|
||||
}
|
||||
default:
|
||||
throw new IllegalStateException("Unknown request method.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest,
|
||||
Request<?> request) throws AuthFailureError {
|
||||
byte[] body = request.getBody();
|
||||
if (body != null) {
|
||||
HttpEntity entity = new ByteArrayEntity(body);
|
||||
httpRequest.setEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the request is executed using the underlying HttpClient.
|
||||
*
|
||||
* <p>Overwrite in subclasses to augment the request.</p>
|
||||
*/
|
||||
protected void onPrepareRequest(HttpUriRequest request) throws IOException {
|
||||
// Nothing.
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.NetworkResponse;
|
||||
|
||||
import org.apache.http.impl.cookie.DateParseException;
|
||||
import org.apache.http.impl.cookie.DateUtils;
|
||||
import org.apache.http.protocol.HTTP;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility methods for parsing HTTP headers.
|
||||
*/
|
||||
public class HttpHeaderParser {
|
||||
|
||||
/**
|
||||
* Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
|
||||
*
|
||||
* @param response The network response to parse headers from
|
||||
* @return a cache entry for the given response, or null if the response is not cacheable.
|
||||
*/
|
||||
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
Map<String, String> headers = response.headers;
|
||||
|
||||
long serverDate = 0;
|
||||
long serverExpires = 0;
|
||||
long softExpire = 0;
|
||||
long maxAge = 0;
|
||||
boolean hasCacheControl = false;
|
||||
|
||||
String serverEtag = null;
|
||||
String headerValue;
|
||||
|
||||
headerValue = headers.get("Date");
|
||||
if (headerValue != null) {
|
||||
serverDate = parseDateAsEpoch(headerValue);
|
||||
}
|
||||
|
||||
headerValue = headers.get("Cache-Control");
|
||||
if (headerValue != null) {
|
||||
hasCacheControl = true;
|
||||
String[] tokens = headerValue.split(",");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
String token = tokens[i].trim();
|
||||
if (token.equals("no-cache") || token.equals("no-store")) {
|
||||
return null;
|
||||
} else if (token.startsWith("max-age=")) {
|
||||
try {
|
||||
maxAge = Long.parseLong(token.substring(8));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
|
||||
maxAge = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headerValue = headers.get("Expires");
|
||||
if (headerValue != null) {
|
||||
serverExpires = parseDateAsEpoch(headerValue);
|
||||
}
|
||||
|
||||
serverEtag = headers.get("ETag");
|
||||
|
||||
// Cache-Control takes precedence over an Expires header, even if both exist and Expires
|
||||
// is more restrictive.
|
||||
if (hasCacheControl) {
|
||||
softExpire = now + maxAge * 1000;
|
||||
} else if (serverDate > 0 && serverExpires >= serverDate) {
|
||||
// Default semantic for Expire header in HTTP specification is softExpire.
|
||||
softExpire = now + (serverExpires - serverDate);
|
||||
}
|
||||
|
||||
Cache.Entry entry = new Cache.Entry();
|
||||
entry.data = response.data;
|
||||
entry.etag = serverEtag;
|
||||
entry.softTtl = softExpire;
|
||||
entry.ttl = entry.softTtl;
|
||||
entry.serverDate = serverDate;
|
||||
entry.responseHeaders = headers;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date in RFC1123 format, and return its value as epoch
|
||||
*/
|
||||
public static long parseDateAsEpoch(String dateStr) {
|
||||
try {
|
||||
// Parse date in RFC1123 format if this header contains one
|
||||
return DateUtils.parseDate(dateStr).getTime();
|
||||
} catch (DateParseException e) {
|
||||
// Date in invalid format, fallback to 0
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the charset specified in the Content-Type of this header,
|
||||
* or the HTTP default (ISO-8859-1) if none can be found.
|
||||
*/
|
||||
public static String parseCharset(Map<String, String> headers) {
|
||||
String contentType = headers.get(HTTP.CONTENT_TYPE);
|
||||
if (contentType != null) {
|
||||
String[] params = contentType.split(";");
|
||||
for (int i = 1; i < params.length; i++) {
|
||||
String[] pair = params[i].trim().split("=");
|
||||
if (pair.length == 2) {
|
||||
if (pair[0].equals("charset")) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return HTTP.UTF_8;
|
||||
}
|
||||
}
|
||||
45
app/src/main/java/com/android/volley/toolbox/HttpStack.java
Normal file
45
app/src/main/java/com/android/volley/toolbox/HttpStack.java
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An HTTP stack abstraction.
|
||||
*/
|
||||
public interface HttpStack {
|
||||
/**
|
||||
* Performs an HTTP request with the given parameters.
|
||||
*
|
||||
* <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
|
||||
* and the Content-Type header is set to request.getPostBodyContentType().</p>
|
||||
*
|
||||
* @param request the request to perform
|
||||
* @param additionalHeaders additional headers to be sent together with
|
||||
* {@link Request#getHeaders()}
|
||||
* @return the HTTP response
|
||||
*/
|
||||
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
|
||||
throws IOException, AuthFailureError;
|
||||
|
||||
}
|
||||
265
app/src/main/java/com/android/volley/toolbox/HurlStack.java
Normal file
265
app/src/main/java/com/android/volley/toolbox/HurlStack.java
Normal file
@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Request.Method;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.ProtocolVersion;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.entity.BasicHttpEntity;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.apache.http.message.BasicHttpResponse;
|
||||
import org.apache.http.message.BasicStatusLine;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
/**
|
||||
* An {@link HttpStack} based on {@link HttpURLConnection}.
|
||||
*/
|
||||
public class HurlStack implements HttpStack {
|
||||
|
||||
private static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
|
||||
/**
|
||||
* An interface for transforming URLs before use.
|
||||
*/
|
||||
public interface UrlRewriter {
|
||||
/**
|
||||
* Returns a URL to use instead of the provided one, or null to indicate
|
||||
* this URL should not be used at all.
|
||||
*/
|
||||
public String rewriteUrl(String originalUrl);
|
||||
}
|
||||
|
||||
private final UrlRewriter mUrlRewriter;
|
||||
private final SSLSocketFactory mSslSocketFactory;
|
||||
|
||||
public HurlStack() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param urlRewriter
|
||||
* Rewriter to use for request URLs
|
||||
*/
|
||||
public HurlStack(UrlRewriter urlRewriter) {
|
||||
this(urlRewriter, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param urlRewriter
|
||||
* Rewriter to use for request URLs
|
||||
* @param sslSocketFactory
|
||||
* SSL factory to use for HTTPS connections
|
||||
*/
|
||||
public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
|
||||
mUrlRewriter = urlRewriter;
|
||||
mSslSocketFactory = sslSocketFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse performRequest(Request<?> request,
|
||||
Map<String, String> additionalHeaders) throws IOException,
|
||||
AuthFailureError {
|
||||
String url = request.getUrl();
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.putAll(request.getHeaders());
|
||||
map.putAll(additionalHeaders);
|
||||
if (mUrlRewriter != null) {
|
||||
String rewritten = mUrlRewriter.rewriteUrl(url);
|
||||
if (rewritten == null) {
|
||||
throw new IOException("URL blocked by rewriter: " + url);
|
||||
}
|
||||
url = rewritten;
|
||||
}
|
||||
URL parsedUrl = new URL(url);
|
||||
HttpURLConnection connection = openConnection(parsedUrl, request);
|
||||
|
||||
// if have etag, don't use lastmodified
|
||||
// because the aliyun cnd will use lastmodified do something we don't know
|
||||
if (map.containsKey("If-None-Match")) {
|
||||
map.remove("If-Modified-Since");
|
||||
}
|
||||
|
||||
for (String headerName : map.keySet()) {
|
||||
connection.addRequestProperty(headerName, map.get(headerName));
|
||||
}
|
||||
|
||||
setConnectionParametersForRequest(connection, request);
|
||||
// Initialize HttpResponse with data from the HttpURLConnection.
|
||||
ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
if (responseCode == -1) {
|
||||
// -1 is returned by getResponseCode() if the response code could
|
||||
// not be retrieved.
|
||||
// Signal to the caller that something was wrong with the
|
||||
// connection.
|
||||
throw new IOException(
|
||||
"Could not retrieve response code from HttpUrlConnection.");
|
||||
}
|
||||
StatusLine responseStatus = new BasicStatusLine(protocolVersion,
|
||||
connection.getResponseCode(), connection.getResponseMessage());
|
||||
BasicHttpResponse response = new BasicHttpResponse(responseStatus);
|
||||
response.setEntity(entityFromConnection(connection));
|
||||
for (Entry<String, List<String>> header : connection.getHeaderFields()
|
||||
.entrySet()) {
|
||||
if (header.getKey() != null) {
|
||||
Header h = new BasicHeader(header.getKey(), header.getValue()
|
||||
.get(0));
|
||||
response.addHeader(h);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an {@link HttpEntity} from the given
|
||||
* {@link HttpURLConnection}.
|
||||
*
|
||||
* @param connection
|
||||
* @return an HttpEntity populated with data from <code>connection</code>.
|
||||
*/
|
||||
private static HttpEntity entityFromConnection(HttpURLConnection connection) {
|
||||
BasicHttpEntity entity = new BasicHttpEntity();
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = connection.getInputStream();
|
||||
} catch (IOException ioe) {
|
||||
inputStream = connection.getErrorStream();
|
||||
}
|
||||
entity.setContent(inputStream);
|
||||
entity.setContentLength(connection.getContentLength());
|
||||
entity.setContentEncoding(connection.getContentEncoding());
|
||||
entity.setContentType(connection.getContentType());
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link HttpURLConnection} for the specified {@code url}.
|
||||
*/
|
||||
protected HttpURLConnection createConnection(URL url) throws IOException {
|
||||
return (HttpURLConnection) url.openConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an {@link HttpURLConnection} with parameters.
|
||||
*
|
||||
* @param url
|
||||
* @return an open connection
|
||||
* @throws IOException
|
||||
*/
|
||||
private HttpURLConnection openConnection(URL url, Request<?> request)
|
||||
throws IOException {
|
||||
HttpURLConnection connection = createConnection(url);
|
||||
|
||||
int timeoutMs = request.getTimeoutMs();
|
||||
connection.setConnectTimeout(timeoutMs);
|
||||
connection.setReadTimeout(timeoutMs);
|
||||
connection.setUseCaches(false);
|
||||
connection.setDoInput(true);
|
||||
|
||||
// use caller-provided custom SslSocketFactory, if any, for HTTPS
|
||||
if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
|
||||
((HttpsURLConnection) connection)
|
||||
.setSSLSocketFactory(mSslSocketFactory);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
/* package */static void setConnectionParametersForRequest(
|
||||
HttpURLConnection connection, Request<?> request)
|
||||
throws IOException, AuthFailureError {
|
||||
switch (request.getMethod()) {
|
||||
case Method.DEPRECATED_GET_OR_POST:
|
||||
// This is the deprecated way that needs to be handled for backwards
|
||||
// compatibility.
|
||||
// If the request's post body is null, then the assumption is that
|
||||
// the request is
|
||||
// GET. Otherwise, it is assumed that the request is a POST.
|
||||
byte[] postBody = request.getPostBody();
|
||||
if (postBody != null) {
|
||||
// Prepare output. There is no need to set Content-Length
|
||||
// explicitly,
|
||||
// since this is handled by HttpURLConnection using the size of
|
||||
// the prepared
|
||||
// output stream.
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.addRequestProperty(HEADER_CONTENT_TYPE,
|
||||
request.getPostBodyContentType());
|
||||
DataOutputStream out = new DataOutputStream(
|
||||
connection.getOutputStream());
|
||||
out.write(postBody);
|
||||
out.close();
|
||||
}
|
||||
break;
|
||||
case Method.GET:
|
||||
// Not necessary to set the request method because connection
|
||||
// defaults to GET but
|
||||
// being explicit here.
|
||||
connection.setRequestMethod("GET");
|
||||
break;
|
||||
case Method.DELETE:
|
||||
connection.setRequestMethod("DELETE");
|
||||
break;
|
||||
case Method.POST:
|
||||
connection.setRequestMethod("POST");
|
||||
addBodyIfExists(connection, request);
|
||||
break;
|
||||
case Method.PUT:
|
||||
connection.setRequestMethod("PUT");
|
||||
addBodyIfExists(connection, request);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown method type.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void addBodyIfExists(HttpURLConnection connection,
|
||||
Request<?> request) throws IOException, AuthFailureError {
|
||||
byte[] body = request.getBody();
|
||||
if (body != null) {
|
||||
connection.setDoOutput(true);
|
||||
connection.addRequestProperty(HEADER_CONTENT_TYPE,
|
||||
request.getBodyContentType());
|
||||
DataOutputStream out = new DataOutputStream(
|
||||
connection.getOutputStream());
|
||||
out.write(body);
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
479
app/src/main/java/com/android/volley/toolbox/ImageLoader.java
Normal file
479
app/src/main/java/com/android/volley/toolbox/ImageLoader.java
Normal file
@ -0,0 +1,479 @@
|
||||
/**
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.RequestQueue;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
import com.android.volley.VolleyError;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Helper that handles loading and caching images from remote URLs.
|
||||
*
|
||||
* The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
|
||||
* and to pass in the default image listener provided by
|
||||
* {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
|
||||
* this class must be made from the main thead, and all responses will be delivered to the main
|
||||
* thread as well.
|
||||
*/
|
||||
public class ImageLoader {
|
||||
/** RequestQueue for dispatching ImageRequests onto. */
|
||||
private final RequestQueue mRequestQueue;
|
||||
|
||||
/** Amount of time to wait after first response arrives before delivering all responses. */
|
||||
private int mBatchResponseDelayMs = 100;
|
||||
|
||||
/** The cache implementation to be used as an L1 cache before calling into volley. */
|
||||
private final ImageCache mCache;
|
||||
|
||||
/**
|
||||
* HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
|
||||
* that we can coalesce multiple requests to the same URL into a single network request.
|
||||
*/
|
||||
private final HashMap<String, BatchedImageRequest> mInFlightRequests =
|
||||
new HashMap<String, BatchedImageRequest>();
|
||||
|
||||
/** HashMap of the currently pending responses (waiting to be delivered). */
|
||||
private final HashMap<String, BatchedImageRequest> mBatchedResponses =
|
||||
new HashMap<String, BatchedImageRequest>();
|
||||
|
||||
/** Handler to the main thread. */
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
/** Runnable for in-flight response delivery. */
|
||||
private Runnable mRunnable;
|
||||
|
||||
/**
|
||||
* Simple cache adapter interface. If provided to the ImageLoader, it
|
||||
* will be used as an L1 cache before dispatch to Volley. Implementations
|
||||
* must not block. Implementation with an LruCache is recommended.
|
||||
*/
|
||||
public interface ImageCache {
|
||||
public Bitmap getBitmap(String url);
|
||||
public void putBitmap(String url, Bitmap bitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new ImageLoader.
|
||||
* @param queue The RequestQueue to use for making image requests.
|
||||
* @param imageCache The cache to use as an L1 cache.
|
||||
*/
|
||||
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
|
||||
mRequestQueue = queue;
|
||||
mCache = imageCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of ImageListener which handles basic functionality
|
||||
* of showing a default image until the network response is received, at which point
|
||||
* it will switch to either the actual image or the error image.
|
||||
* @param imageView The imageView that the listener is associated with.
|
||||
* @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
|
||||
* @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
|
||||
*/
|
||||
public static ImageListener getImageListener(final ImageView view,
|
||||
final int defaultImageResId, final int errorImageResId) {
|
||||
return new ImageListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
if (errorImageResId != 0) {
|
||||
view.setImageResource(errorImageResId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(ImageContainer response, boolean isImmediate) {
|
||||
if (response.getBitmap() != null) {
|
||||
view.setImageBitmap(response.getBitmap());
|
||||
} else if (defaultImageResId != 0) {
|
||||
view.setImageResource(defaultImageResId);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the response handlers on image requests.
|
||||
*
|
||||
* The call flow is this:
|
||||
* 1. Upon being attached to a request, onResponse(response, true) will
|
||||
* be invoked to reflect any cached data that was already available. If the
|
||||
* data was available, response.getBitmap() will be non-null.
|
||||
*
|
||||
* 2. After a network response returns, only one of the following cases will happen:
|
||||
* - onResponse(response, false) will be called if the image was loaded.
|
||||
* or
|
||||
* - onErrorResponse will be called if there was an error loading the image.
|
||||
*/
|
||||
public interface ImageListener extends ErrorListener {
|
||||
/**
|
||||
* Listens for non-error changes to the loading of the image request.
|
||||
*
|
||||
* @param response Holds all information pertaining to the request, as well
|
||||
* as the bitmap (if it is loaded).
|
||||
* @param isImmediate True if this was called during ImageLoader.get() variants.
|
||||
* This can be used to differentiate between a cached image loading and a network
|
||||
* image loading in order to, for example, run an animation to fade in network loaded
|
||||
* images.
|
||||
*/
|
||||
public void onResponse(ImageContainer response, boolean isImmediate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is available in the cache.
|
||||
* @param requestUrl The url of the remote image
|
||||
* @param maxWidth The maximum width of the returned image.
|
||||
* @param maxHeight The maximum height of the returned image.
|
||||
* @return True if the item exists in cache, false otherwise.
|
||||
*/
|
||||
public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
|
||||
throwIfNotOnMainThread();
|
||||
|
||||
String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
|
||||
return mCache.getBitmap(cacheKey) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ImageContainer for the requested URL.
|
||||
*
|
||||
* The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
|
||||
* If the default was returned, the {@link ImageLoader} will be invoked when the
|
||||
* request is fulfilled.
|
||||
*
|
||||
* @param requestUrl The URL of the image to be loaded.
|
||||
* @param defaultImage Optional default image to return until the actual image is loaded.
|
||||
*/
|
||||
public ImageContainer get(String requestUrl, final ImageListener listener) {
|
||||
return get(requestUrl, listener, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues a bitmap request with the given URL if that image is not available
|
||||
* in the cache, and returns a bitmap container that contains all of the data
|
||||
* relating to the request (as well as the default image if the requested
|
||||
* image is not available).
|
||||
* @param requestUrl The url of the remote image
|
||||
* @param imageListener The listener to call when the remote image is loaded
|
||||
* @param maxWidth The maximum width of the returned image.
|
||||
* @param maxHeight The maximum height of the returned image.
|
||||
* @return A container object that contains all of the properties of the request, as well as
|
||||
* the currently available image (default if remote is not loaded).
|
||||
*/
|
||||
public ImageContainer get(String requestUrl, ImageListener imageListener,
|
||||
int maxWidth, int maxHeight) {
|
||||
// only fulfill requests that were initiated from the main thread.
|
||||
throwIfNotOnMainThread();
|
||||
|
||||
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
|
||||
|
||||
// Try to look up the request in the cache of remote images.
|
||||
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
|
||||
if (cachedBitmap != null) {
|
||||
// Return the cached bitmap.
|
||||
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
|
||||
imageListener.onResponse(container, true);
|
||||
return container;
|
||||
}
|
||||
|
||||
// The bitmap did not exist in the cache, fetch it!
|
||||
ImageContainer imageContainer =
|
||||
new ImageContainer(null, requestUrl, cacheKey, imageListener);
|
||||
|
||||
// Update the caller to let them know that they should use the default bitmap.
|
||||
imageListener.onResponse(imageContainer, true);
|
||||
|
||||
// Check to see if a request is already in-flight.
|
||||
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
|
||||
if (request != null) {
|
||||
// If it is, add this request to the list of listeners.
|
||||
request.addContainer(imageContainer);
|
||||
return imageContainer;
|
||||
}
|
||||
|
||||
// The request is not already in flight. Send the new request to the network and
|
||||
// track it.
|
||||
Request<?> newRequest =
|
||||
new ImageRequest(requestUrl, new Listener<Bitmap>() {
|
||||
@Override
|
||||
public void onResponse(Bitmap response) {
|
||||
onGetImageSuccess(cacheKey, response);
|
||||
}
|
||||
}, maxWidth, maxHeight,
|
||||
Config.RGB_565, new ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onGetImageError(cacheKey, error);
|
||||
}
|
||||
});
|
||||
|
||||
mRequestQueue.add(newRequest);
|
||||
mInFlightRequests.put(cacheKey,
|
||||
new BatchedImageRequest(newRequest, imageContainer));
|
||||
return imageContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the amount of time to wait after the first response arrives before delivering all
|
||||
* responses. Batching can be disabled entirely by passing in 0.
|
||||
* @param newBatchedResponseDelayMs The time in milliseconds to wait.
|
||||
*/
|
||||
public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
|
||||
mBatchResponseDelayMs = newBatchedResponseDelayMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when an image was successfully loaded.
|
||||
* @param cacheKey The cache key that is associated with the image request.
|
||||
* @param response The bitmap that was returned from the network.
|
||||
*/
|
||||
private void onGetImageSuccess(String cacheKey, Bitmap response) {
|
||||
// cache the image that was fetched.
|
||||
mCache.putBitmap(cacheKey, response);
|
||||
|
||||
// remove the request from the list of in-flight requests.
|
||||
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
|
||||
|
||||
if (request != null) {
|
||||
// Update the response bitmap.
|
||||
request.mResponseBitmap = response;
|
||||
|
||||
// Send the batched response
|
||||
batchResponse(cacheKey, request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when an image failed to load.
|
||||
* @param cacheKey The cache key that is associated with the image request.
|
||||
*/
|
||||
private void onGetImageError(String cacheKey, VolleyError error) {
|
||||
// Notify the requesters that something failed via a null result.
|
||||
// Remove this request from the list of in-flight requests.
|
||||
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
|
||||
|
||||
// Set the error for this request
|
||||
request.setError(error);
|
||||
|
||||
if (request != null) {
|
||||
// Send the batched response
|
||||
batchResponse(cacheKey, request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container object for all of the data surrounding an image request.
|
||||
*/
|
||||
public class ImageContainer {
|
||||
/**
|
||||
* The most relevant bitmap for the container. If the image was in cache, the
|
||||
* Holder to use for the final bitmap (the one that pairs to the requested URL).
|
||||
*/
|
||||
private Bitmap mBitmap;
|
||||
|
||||
private final ImageListener mListener;
|
||||
|
||||
/** The cache key that was associated with the request */
|
||||
private final String mCacheKey;
|
||||
|
||||
/** The request URL that was specified */
|
||||
private final String mRequestUrl;
|
||||
|
||||
/**
|
||||
* Constructs a BitmapContainer object.
|
||||
* @param bitmap The final bitmap (if it exists).
|
||||
* @param requestUrl The requested URL for this container.
|
||||
* @param cacheKey The cache key that identifies the requested URL for this container.
|
||||
*/
|
||||
public ImageContainer(Bitmap bitmap, String requestUrl,
|
||||
String cacheKey, ImageListener listener) {
|
||||
mBitmap = bitmap;
|
||||
mRequestUrl = requestUrl;
|
||||
mCacheKey = cacheKey;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases interest in the in-flight request (and cancels it if no one else is listening).
|
||||
*/
|
||||
public void cancelRequest() {
|
||||
if (mListener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
|
||||
if (request != null) {
|
||||
boolean canceled = request.removeContainerAndCancelIfNecessary(this);
|
||||
if (canceled) {
|
||||
mInFlightRequests.remove(mCacheKey);
|
||||
}
|
||||
} else {
|
||||
// check to see if it is already batched for delivery.
|
||||
request = mBatchedResponses.get(mCacheKey);
|
||||
if (request != null) {
|
||||
request.removeContainerAndCancelIfNecessary(this);
|
||||
if (request.mContainers.size() == 0) {
|
||||
mBatchedResponses.remove(mCacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
|
||||
*/
|
||||
public Bitmap getBitmap() {
|
||||
return mBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested URL for this container.
|
||||
*/
|
||||
public String getRequestUrl() {
|
||||
return mRequestUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class used to map a Request to the set of active ImageContainer objects that are
|
||||
* interested in its results.
|
||||
*/
|
||||
private class BatchedImageRequest {
|
||||
/** The request being tracked */
|
||||
private final Request<?> mRequest;
|
||||
|
||||
/** The result of the request being tracked by this item */
|
||||
private Bitmap mResponseBitmap;
|
||||
|
||||
/** Error if one occurred for this response */
|
||||
private VolleyError mError;
|
||||
|
||||
/** List of all of the active ImageContainers that are interested in the request */
|
||||
private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
|
||||
|
||||
/**
|
||||
* Constructs a new BatchedImageRequest object
|
||||
* @param request The request being tracked
|
||||
* @param container The ImageContainer of the person who initiated the request.
|
||||
*/
|
||||
public BatchedImageRequest(Request<?> request, ImageContainer container) {
|
||||
mRequest = request;
|
||||
mContainers.add(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error for this response
|
||||
*/
|
||||
public void setError(VolleyError error) {
|
||||
mError = error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error for this response
|
||||
*/
|
||||
public VolleyError getError() {
|
||||
return mError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another ImageContainer to the list of those interested in the results of
|
||||
* the request.
|
||||
*/
|
||||
public void addContainer(ImageContainer container) {
|
||||
mContainers.add(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detatches the bitmap container from the request and cancels the request if no one is
|
||||
* left listening.
|
||||
* @param container The container to remove from the list
|
||||
* @return True if the request was canceled, false otherwise.
|
||||
*/
|
||||
public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
|
||||
mContainers.remove(container);
|
||||
if (mContainers.size() == 0) {
|
||||
mRequest.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the runnable for batched delivery of responses if it is not already started.
|
||||
* @param cacheKey The cacheKey of the response being delivered.
|
||||
* @param request The BatchedImageRequest to be delivered.
|
||||
* @param error The volley error associated with the request (if applicable).
|
||||
*/
|
||||
private void batchResponse(String cacheKey, BatchedImageRequest request) {
|
||||
mBatchedResponses.put(cacheKey, request);
|
||||
// If we don't already have a batch delivery runnable in flight, make a new one.
|
||||
// Note that this will be used to deliver responses to all callers in mBatchedResponses.
|
||||
if (mRunnable == null) {
|
||||
mRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (BatchedImageRequest bir : mBatchedResponses.values()) {
|
||||
for (ImageContainer container : bir.mContainers) {
|
||||
// If one of the callers in the batched request canceled the request
|
||||
// after the response was received but before it was delivered,
|
||||
// skip them.
|
||||
if (container.mListener == null) {
|
||||
continue;
|
||||
}
|
||||
if (bir.getError() == null) {
|
||||
container.mBitmap = bir.mResponseBitmap;
|
||||
container.mListener.onResponse(container, false);
|
||||
} else {
|
||||
container.mListener.onErrorResponse(bir.getError());
|
||||
}
|
||||
}
|
||||
}
|
||||
mBatchedResponses.clear();
|
||||
mRunnable = null;
|
||||
}
|
||||
|
||||
};
|
||||
// Post the runnable.
|
||||
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
private void throwIfNotOnMainThread() {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates a cache key for use with the L1 cache.
|
||||
* @param url The URL of the request.
|
||||
* @param maxWidth The max-width of the output.
|
||||
* @param maxHeight The max-height of the output.
|
||||
*/
|
||||
private static String getCacheKey(String url, int maxWidth, int maxHeight) {
|
||||
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
|
||||
.append("#H").append(maxHeight).append(url).toString();
|
||||
}
|
||||
}
|
||||
211
app/src/main/java/com/android/volley/toolbox/ImageRequest.java
Normal file
211
app/src/main/java/com/android/volley/toolbox/ImageRequest.java
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.android.volley.DefaultRetryPolicy;
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.ParseError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyLog;
|
||||
|
||||
/**
|
||||
* A canned request for getting an image at a given URL and calling
|
||||
* back with a decoded Bitmap.
|
||||
*/
|
||||
public class ImageRequest extends Request<Bitmap> {
|
||||
/** Socket timeout in milliseconds for image requests */
|
||||
private static final int IMAGE_TIMEOUT_MS = 1000;
|
||||
|
||||
/** Default number of retries for image requests */
|
||||
private static final int IMAGE_MAX_RETRIES = 2;
|
||||
|
||||
/** Default backoff multiplier for image requests */
|
||||
private static final float IMAGE_BACKOFF_MULT = 2f;
|
||||
|
||||
private final Response.Listener<Bitmap> mListener;
|
||||
private final Config mDecodeConfig;
|
||||
private final int mMaxWidth;
|
||||
private final int mMaxHeight;
|
||||
|
||||
/** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
|
||||
private static final Object sDecodeLock = new Object();
|
||||
|
||||
/**
|
||||
* Creates a new image request, decoding to a maximum specified width and
|
||||
* height. If both width and height are zero, the image will be decoded to
|
||||
* its natural size. If one of the two is nonzero, that dimension will be
|
||||
* clamped and the other one will be set to preserve the image's aspect
|
||||
* ratio. If both width and height are nonzero, the image will be decoded to
|
||||
* be fit in the rectangle of dimensions width x height while keeping its
|
||||
* aspect ratio.
|
||||
*
|
||||
* @param url URL of the image
|
||||
* @param listener Listener to receive the decoded bitmap
|
||||
* @param maxWidth Maximum width to decode this bitmap to, or zero for none
|
||||
* @param maxHeight Maximum height to decode this bitmap to, or zero for
|
||||
* none
|
||||
* @param decodeConfig Format to decode the bitmap to
|
||||
* @param errorListener Error listener, or null to ignore errors
|
||||
*/
|
||||
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
|
||||
Config decodeConfig, Response.ErrorListener errorListener) {
|
||||
super(Method.GET, url, errorListener);
|
||||
setRetryPolicy(
|
||||
new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
|
||||
mListener = listener;
|
||||
mDecodeConfig = decodeConfig;
|
||||
mMaxWidth = maxWidth;
|
||||
mMaxHeight = maxHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Priority getPriority() {
|
||||
return Priority.LOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales one side of a rectangle to fit aspect ratio.
|
||||
*
|
||||
* @param maxPrimary Maximum size of the primary dimension (i.e. width for
|
||||
* max width), or zero to maintain aspect ratio with secondary
|
||||
* dimension
|
||||
* @param maxSecondary Maximum size of the secondary dimension, or zero to
|
||||
* maintain aspect ratio with primary dimension
|
||||
* @param actualPrimary Actual size of the primary dimension
|
||||
* @param actualSecondary Actual size of the secondary dimension
|
||||
*/
|
||||
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
|
||||
int actualSecondary) {
|
||||
// If no dominant value at all, just return the actual.
|
||||
if (maxPrimary == 0 && maxSecondary == 0) {
|
||||
return actualPrimary;
|
||||
}
|
||||
|
||||
// If primary is unspecified, scale primary to match secondary's scaling ratio.
|
||||
if (maxPrimary == 0) {
|
||||
double ratio = (double) maxSecondary / (double) actualSecondary;
|
||||
return (int) (actualPrimary * ratio);
|
||||
}
|
||||
|
||||
if (maxSecondary == 0) {
|
||||
return maxPrimary;
|
||||
}
|
||||
|
||||
double ratio = (double) actualSecondary / (double) actualPrimary;
|
||||
int resized = maxPrimary;
|
||||
if (resized * ratio > maxSecondary) {
|
||||
resized = (int) (maxSecondary / ratio);
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
|
||||
// Serialize all decode on a global lock to reduce concurrent heap usage.
|
||||
synchronized (sDecodeLock) {
|
||||
try {
|
||||
return doParse(response);
|
||||
} catch (OutOfMemoryError e) {
|
||||
VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
|
||||
return Response.error(new ParseError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The real guts of parseNetworkResponse. Broken out for readability.
|
||||
*/
|
||||
private Response<Bitmap> doParse(NetworkResponse response) {
|
||||
byte[] data = response.data;
|
||||
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
|
||||
Bitmap bitmap = null;
|
||||
if (mMaxWidth == 0 && mMaxHeight == 0) {
|
||||
decodeOptions.inPreferredConfig = mDecodeConfig;
|
||||
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
|
||||
} else {
|
||||
// If we have to resize this image, first get the natural bounds.
|
||||
decodeOptions.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
|
||||
int actualWidth = decodeOptions.outWidth;
|
||||
int actualHeight = decodeOptions.outHeight;
|
||||
|
||||
// Then compute the dimensions we would ideally like to decode to.
|
||||
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
|
||||
actualWidth, actualHeight);
|
||||
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
|
||||
actualHeight, actualWidth);
|
||||
|
||||
// Decode to the nearest power of two scaling factor.
|
||||
decodeOptions.inJustDecodeBounds = false;
|
||||
// TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
|
||||
// decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
|
||||
decodeOptions.inSampleSize =
|
||||
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
|
||||
Bitmap tempBitmap =
|
||||
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
|
||||
|
||||
// If necessary, scale down to the maximal acceptable size.
|
||||
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
|
||||
tempBitmap.getHeight() > desiredHeight)) {
|
||||
bitmap = Bitmap.createScaledBitmap(tempBitmap,
|
||||
desiredWidth, desiredHeight, true);
|
||||
tempBitmap.recycle();
|
||||
} else {
|
||||
bitmap = tempBitmap;
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
return Response.error(new ParseError(response));
|
||||
} else {
|
||||
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deliverResponse(Bitmap response) {
|
||||
mListener.onResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the largest power-of-two divisor for use in downscaling a bitmap
|
||||
* that will not result in the scaling past the desired dimensions.
|
||||
*
|
||||
* @param actualWidth Actual width of the bitmap
|
||||
* @param actualHeight Actual height of the bitmap
|
||||
* @param desiredWidth Desired width of the bitmap
|
||||
* @param desiredHeight Desired height of the bitmap
|
||||
*/
|
||||
// Visible for testing.
|
||||
static int findBestSampleSize(
|
||||
int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
|
||||
double wr = (double) actualWidth / desiredWidth;
|
||||
double hr = (double) actualHeight / desiredHeight;
|
||||
double ratio = Math.min(wr, hr);
|
||||
float n = 1.0f;
|
||||
while ((n * 2) <= ratio) {
|
||||
n *= 2;
|
||||
}
|
||||
|
||||
return (int) n;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.ParseError;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* A request for retrieving a {@link JSONArray} response body at a given URL.
|
||||
*/
|
||||
public class JsonArrayRequest extends JsonRequest<JSONArray> {
|
||||
|
||||
/**
|
||||
* Creates a new request.
|
||||
* @param url URL to fetch the JSON from
|
||||
* @param listener Listener to receive the JSON response
|
||||
* @param errorListener Error listener, or null to ignore errors.
|
||||
*/
|
||||
public JsonArrayRequest(String url, Listener<JSONArray> listener, ErrorListener errorListener) {
|
||||
super(Method.GET, url, null, listener, errorListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) {
|
||||
try {
|
||||
String jsonString =
|
||||
new String(response.data, HttpHeaderParser.parseCharset(response.headers));
|
||||
return Response.success(new JSONArray(jsonString),
|
||||
HttpHeaderParser.parseCacheHeaders(response));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return Response.error(new ParseError(e));
|
||||
} catch (JSONException je) {
|
||||
return Response.error(new ParseError(je));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.ParseError;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
|
||||
* optional {@link JSONObject} to be passed in as part of the request body.
|
||||
*/
|
||||
public class JsonObjectRequest extends JsonRequest<JSONObject> {
|
||||
|
||||
/**
|
||||
* Creates a new request.
|
||||
* @param method the HTTP method to use
|
||||
* @param url URL to fetch the JSON from
|
||||
* @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and
|
||||
* indicates no parameters will be posted along with request.
|
||||
* @param listener Listener to receive the JSON response
|
||||
* @param errorListener Error listener, or null to ignore errors.
|
||||
*/
|
||||
public JsonObjectRequest(int method, String url, JSONObject jsonRequest,
|
||||
Listener<JSONObject> listener, ErrorListener errorListener) {
|
||||
super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener,
|
||||
errorListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is
|
||||
* <code>null</code>, <code>POST</code> otherwise.
|
||||
*
|
||||
* @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)
|
||||
*/
|
||||
public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener,
|
||||
ErrorListener errorListener) {
|
||||
this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest,
|
||||
listener, errorListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
|
||||
try {
|
||||
String jsonString =
|
||||
new String(response.data, HttpHeaderParser.parseCharset(response.headers));
|
||||
return Response.success(new JSONObject(jsonString),
|
||||
HttpHeaderParser.parseCacheHeaders(response));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return Response.error(new ParseError(e));
|
||||
} catch (JSONException je) {
|
||||
return Response.error(new ParseError(je));
|
||||
}
|
||||
}
|
||||
}
|
||||
102
app/src/main/java/com/android/volley/toolbox/JsonRequest.java
Normal file
102
app/src/main/java/com/android/volley/toolbox/JsonRequest.java
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
import com.android.volley.VolleyLog;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* A request for retrieving a T type response body at a given URL that also
|
||||
* optionally sends along a JSON body in the request specified.
|
||||
*
|
||||
* @param <T> JSON type of response expected
|
||||
*/
|
||||
public abstract class JsonRequest<T> extends Request<T> {
|
||||
/** Charset for request. */
|
||||
private static final String PROTOCOL_CHARSET = "utf-8";
|
||||
|
||||
/** Content type for request. */
|
||||
private static final String PROTOCOL_CONTENT_TYPE =
|
||||
String.format("application/json; charset=%s", PROTOCOL_CHARSET);
|
||||
|
||||
private final Listener<T> mListener;
|
||||
private final String mRequestBody;
|
||||
|
||||
/**
|
||||
* Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
|
||||
* or {@link #getPostParams()} is overridden (which defaults to POST).
|
||||
*
|
||||
* @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
|
||||
*/
|
||||
public JsonRequest(String url, String requestBody, Listener<T> listener,
|
||||
ErrorListener errorListener) {
|
||||
this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
|
||||
}
|
||||
|
||||
public JsonRequest(int method, String url, String requestBody, Listener<T> listener,
|
||||
ErrorListener errorListener) {
|
||||
super(method, url, errorListener);
|
||||
mListener = listener;
|
||||
mRequestBody = requestBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deliverResponse(T response) {
|
||||
mListener.onResponse(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getBodyContentType()}.
|
||||
*/
|
||||
@Override
|
||||
public String getPostBodyContentType() {
|
||||
return getBodyContentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getBody()}.
|
||||
*/
|
||||
@Override
|
||||
public byte[] getPostBody() {
|
||||
return getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBodyContentType() {
|
||||
return PROTOCOL_CONTENT_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBody() {
|
||||
try {
|
||||
return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
|
||||
mRequestBody, PROTOCOL_CHARSET);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.ImageLoader.ImageContainer;
|
||||
import com.android.volley.toolbox.ImageLoader.ImageListener;
|
||||
|
||||
/**
|
||||
* Handles fetching an image from a URL as well as the life-cycle of the
|
||||
* associated request.
|
||||
*/
|
||||
public class NetworkImageView extends ImageView {
|
||||
/** The URL of the network image to load */
|
||||
private String mUrl;
|
||||
|
||||
/**
|
||||
* Resource ID of the image to be used as a placeholder until the network image is loaded.
|
||||
*/
|
||||
private int mDefaultImageId;
|
||||
|
||||
/**
|
||||
* Resource ID of the image to be used if the network response fails.
|
||||
*/
|
||||
private int mErrorImageId;
|
||||
|
||||
/** Local copy of the ImageLoader. */
|
||||
private ImageLoader mImageLoader;
|
||||
|
||||
/** Current ImageContainer. (either in-flight or finished) */
|
||||
private ImageContainer mImageContainer;
|
||||
|
||||
public NetworkImageView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public NetworkImageView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets URL of the image that should be loaded into this view. Note that calling this will
|
||||
* immediately either set the cached image (if available) or the default image specified by
|
||||
* {@link NetworkImageView#setDefaultImageResId(int)} on the view.
|
||||
*
|
||||
* NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and
|
||||
* {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling
|
||||
* this function.
|
||||
*
|
||||
* @param url The URL that should be loaded into this ImageView.
|
||||
* @param imageLoader ImageLoader that will be used to make the request.
|
||||
*/
|
||||
public void setImageUrl(String url, ImageLoader imageLoader) {
|
||||
mUrl = url;
|
||||
mImageLoader = imageLoader;
|
||||
// The URL has potentially changed. See if we need to load it.
|
||||
loadImageIfNecessary(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default image resource ID to be used for this view until the attempt to load it
|
||||
* completes.
|
||||
*/
|
||||
public void setDefaultImageResId(int defaultImage) {
|
||||
mDefaultImageId = defaultImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the error image resource ID to be used for this view in the event that the image
|
||||
* requested fails to load.
|
||||
*/
|
||||
public void setErrorImageResId(int errorImage) {
|
||||
mErrorImageId = errorImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the image for the view if it isn't already loaded.
|
||||
* @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
|
||||
*/
|
||||
private void loadImageIfNecessary(final boolean isInLayoutPass) {
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
boolean isFullyWrapContent = getLayoutParams() != null
|
||||
&& getLayoutParams().height == LayoutParams.WRAP_CONTENT
|
||||
&& getLayoutParams().width == LayoutParams.WRAP_CONTENT;
|
||||
// if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
|
||||
// view, hold off on loading the image.
|
||||
if (width == 0 && height == 0 && !isFullyWrapContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
|
||||
// currently loaded image.
|
||||
if (TextUtils.isEmpty(mUrl)) {
|
||||
if (mImageContainer != null) {
|
||||
mImageContainer.cancelRequest();
|
||||
mImageContainer = null;
|
||||
}
|
||||
setImageBitmap(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// if there was an old request in this view, check if it needs to be canceled.
|
||||
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
|
||||
if (mImageContainer.getRequestUrl().equals(mUrl)) {
|
||||
// if the request is from the same URL, return.
|
||||
return;
|
||||
} else {
|
||||
// if there is a pre-existing request, cancel it if it's fetching a different URL.
|
||||
mImageContainer.cancelRequest();
|
||||
setImageBitmap(null);
|
||||
}
|
||||
}
|
||||
|
||||
// The pre-existing content of this view didn't match the current URL. Load the new image
|
||||
// from the network.
|
||||
ImageContainer newContainer = mImageLoader.get(mUrl,
|
||||
new ImageListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
if (mErrorImageId != 0) {
|
||||
setImageResource(mErrorImageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(final ImageContainer response, boolean isImmediate) {
|
||||
// If this was an immediate response that was delivered inside of a layout
|
||||
// pass do not set the image immediately as it will trigger a requestLayout
|
||||
// inside of a layout. Instead, defer setting the image by posting back to
|
||||
// the main thread.
|
||||
if (isImmediate && isInLayoutPass) {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onResponse(response, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.getBitmap() != null) {
|
||||
setImageBitmap(response.getBitmap());
|
||||
} else if (mDefaultImageId != 0) {
|
||||
setImageResource(mDefaultImageId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update the ImageContainer to be the new bitmap container.
|
||||
mImageContainer = newContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
loadImageIfNecessary(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
if (mImageContainer != null) {
|
||||
// If the view was bound to an image request, cancel it and clear
|
||||
// out the image from the view.
|
||||
mImageContainer.cancelRequest();
|
||||
setImageBitmap(null);
|
||||
// also clear out the container so we can reload the image if necessary.
|
||||
mImageContainer = null;
|
||||
}
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawableStateChanged() {
|
||||
super.drawableStateChanged();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/com/android/volley/toolbox/NoCache.java
Normal file
49
app/src/main/java/com/android/volley/toolbox/NoCache.java
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
|
||||
/**
|
||||
* A cache that doesn't.
|
||||
*/
|
||||
public class NoCache implements Cache {
|
||||
@Override
|
||||
public void clear() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry get(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String key, Entry entry) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(String key, boolean fullExpire) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String key) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A variation of {@link ByteArrayOutputStream} that uses a pool of byte[] buffers instead
|
||||
* of always allocating them fresh, saving on heap churn.
|
||||
*/
|
||||
public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
|
||||
/**
|
||||
* If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is
|
||||
* the default size to which the underlying byte array is initialized.
|
||||
*/
|
||||
private static final int DEFAULT_SIZE = 256;
|
||||
|
||||
private final ByteArrayPool mPool;
|
||||
|
||||
/**
|
||||
* Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written
|
||||
* to this instance, the underlying byte array will expand.
|
||||
*/
|
||||
public PoolingByteArrayOutputStream(ByteArrayPool pool) {
|
||||
this(pool, DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
|
||||
* more than {@code size} bytes are written to this instance, the underlying byte array will
|
||||
* expand.
|
||||
*
|
||||
* @param size initial size for the underlying byte array. The value will be pinned to a default
|
||||
* minimum size.
|
||||
*/
|
||||
public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
|
||||
mPool = pool;
|
||||
buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mPool.returnBuf(buf);
|
||||
buf = null;
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalize() {
|
||||
mPool.returnBuf(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures there is enough space in the buffer for the given number of additional bytes.
|
||||
*/
|
||||
private void expand(int i) {
|
||||
/* Can the buffer handle @i more bytes, if not expand it */
|
||||
if (count + i <= buf.length) {
|
||||
return;
|
||||
}
|
||||
byte[] newbuf = mPool.getBuf((count + i) * 2);
|
||||
System.arraycopy(buf, 0, newbuf, 0, count);
|
||||
mPool.returnBuf(buf);
|
||||
buf = newbuf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] buffer, int offset, int len) {
|
||||
expand(len);
|
||||
super.write(buffer, offset, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int oneByte) {
|
||||
expand(1);
|
||||
super.write(oneByte);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user