Commit bb8e3cd0 by 宁斌

新建雲端GSA項目

parents
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
.gradle
/gradle/
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
*/build
*/build
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.externalNativeBuild
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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 *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.billy.android.android_internal" />
package android.app;
/**
* 模拟系统源码,供CC中调用
* @author billy.qi
* @since 18/8/13 13:58
*/
public class ActivityThread {
public static Application currentApplication() {
return null;
}
}
package android.app;
/**
* 模拟系统源码,供CC中调用
* @author billy.qi
* @since 18/8/13 13:57
*/
public class AppGlobals {
public static Application getInitialApplication() {
return null;
}
}
ext.mainApp = true //设置为true,表示此module为主app module,一直以application方式编译
apply from: rootProject.file('cc-settings.gradle')
//获取时间戳
def getDate() {
def date = new Date()
def formattedDate = date.format('yyyyMMddHH')
return formattedDate
}
android {
signingConfigs {
GSAndroidNew {
keyAlias 'keycypos'
keyPassword 'qingkeke'
storeFile file('cyposandroid.keyset')
storePassword 'qingkeke'
}
}
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
// 避免 lint 检测出错时停止构建
lintOptions {
abortOnError false
}
defaultConfig {
applicationId "com.gingersoft.cloud.gsa"
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
multiDexEnabled true
}
buildTypes {
release {
postprocessing {
removeUnusedCode false
removeUnusedResources false
obfuscate false
optimizeCode false
proguardFiles 'proguard.cfg'
signingConfig signingConfigs.GSAndroidNew
}
}
}
//修改生成的apk名字
applicationVariants.all { variant ->
variant.outputs.all {
def fileName
def date = new Date()
def formattedDate = date.format('yyyy-MM-dd')
if (variant.buildType.name.equals('release')) {
fileName = "${formattedDate}_GsAndroid_${variant.mergedFlavor.versionName}_正式版_.apk"
} else if (variant.buildType.name.equals('Test')) {
fileName = "${formattedDate}__GsAndroid_${variant.mergedFlavor.versionName}_测试版_.apk"
} else if (variant.buildType.name.equals('debug')) {
fileName = "${formattedDate}__GsAndroid_${variant.mergedFlavor.versionName}_debug_.apk"
}
outputFileName = fileName;
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':mvp:arms')
addComponent 'component_login'
addComponent 'component_register'
// test
testImplementation rootProject.ext.dependencies["junit"]
debugImplementation rootProject.ext.dependencies["canary-debug"]
releaseImplementation rootProject.ext.dependencies["canary-release"]
testImplementation rootProject.ext.dependencies["canary-release"]
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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 *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
package com.gingersoft.cloud.gsa;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.gingersoft.cloud.gsa", appContext.getPackageName());
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gingersoft.cloud.gsa">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".main.ActivityA">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
package com.gingersoft.cloud.gsa.main;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import com.gingersoft.cloud.gsa.R;
/**
* @author billy.qi
*/
public class ActivityA extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = new TextView(this);
textView.setGravity(Gravity.CENTER);
textView.setText("ActivityA\nClick to finish this activity");
setContentView(textView);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}
package com.gingersoft.cloud.gsa.main;
import android.app.Application;
import com.billy.cc.core.component.CC;
/**
* @author billy.qi
* @since 17/11/20 19:28
*/
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
CC.enableVerboseLog(true);
CC.enableDebug(true);
CC.enableRemoteCC(true);
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M6.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/login_user_state_observer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
/>
<TextView
android:id="@+id/login_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
>
</ScrollView>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:background="#cccccc"
android:scrollbars="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/console"
android:padding="10dp"
/>
</ScrollView>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/login_user_state_observer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
/>
<TextView
android:id="@+id/login_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
>
</ScrollView>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:background="#cccccc"
android:scrollbars="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/console"
android:padding="10dp"
/>
</ScrollView>
</LinearLayout>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.gingersoft.cloud.gsa.MainActivity">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>
<resources>
<string name="app_name">GSA-Cloud</string>
<string name="action_settings">Settings</string>
</resources>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
package com.gingersoft.cloud.gsa;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
\ No newline at end of file
Properties properties = new Properties()
try {
properties.load(project.rootProject.file('local.properties').newDataInputStream())
} catch(Exception ignored) {
return
}
if (!properties.getProperty("is_repo_owner")) {
return
}
if (project.hasProperty("android")) { // Android libraries
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
} else { // Java libraries
task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
from sourceSets.main.allSource
}
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
project.tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
options.addStringOption('encoding', 'UTF-8')
}
// Bintray
apply plugin: 'com.novoda.bintray-release'
publish {
repoName = 'android'
userOrg = properties.getProperty("bintray.userOrg")
groupId = publishedGroupId
artifactId = artifact
publishVersion = libraryVersion
desc = libraryDescription
website = siteUrl
}
apply plugin: 'maven'
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('../repo-local')) //deploy到本地仓库
pom.groupId = publishedGroupId
pom.artifactId = artifact
pom.version = libraryVersion + '-SNAPSHOT'
}
}
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle"
buildscript {
ext.kotlin_version = '1.2.51'
repositories {
maven{ url rootProject.file("repo-local") }
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.novoda:bintray-release:0.9.2'
classpath 'com.billy.android:cc-register:1.1.2'
}
}
allprojects {
repositories {
maven{ url rootProject.file("repo-local") }
maven { url "https://jitpack.io" }
google()
jcenter()
mavenLocal()
mavenCentral()
maven { url "https://jitpack.io" }
maven { url "https://maven.google.com" }//Support-library 需要 Google 仓库
maven { url 'https://dl.bintray.com/poldz123/maven/' }
maven { url 'http://www.idescout.com/maven/repo/' }
//阿里云仓库
maven { url "http://maven.aliyun.com/nexus/content/repositories/releases" }
maven{ url 'https://maven.aliyun.com/repository/google' }
maven{ url 'https://maven.aliyun.com/repository/jcenter' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile localGroovy()
}
repositories {
mavenCentral()
}
dependencies {
compile 'com.android.tools.build:gradle:2.2.0'
}
ext {
bintrayRepo = 'android'
bintrayName = 'cc-register'
publishedGroupId = 'com.billy.android'
libraryName = 'CcRegister'
artifact = 'cc-register'
libraryDescription = 'android component caller'
siteUrl = 'https://github.com/luckybilly/CC'
gitUrl = 'https://github.com/luckybilly/CC.git'
libraryVersion = '1.1.2'
developerId = 'billy'
developerName = 'billy'
developerEmail = 'okkanan@hotmail.com'
licenseName = 'The Apache Software License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
allLicenses = ["Apache-2.0"]
}
apply from: rootProject.file('bintray.gradle')
\ No newline at end of file
package com.billy.android.register
import com.android.builder.model.AndroidProject
import com.google.gson.Gson
import org.gradle.api.Project
import java.lang.reflect.Type
/**
* 文件操作辅助类
* @author zhangkb
* @since 2018/04/13
*/
class RegisterCache {
final static def CACHE_INFO_DIR = "cc-register"
/**
* 缓存自动注册配置的文件
* @param project
* @return file
*/
static File getRegisterInfoCacheFile(Project project) {
return getCacheFile(project, "register-info.json")
}
/**
* 缓存扫描到结果的文件
* @param project
* @return File
*/
static File getRegisterCacheFile(Project project) {
return getCacheFile(project, "register-cache.json")
}
static File getBuildTypeCacheFile(Project project) {
return getCacheFile(project, "build-type.json")
}
private static File getCacheFile(Project project, String fileName) {
String baseDir = getCacheFileDir(project)
if (mkdirs(baseDir)) {
return new File(baseDir + fileName)
} else {
throw new FileNotFoundException("Not found path:" + baseDir)
}
}
static boolean isSameAsLastBuildType(Project project, boolean isApp) {
File cacheFile = getCacheFile(project, "build-type.json")
if (cacheFile.exists()) {
return (cacheFile.text == 'true') == isApp
}
return false
}
static void cacheBuildType(Project project, boolean isApp) {
File cacheFile = getCacheFile(project, "build-type.json")
cacheFile.getParentFile().mkdirs()
if (!cacheFile.exists())
cacheFile.createNewFile()
cacheFile.write(isApp.toString())
}
/**
* 将扫描到的结果缓存起来
* @param cacheFile
* @param harvests
*/
static void cacheRegisterHarvest(File cacheFile, String harvests) {
if (!cacheFile || !harvests)
return
cacheFile.getParentFile().mkdirs()
if (!cacheFile.exists())
cacheFile.createNewFile()
cacheFile.write(harvests)
}
private static String getCacheFileDir(Project project) {
return project.getBuildDir().absolutePath + File.separator + AndroidProject.FD_INTERMEDIATES + File.separator + CACHE_INFO_DIR + File.separator
}
/**
* 读取文件内容并创建Map
* @param file 缓存文件
* @param type map的类型
* @return
*/
static Map readToMap(File file, Type type) {
Map map = null
if (file.exists()) {
if (type) {
def text = file.text
if (text) {
try {
map = new Gson().fromJson(text, type)
} catch (Exception e) {
e.printStackTrace()
}
}
}
}
if (map == null) {
map = new HashMap()
}
return map
}
/**
* 创建文件夹
* @param dirPath
* @return boolean
*/
static boolean mkdirs(String dirPath) {
def baseDirFile = new File(dirPath)
def isSuccess = true
if (!baseDirFile.isDirectory()) {
isSuccess = baseDirFile.mkdirs()
}
return isSuccess
}
}
\ No newline at end of file
package com.billy.android.register
import org.gradle.api.Project
/**
* aop的配置信息
* @author billy.qi
* @since 17/3/28 11:48
*/
class RegisterExtension {
static final String PLUGIN_NAME = RegisterPlugin.PLUGIN_NAME
public ArrayList<Map<String, Object>> registerInfo = []
ArrayList<RegisterInfo> list = new ArrayList<>()
Project project
def cacheEnabled = true
def multiProcessEnabled = false
ArrayList<String> excludeProcessNames = []
RegisterExtension() {}
void convertConfig() {
registerInfo.each { map ->
RegisterInfo info = new RegisterInfo()
info.interfaceName = map.get('scanInterface')
def superClasses = map.get('scanSuperClasses')
if (!superClasses) {
superClasses = new ArrayList<String>()
} else if (superClasses instanceof String) {
ArrayList<String> superList = new ArrayList<>()
superList.add(superClasses)
superClasses = superList
}
info.superClassNames = superClasses
info.initClassName = map.get('codeInsertToClassName') //代码注入的类
info.initMethodName = map.get('codeInsertToMethodName') //代码注入的方法(默认为static块)
info.registerMethodName = map.get('registerMethodName') //生成的代码所调用的方法
info.registerClassName = map.get('registerClassName') //注册方法所在的类
info.paramType = map.get('paramType') //注册方法参数类型:'object':参数为对象,'class':参数为Class类型
info.include = map.get('include')
info.exclude = map.get('exclude')
info.init()
if (info.validate())
list.add(info)
else {
project.logger.error(PLUGIN_NAME + ' extension error: scanInterface, codeInsertToClassName and registerMethodName should not be null\n' + info.toString())
}
}
if (cacheEnabled) {
checkRegisterInfo()
} else {
deleteFile(RegisterCache.getRegisterInfoCacheFile(project))
deleteFile(RegisterCache.getRegisterCacheFile(project))
}
}
private void checkRegisterInfo() {
def registerInfo = RegisterCache.getRegisterInfoCacheFile(project)
def listInfo = list.toString()
def sameInfo = false
if (!registerInfo.exists()) {
registerInfo.createNewFile()
} else if(registerInfo.canRead()) {
def info = registerInfo.text
sameInfo = info == listInfo
if (!sameInfo) {
project.logger.error("${PLUGIN_NAME} registerInfo has been changed since project(':$project.name') last build")
}
} else {
project.logger.error("${PLUGIN_NAME} read registerInfo error--------")
}
if (!sameInfo) {
deleteFile(RegisterCache.getRegisterCacheFile(project))
}
if (registerInfo.canWrite()) {
registerInfo.write(listInfo)
} else {
project.logger.error("${PLUGIN_NAME} write registerInfo error--------")
}
}
private void deleteFile(File file) {
if (file.exists()) {
//registerInfo 配置有改动就删除緩存文件
file.delete()
}
}
void reset() {
list.each { info ->
info.reset()
}
}
@Override
String toString() {
StringBuilder sb = new StringBuilder(RegisterPlugin.EXT_NAME).append(' = {')
.append('\n cacheEnabled = ').append(cacheEnabled)
.append('\n registerInfo = [\n')
def size = list.size()
for (int i = 0; i < size; i++) {
sb.append('\t' + list.get(i).toString().replaceAll('\n', '\n\t'))
if (i < size - 1)
sb.append(',\n')
}
sb.append('\n ]\n}')
return sb.toString()
}
}
\ No newline at end of file
package com.billy.android.register
import java.util.regex.Pattern
/**
* aop的配置信息
* @author billy.qi
* @since 17/3/28 11:48
*/
class RegisterInfo {
public static final String PARAM_TYPE_OBJECT = 'object'
public static final String PARAM_TYPE_CLASS = 'class'
public static final String PARAM_TYPE_CLASS_NAME = 'string'
static final DEFAULT_EXCLUDE = [
'.*/R(\\$[^/]*)?'
, '.*/BuildConfig$'
]
//以下是可配置参数
String interfaceName = ''
ArrayList<String> superClassNames = []
String initClassName = ''
String initMethodName = ''
String registerClassName = ''
String registerMethodName = ''
String paramType = '' //注册方法参数类型:'object':(默认值)参数为对象,'class':参数为Class类型
ArrayList<String> include = []
ArrayList<String> exclude = []
//以下不是可配置参数
ArrayList<Pattern> includePatterns = []
ArrayList<Pattern> excludePatterns = []
File fileContainsInitClass //initClassName的class文件或含有initClassName类的jar文件
Set<String> classList = new HashSet<>()
RegisterInfo(){}
void reset() {
fileContainsInitClass = null
classList.clear()
}
boolean validate() {
return interfaceName && registerClassName && registerMethodName
}
//用于在console中输出日志
@Override
String toString() {
StringBuilder sb = new StringBuilder('{')
sb.append('\n\t').append('scanInterface').append('\t\t\t=\t').append(interfaceName)
sb.append('\n\t').append('scanSuperClasses').append('\t\t=\t[')
for (int i = 0; i < superClassNames.size(); i++) {
if (i > 0) sb.append(',')
sb.append(' \'').append(superClassNames.get(i)).append('\'')
}
sb.append(' ]')
sb.append('\n\t').append('codeInsertToClassName').append('\t=\t').append(initClassName)
sb.append('\n\t').append('codeInsertToMethodName').append('\t=\t').append(initMethodName)
sb.append('\n\t').append('registerMethodName').append('\t\t=\tpublic static void ')
.append(registerClassName).append('.').append(registerMethodName)
sb.append('\n\t').append('paramType').append('\t\t\t=\t\'').append(paramType).append('\'')
sb.append('\n\t').append('include').append(' = [')
include.each { i ->
sb.append('\n\t\t\'').append(i).append('\'')
}
sb.append('\n\t]')
sb.append('\n\t').append('exclude').append(' = [')
exclude.each { i ->
sb.append('\n\t\t\'').append(i).append('\'')
}
sb.append('\n\t]\n}')
return sb.toString()
}
void init() {
if (include == null) include = new ArrayList<>()
if (include.empty) include.add(".*") //如果没有设置则默认为include所有
if (exclude == null) exclude = new ArrayList<>()
if (!registerClassName)
registerClassName = initClassName
//将interfaceName中的'.'转换为'/'
if (interfaceName)
interfaceName = convertDotToSlash(interfaceName)
//将superClassName中的'.'转换为'/'
if (superClassNames == null) superClassNames = new ArrayList<>()
for (int i = 0; i < superClassNames.size(); i++) {
def superClass = convertDotToSlash(superClassNames.get(i))
superClassNames.set(i, superClass)
}
//注册和初始化的方法所在的类默认为同一个类
initClassName = convertDotToSlash(initClassName)
//默认插入到static块中
if (!initMethodName)
initMethodName = "<clinit>"
registerClassName = convertDotToSlash(registerClassName)
if (!paramType)
paramType = PARAM_TYPE_OBJECT
//添加默认的排除项
DEFAULT_EXCLUDE.each { e ->
if (!exclude.contains(e))
exclude.add(e)
}
initPattern(include, includePatterns)
initPattern(exclude, excludePatterns)
}
static String convertDotToSlash(String str) {
return str ? str.replaceAll('\\.', '/').intern() : str
}
private static void initPattern(ArrayList<String> list, ArrayList<Pattern> patterns) {
list.each { s ->
patterns.add(Pattern.compile(s))
}
}
}
\ No newline at end of file
package com.billy.android.register
import com.android.build.gradle.AppExtension
import com.billy.android.register.cc.DefaultRegistryHelper
import com.billy.android.register.cc.ProjectModuleManager
import com.billy.android.register.cc.generator.ManifestGenerator
import org.apache.commons.io.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* 自动注册插件入口
* @author billy.qi
* @since 17/3/14 17:35
*/
public class RegisterPlugin implements Plugin<Project> {
public static final String PLUGIN_NAME = 'cc-register'
public static final String EXT_NAME = 'ccregister'
@Override
public void apply(Project project) {
println "project(${project.name}) apply ${PLUGIN_NAME} plugin"
project.extensions.create(EXT_NAME, RegisterExtension)
def isApp = ProjectModuleManager.manageModule(project)
performBuildTypeCache(project, isApp)
if (isApp) {
println "project(${project.name}) register ${PLUGIN_NAME} transform"
def android = project.extensions.getByType(AppExtension)
def transformImpl = new RegisterTransform(project)
android.registerTransform(transformImpl)
project.afterEvaluate {
RegisterExtension config = init(project, transformImpl)//此处要先于transformImpl.transform方法执行
if (config.multiProcessEnabled) {
ManifestGenerator.generateManifestFileContent(project, config.excludeProcessNames)
}
}
}
}
private static void performBuildTypeCache(Project project, boolean isApp) {
if (!RegisterCache.isSameAsLastBuildType(project, isApp)) {
RegisterCache.cacheBuildType(project, isApp)
//兼容gradle3.0以上组件独立运行时出现的问题:https://github.com/luckybilly/CC/issues/62
//切换app/lib编译时,将transform目录清除
def cachedJniFile = project.file("build/intermediates/transforms/")
if (cachedJniFile && cachedJniFile.exists() && cachedJniFile.isDirectory()) {
FileUtils.deleteDirectory(cachedJniFile)
}
}
}
static RegisterExtension init(Project project, RegisterTransform transformImpl) {
RegisterExtension extension = project.extensions.findByName(EXT_NAME) as RegisterExtension
extension.project = project
extension.convertConfig()
DefaultRegistryHelper.addDefaultRegistry(extension.list)
transformImpl.extension = extension
return extension
}
}
package com.billy.android.register
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.builder.model.Version
import com.billy.android.register.cc.generator.ManifestGenerator
import com.billy.android.register.cc.generator.ProviderGenerator
import com.billy.android.register.cc.generator.RegistryCodeGenerator
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
/**
* 自动注册的核心类
* @author billy.qi
* @since 17/3/21 11:48
*/
class RegisterTransform extends Transform {
static final String PLUGIN_NAME = RegisterPlugin.PLUGIN_NAME
Project project
RegisterExtension extension;
def cacheEnabled
def isAllScan = false
Map<String, ScanHarvest> cacheMap = null
RegisterTransform(Project project) {
this.project = project
}
@Override
String getName() {
return PLUGIN_NAME
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 是否支持增量编译
* @return
*/
@Override
boolean isIncremental() {
return true
}
def classFolder = null
@Override
void transform(Context context, Collection<TransformInput> inputs
, Collection<TransformInput> referencedInputs
, TransformOutputProvider outputProvider
, boolean isIncremental) throws IOException, TransformException, InterruptedException {
project.logger.warn("start ${PLUGIN_NAME} transform...")
extension.reset()
project.logger.warn(extension.toString())
def clearCache = !isIncremental
// clean build cache
if (clearCache) {
outputProvider.deleteAll()
}
long time = System.currentTimeMillis()
cacheEnabled = extension.cacheEnabled
int asmApiLevel = CodeScanner.getAsmApiLevel()
println("${PLUGIN_NAME}----------- work environment ----------------\n" )
println(">>>> gradle version: ${project.gradle.gradleVersion}")
println(">>>> gradle plugin version:${Version.ANDROID_GRADLE_PLUGIN_VERSION}")
println(">>>> current ASM level: ASM${(asmApiLevel >> 16)}")
println("\n${PLUGIN_NAME}-----------isIncremental:${isIncremental}--------extension.cacheEnabled:${cacheEnabled}--------------------\n")
File cacheFile = null
Gson gson = null
if (cacheEnabled) { //开启了缓存
gson = new Gson()
cacheFile = RegisterCache.getRegisterCacheFile(project)
if (clearCache && cacheFile.exists())
cacheFile.delete()
cacheMap = RegisterCache.readToMap(cacheFile, new TypeToken<HashMap<String, ScanHarvest>>() {
}.getType())
if (cacheMap.isEmpty()) {
isAllScan = true
}
}
CodeScanner scanProcessor = new CodeScanner(extension.list, cacheMap)
// 遍历输入文件
inputs.each { TransformInput input ->
// 遍历jar
input.jarInputs.each { JarInput jarInput ->
if (jarInput.status != Status.NOTCHANGED && cacheMap) {
cacheMap.remove(jarInput.file.absolutePath)
}
scanJar(jarInput, outputProvider, scanProcessor)
}
// 遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
long dirTime = System.currentTimeMillis()
def root = scanClass(outputProvider, directoryInput, scanProcessor)
long scanTime = System.currentTimeMillis()
println "${PLUGIN_NAME} cost time: ${System.currentTimeMillis() - dirTime}, scan time: ${scanTime - dirTime}. path=${root}"
}
}
if (cacheMap != null && cacheFile && gson) {
def json = gson.toJson(cacheMap)
RegisterCache.cacheRegisterHarvest(cacheFile, json)
}
def scanFinishTime = System.currentTimeMillis()
project.logger.error("${PLUGIN_NAME} scan all class cost time: " + (scanFinishTime - time) + " ms")
extension.list.each { ext ->
if (ext.fileContainsInitClass) {
println('')
println("insert register code to file:" + ext.fileContainsInitClass.absolutePath)
if (ext.classList.isEmpty()) {
project.logger.error("No class implements found for interface:" + ext.interfaceName)
} else {
ext.classList.each {
println(it)
}
RegistryCodeGenerator.insertInitCodeTo(ext)
}
} else {
project.logger.error("The specified register class not found:" + ext.registerClassName)
}
}
project.logger.error("${PLUGIN_NAME} insert code cost time: " + (System.currentTimeMillis() - scanFinishTime) + " ms")
if (extension.multiProcessEnabled && classFolder) {
def processNames = ManifestGenerator.getCachedProcessNames(project.name, context.variantName)
processNames.each { processName ->
if (processName) {
ProviderGenerator.generateProvider(processName, classFolder)
}
}
}
def finishTime = System.currentTimeMillis()
project.logger.error("${PLUGIN_NAME} cost time: " + (finishTime - time) + " ms")
}
static void scanJar(JarInput jarInput, TransformOutputProvider outputProvider, CodeScanner scanProcessor) {
// 获得输入文件
File src = jarInput.file
//遍历jar的字节码类文件,找到需要自动注册的类
File dest = getDestFile(jarInput, outputProvider)
long time = System.currentTimeMillis();
if (!scanProcessor.scanJar(src, dest) //直接读取了缓存,没有执行实际的扫描
//此jar文件中不需要被注入代码
//为了避免增量编译时代码注入重复,被注入代码的jar包每次都重新复制
&& !scanProcessor.isCachedJarContainsInitClass(src.absolutePath)) {
//不需要执行文件复制,直接返回
return
}
println "${PLUGIN_NAME} cost time: " + (System.currentTimeMillis() - time) + " ms to scan jar file:" + dest.absolutePath
//复制jar文件到transform目录:build/transforms/cc-register/
FileUtils.copyFile(src, dest)
}
static File getDestFile(JarInput jarInput, TransformOutputProvider outputProvider) {
def destName = jarInput.name
// 重名名输出文件,因为可能同名,会覆盖
def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length() - 4)
}
// 获得输出文件
File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
return dest
}
def scanClass(TransformOutputProvider outputProvider, DirectoryInput directoryInput, CodeScanner scanProcessor) {
boolean leftSlash = File.separator == '/'
// 获得产物的目录
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
classFolder = dest
String root = directoryInput.file.absolutePath
if (!root.endsWith(File.separator))
root += File.separator
// changedFiles 为空 或者 关闭缓存
if (directoryInput.changedFiles.isEmpty() || !cacheEnabled || isAllScan) {
//遍历目录下的每个文件
directoryInput.file.eachFileRecurse { File file ->
scanClassFile(file, root, leftSlash, scanProcessor, dest)
}
} else {
//移除发生改变的缓存
directoryInput.changedFiles.each { fileList ->
cacheMap.remove(fileList.key.absolutePath)
}
cacheMap.each { cache ->
if (cache.key.endsWith(".class")) {
def path = cache.key.replace(root, '')
scanProcessor.hitCache(new File(cache.key), new File(dest, path))
}
}
//扫描发生改变的文件
directoryInput.changedFiles.each { fileList ->
def file = fileList.key
if (fileList.value == Status.CHANGED || fileList.value == Status.ADDED) {
scanClassFile(file, root, leftSlash, scanProcessor, dest)
}
}
}
// 处理完后拷到目标文件
FileUtils.copyDirectory(directoryInput.file, dest)
return root
}
private static void scanClassFile(File file, String root, boolean leftSlash, CodeScanner scanProcessor, File dest) {
def path = file.absolutePath.replace(root, '')
if (file.isFile()) {
def entryName = path
if (!leftSlash) {
entryName = entryName.replaceAll("\\\\", "/")
}
scanProcessor.checkInitClass(entryName, new File(dest.absolutePath + File.separator + path), file.absolutePath)
if (scanProcessor.shouldProcessClass(entryName)) {
scanProcessor.scanClass(file)
}
}
}
}
\ No newline at end of file
package com.billy.android.register
/**
* 已扫描到接口或者codeInsertToClassName jar的信息
* @author zhangkb
* @since 2018/04/17
*/
class ScanHarvest {
List<Harvest> harvestList = new ArrayList<>()
class Harvest {
String className
String interfaceName
boolean isInitClass
String processName
}
}
\ No newline at end of file
package com.billy.android.register.cc
import com.billy.android.register.RegisterInfo
/**
* CC框架自身默认的注册配置辅助类
*/
class DefaultRegistryHelper {
static void addDefaultRegistry(ArrayList<RegisterInfo> list) {
def exclude = ['com/billy/cc/core/component/.*']
addDefaultRegistryFor(list,
'com.billy.cc.core.component.IComponent',
'com.billy.cc.core.component.ComponentManager',
'registerComponent',
RegisterInfo.PARAM_TYPE_OBJECT,
exclude)
addDefaultRegistryFor(list,
'com.billy.cc.core.component.IGlobalCCInterceptor',
'com.billy.cc.core.component.GlobalCCInterceptorManager',
'registerGlobalInterceptor',
RegisterInfo.PARAM_TYPE_OBJECT,
exclude)
addDefaultRegistryFor(list,
'com.billy.cc.core.component.IParamJsonConverter',
'com.billy.cc.core.component.remote.RemoteParamUtil',
'initRemoteCCParamJsonConverter',
RegisterInfo.PARAM_TYPE_OBJECT,
exclude)
}
static void addDefaultRegistryFor(ArrayList<RegisterInfo> list, String interfaceName,
String codeInsertToClassName, String registerMethodName,
String paramType,
List<String> exclude) {
if (!list.find { it.interfaceName == RegisterInfo.convertDotToSlash(interfaceName) }) {
RegisterInfo info = new RegisterInfo()
info.interfaceName = interfaceName
info.superClassNames = []
info.initClassName = codeInsertToClassName //代码注入的类
info.registerMethodName = registerMethodName //生成的代码所调用的方法
info.paramType = paramType //注册方法的类型
info.exclude = exclude
info.init()
list.add(info)
}
}
}
\ No newline at end of file
package com.billy.android.register.cc.generator
import com.android.build.gradle.AppExtension
import com.android.builder.model.Version
import com.billy.android.register.RegisterPlugin
import groovy.util.slurpersupport.GPathResult
import groovy.xml.MarkupBuilder
import org.gradle.api.Project
import org.gradle.util.GradleVersion
/**
* 生成provider类
*/
class ManifestGenerator {
static final String AUTHORITY = "com.billy.cc.core.remote"
static Map<String, Set<String>> cachedProcessNames = new HashMap<>()
static void cacheProcessNames(String projectName, String variantName, Set<String> processNames) {
cachedProcessNames.put(projectName + "_" + variantName, processNames)
}
static Set<String> getCachedProcessNames(String projectName, String variantName) {
return cachedProcessNames.get(projectName + "_" + variantName)
}
/**
* 为processManifest的task添加自动注入子进程provider的功能
*/
static void generateManifestFileContent(Project project, ArrayList<String> excludeProcessNames) {
def android = project.extensions.getByType(AppExtension)
android.applicationVariants.all { variant ->
String pkgName = [variant.mergedFlavor.applicationId, variant.mergedFlavor.applicationIdSuffix, variant.buildType.applicationIdSuffix].findAll().join()
variant.outputs.each { output ->
def processManifest = null
def gradlePluginAfter_3_3_0 = GradleVersion.version(Version.ANDROID_GRADLE_PLUGIN_VERSION) >= GradleVersion.version('3.3.0')
//fix warning:
// WARNING: API 'variantOutput.getProcessManifest()' is obsolete and has
// been replaced with 'variantOutput.getProcessManifestProvider()'.
// It will be removed at the end of 2019.
if (gradlePluginAfter_3_3_0) {
try {
processManifest = output.processManifestProvider.get()
} catch(Throwable ignored){}
}
if(processManifest == null) {
processManifest = output.processManifest
}
processManifest.doLast {
processManifest.outputs.files.each { File file ->
//在gradle plugin 3.0.0之前,file是文件,且文件名为AndroidManifest.xml
//在gradle plugin 3.0.0之后,file是目录,AndroidManifest.xml文件在此目录下
def manifestFile = null
if (file.name =="AndroidManifest.xml") {
manifestFile = file
} else if (file.isDirectory()) {
manifestFile = new File(file, "AndroidManifest.xml")
}
if (manifestFile && manifestFile.exists()) {
println "${RegisterPlugin.PLUGIN_NAME} regist provider into:${manifestFile.absolutePath}"
def manifest = new XmlSlurper().parse(manifestFile)
if (!pkgName) pkgName = manifest.'@package'
HashSet<String> processNames = getAllManifestedProcessNames(manifest)
processNames.removeAll(excludeProcessNames)
if (!processNames.empty) {
writeProvidersIntoManifestFile(pkgName, manifestFile, processNames)
}
//将processManifestTask执行后扫描出的子进程名称缓存起来给transform使用
cacheProcessNames(project.name, variant.name, processNames)
}
}
}
}
}
}
/**
* 分析merge后的AndroidManifest.xml文件中的四大组件,收集所有子进程名称
*/
private static HashSet<String> getAllManifestedProcessNames(GPathResult manifest) {
Set<String> processNames = new HashSet<>()
manifest.application.activity.each {
addSubProcess(processNames, it)
}
manifest.application.service.each {
addSubProcess(processNames, it)
}
manifest.application.receiver.each {
addSubProcess(processNames, it)
}
manifest.application.provider.each {
addSubProcess(processNames, it)
}
return processNames
}
private static void addSubProcess(Set<String> processNames, def it) {
String processName = it.'@android:process'
if (processName && !processNames.contains(processName)) {
processNames.add(processName)
}
}
private static void writeProvidersIntoManifestFile(String pkgName, File manifestFile, Set<String> processNames) {
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.root {
processNames.each { processName ->
if (processName){
def providerName = ProviderGenerator.getSubProcessProviderClassName(processName)
providerName = providerName.replaceAll("/", ".")
//兼容以冒号开头的子进程和直接命名的子进程
def realProcess = processName.startsWith(":") ? (pkgName + processName) : processName
provider(
"android:authorities": "${realProcess}.${AUTHORITY}",
"android:exported": "true",
"android:name": providerName,
"android:process": processName
)
}
}
}
def providerXml = writer.toString().replace("<root>", "").replace("</root>", "")
String content = manifestFile.getText("UTF-8")
int index = content.lastIndexOf("</application>")
content = content.substring(0, index) + providerXml + content.substring(index)
manifestFile.write(content, 'UTF-8')
}
}
\ No newline at end of file
package com.billy.android.register.cc.generator
import com.billy.android.register.RegisterTransform
import org.objectweb.asm.*
/**
* 生成provider类
*/
class ProviderGenerator implements Opcodes {
public static final String MULTI_PROCESS_PROVIDER_CLASS = "com/billy/cc/core/component/remote/RemoteProvider"
public static final String MAIN_CC_SUB_PROCESS_FOLDER = "com/billy/cc/core/providers"
static String getSubProcessProviderClassName(String processName) {
processName = getSubProcessRealName(processName).replaceAll("\\.", "_")
return "${MAIN_CC_SUB_PROCESS_FOLDER}/CC_Provider_${processName}"
}
static String getSubProcessRealName(String processName) {
if (processName && processName.startsWith(":"))
processName = processName.substring(1)
return processName
}
/**
* 生成RemoteProvider的子类
* @param className 子类名称(根据
* @param dir
*/
static void generateProvider(String processName, File dir) {
String className = getSubProcessProviderClassName(processName)
File file = new File(dir, className + ".class")
if (file.exists()) {
return
}
println("${RegisterTransform.PLUGIN_NAME} generated a provider: ${file.absolutePath}")
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs()
}
file.createNewFile()
byte[] bytes = generate(className, MULTI_PROCESS_PROVIDER_CLASS)
FileOutputStream fos = new FileOutputStream(file)
fos.write(bytes)
fos.close()
}
/**
* 用ASM生成指定类的子类
* @param className 要生成的子类名称
* @param superClassName 指定的父类名称
* @return 生成的字节码
* @throws Exception
*/
static byte[] generate(String className, String superClassName) throws Exception {
ClassWriter cw = new ClassWriter(0)
cw.visit(52, ACC_PUBLIC + ACC_SUPER, className, null, superClassName, null)
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null)
mv.visitCode()
mv.visitVarInsn(ALOAD, 0)
mv.visitMethodInsn(INVOKESPECIAL, superClassName, "<init>", "()V", false)
mv.visitInsn(RETURN)
mv.visitMaxs(1, 1)
mv.visitEnd()
cw.visitEnd()
return cw.toByteArray()
}
}
\ No newline at end of file
package com.billy.android.register.cc.generator
import com.billy.android.register.RegisterInfo
import org.apache.commons.io.IOUtils
import org.objectweb.asm.*
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
/**
*
* @author billy.qi
* @since 17/3/20 11:48
*/
class RegistryCodeGenerator {
RegisterInfo extension
private RegistryCodeGenerator(RegisterInfo extension) {
this.extension = extension
}
static void insertInitCodeTo(RegisterInfo extension) {
if (extension != null && !extension.classList.isEmpty()) {
RegistryCodeGenerator processor = new RegistryCodeGenerator(extension)
File file = extension.fileContainsInitClass
if (file.getName().endsWith('.jar'))
processor.generateCodeIntoJarFile(file)
else
processor.generateCodeIntoClassFile(file)
}
}
//处理jar包中的class代码注入
private File generateCodeIntoJarFile(File jarFile) {
if (jarFile) {
def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
if (optJar.exists())
optJar.delete()
def file = new JarFile(jarFile)
Enumeration enumeration = file.entries()
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = file.getInputStream(jarEntry)
jarOutputStream.putNextEntry(zipEntry)
if (isInitClass(entryName)) {
println('generate code into:' + entryName)
def bytes = doGenerateCode(inputStream)
jarOutputStream.write(bytes)
} else {
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
inputStream.close()
jarOutputStream.closeEntry()
}
jarOutputStream.close()
file.close()
if (jarFile.exists()) {
jarFile.delete()
}
optJar.renameTo(jarFile)
}
return jarFile
}
boolean isInitClass(String entryName) {
if (entryName == null || !entryName.endsWith(".class"))
return false
if (extension.initClassName) {
entryName = entryName.substring(0, entryName.lastIndexOf('.'))
return extension.initClassName == entryName
}
return false
}
/**
* 处理class的注入
* @param file class文件
* @return 修改后的字节码文件内容
*/
private byte[] generateCodeIntoClassFile(File file) {
def optClass = new File(file.getParent(), file.name + ".opt")
FileInputStream inputStream = new FileInputStream(file)
FileOutputStream outputStream = new FileOutputStream(optClass)
def bytes = doGenerateCode(inputStream)
outputStream.write(bytes)
inputStream.close()
outputStream.close()
if (file.exists()) {
file.delete()
}
optClass.renameTo(file)
return bytes
}
private byte[] doGenerateCode(InputStream inputStream) {
ClassReader cr = new ClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, 0)
ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
return cw.toByteArray()
}
class MyClassVisitor extends ClassVisitor {
MyClassVisitor(int api, ClassVisitor cv) {
super(api, cv)
}
void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
if (name == extension.initMethodName) { //注入代码到指定的方法之中
boolean _static = (access & Opcodes.ACC_STATIC) > 0
mv = new MyMethodVisitor(Opcodes.ASM5, mv, _static)
}
return mv
}
}
class MyMethodVisitor extends MethodVisitor {
boolean _static;
MyMethodVisitor(int api, MethodVisitor mv, boolean _static) {
super(api, mv)
this._static = _static;
}
@Override
void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
extension.classList.each { name ->
if (!_static) {
//加载this
mv.visitVarInsn(Opcodes.ALOAD, 0)
}
String paramType
if (extension.paramType == RegisterInfo.PARAM_TYPE_CLASS){
mv.visitLdcInsn(Type.getType("L${name};"))
paramType = 'java/lang/Class'
} else if (extension.paramType == RegisterInfo.PARAM_TYPE_CLASS_NAME){
mv.visitLdcInsn(name.replaceAll("/", "."))
paramType = 'java/lang/String'
} else {
//用无参构造方法创建一个组件实例
mv.visitTypeInsn(Opcodes.NEW, name)
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>", "()V", false)
paramType = extension.interfaceName
}
int methodOpcode = _static ? Opcodes.INVOKESTATIC : Opcodes.INVOKESPECIAL
//调用注册方法将组件实例注册到组件库中
mv.visitMethodInsn(methodOpcode
, extension.registerClassName
, extension.registerMethodName
, "(L${paramType};)V"
, false)
}
}
super.visitInsn(opcode)
}
@Override
void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals)
}
}
}
\ No newline at end of file
implementation-class=com.billy.android.register.RegisterPlugin
\ No newline at end of file
//注: 从CC 1.x升级到CC 2.x的用户,用此文件替换原来的cc-settings.gradle的同时,需要在根目录build.gradle中将插件地址按照如下方式更换一下:
// classpath 'com.billy.android:autoregister:x.x.x' -> classpath 'com.billy.android:cc-register:x.x.x'
//cc-register extension:
// 功能介绍:
// 完成组件、拦截器及跨进程json解释器等CC库自身需要的自动注册功能
// 支持新增自定义的其它自动注册功能,参考AutoRegister,用法参考cc-settings-demo.gradle
project.apply plugin: 'cc-register'
project.dependencies.add('api', "com.billy.android:cc:2.1.5") //用最新版
//此文件是作为组件化配置的公共gradle脚本文件,在每个组件中都apply此文件,下载到工程根目录后,可以在下方添加一些自己工程中通用的配置
// 可参考cc-settings-demo.gradle
// 例如:
// 1. 添加全局拦截器、下沉的公共类库等一些公共基础库的依赖;
// 2. 添加自定义的通过cc-register实现的自动注册配置
// 3. 开启app内部多进程支持
// 4. 其它公共配置信息
//开启app内部多进程组件调用时启用下面这行代码
//文档地址:https://luckybilly.github.io/CC-website/#/manual-multi-process
//ccregister.multiProcessEnabled = true
//开启app内部多进程组件调用时,可以启用下方的配置排除一些进程
//ccregister.excludeProcessNames = [':pushservice', ':processNameB']
//按照如下格式添加自定义注册项,可添加多个(也可每次add一个,add多次)
// 文档地址: https://luckybilly.github.io/CC-website/#/manual-IActionProcessor
//ccregister.registerInfo.add([
// //在自动注册组件的基础上增加:自动注册组件B的processor
// 'scanInterface' : 'com.billy.cc.demo.component.b.processor.IActionProcessor'
// , 'codeInsertToClassName' : 'com.billy.cc.demo.component.b.ComponentB'
// , 'codeInsertToMethodName' : 'initProcessors'
// , 'registerMethodName' : 'add'
//])
\ No newline at end of file
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode 1
versionName "1.0"
consumerProguardFiles 'proguard-rules.pro'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
}
dependencies {
//发布到bintray时,要写成compile才行
compile "com.billy.android:pools:0.0.6"
compileOnly rootProject.ext.dependencies["appcompat-v7"]
compileOnly project(':android_internal')
}
sourceCompatibility = 1.7
targetCompatibility = 1.7
ext {
bintrayRepo = 'android'
bintrayName = 'cc'
publishedGroupId = 'com.billy.android'
libraryName = 'CC'
artifact = 'cc'
libraryDescription = 'android component caller'
siteUrl = 'https://github.com/luckybilly/CC'
gitUrl = 'git@github.com:luckybilly/CC.git'
libraryVersion = '2.1.6'
developerId = 'billy'
developerName = 'billy'
developerEmail = 'okkanan@hotmail.com'
licenseName = 'The Apache Software License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
allLicenses = ["Apache-2.0"]
}
apply from: rootProject.file('bintray.gradle')
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/billy/Library/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 *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn android.app.**
-dontwarn android.support.**
#保持实现Parcelable的类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
\ No newline at end of file
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.billy.cc.core.component">
<application >
<provider
android:authorities="${applicationId}.com.billy.cc.core.remote"
android:name=".remote.RemoteProvider"
android:exported="true"
/>
<activity android:name=".remote.RemoteConnectionActivity"
android:taskAffinity="com.billy.cc.connection"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:exported="true"
>
<intent-filter>
<action android:name="action.com.billy.cc.connection" />
</intent-filter>
</activity>
</application>
</manifest>
// com.billy.core.component.IProcessCrossCCService.aidl
package com.billy.cc.core.component.remote;
// Declare any non-default types here with import statements
import com.billy.cc.core.component.remote.RemoteCC;
import com.billy.cc.core.component.remote.RemoteCCResult;
import com.billy.cc.core.component.remote.IRemoteCallback;
interface IRemoteCCService {
void call(in RemoteCC remoteCC, in IRemoteCallback callback);
void cancel(String callId);
void timeout(String callId);
String getComponentProcessName(String componentName);
}
// com.billy.core.component.IProcessCrossCCService.aidl
package com.billy.cc.core.component.remote;
// Declare any non-default types here with import statements
import com.billy.cc.core.component.remote.RemoteCCResult;
interface IRemoteCallback {
void callback(in RemoteCCResult remoteCCResult);
}
// IProcessCrossCC.aidl
package com.billy.cc.core.component.remote;
// Declare any non-default types here with import statements
parcelable RemoteCC;
// IProcessCrossCC.aidl
package com.billy.cc.core.component.remote;
// Declare any non-default types here with import statements
parcelable RemoteCCResult;
package com.billy.cc.core.component;
import android.text.TextUtils;
/**
* 转发组件调用 <br>
* 注:如果需要做成全局拦截器,需要额外实现 {@link IGlobalCCInterceptor}接口
* @author billy.qi
* @since 18/9/2 13:40
*/
public abstract class BaseForwardInterceptor implements ICCInterceptor {
@Override
public CCResult intercept(Chain chain) {
CC cc = chain.getCC();
String forwardComponentName = shouldForwardCC(cc, cc.getComponentName());
if (!TextUtils.isEmpty(forwardComponentName)) {
cc.forwardTo(forwardComponentName);
}
return chain.proceed();
}
/**
* 根据当前组件调用对象获取需要转发到的组件名称
* @param cc 当前组件调用对象
* @param componentName 当前调用的组件名称
* @return 转发的目标组件名称(为null则不执行转发)
*/
protected abstract String shouldForwardCC(CC cc, String componentName);
}
package com.billy.cc.core.component;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
import android.os.SystemClock;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* CC监控系统
* 每个CC对象有自己的超时时间点,
* 维护一个待监控的CC列表,当列表不为空时,启动一个线程进行监控
* @author billy.qi
*/
class CCMonitor {
static final ConcurrentHashMap<String, CC> CC_MAP = new ConcurrentHashMap<>();
private static final AtomicBoolean STOPPED = new AtomicBoolean(true);
private static volatile long minTimeoutAt = Long.MAX_VALUE;
private static final byte[] LOCK = new byte[0];
static void addMonitorFor(CC cc) {
if (cc != null) {
CC_MAP.put(cc.getCallId(), cc);
cc.addCancelOnFragmentDestroyIfSet();
long timeoutAt = cc.timeoutAt;
if (timeoutAt > 0) {
if (minTimeoutAt > timeoutAt) {
minTimeoutAt = timeoutAt;
//如果最小timeout时间有变化,且监控线程在wait,则唤醒监控线程
synchronized (LOCK) {
LOCK.notifyAll();
}
}
if (STOPPED.compareAndSet(true, false)) {
new TimeoutMonitorThread().start();
}
}
if (CC.VERBOSE_LOG) {
CC.verboseLog(cc.getCallId(), "totalCC count=" + CC_MAP.size()
+ ". add monitor for:" + cc);
}
}
}
static CC getById(String callId) {
return callId == null ? null : CC_MAP.get(callId);
}
static void removeById(String callId) {
CC_MAP.remove(callId);
}
/**
* CC超时监控线程
*/
private static class TimeoutMonitorThread extends Thread {
@Override
public void run() {
if (STOPPED.get()) {
return;
}
while(CC_MAP.size() > 0 || minTimeoutAt == Long.MAX_VALUE) {
try {
long millis = minTimeoutAt - SystemClock.elapsedRealtime();
if (millis > 0) {
synchronized (LOCK) {
LOCK.wait(millis);
}
}
//next cc timeout
long min = Long.MAX_VALUE;
long now = SystemClock.elapsedRealtime();
for (CC cc : CC_MAP.values()) {
if (!cc.isFinished()) {
long timeoutAt = cc.timeoutAt;
if (timeoutAt > 0) {
if (timeoutAt < now) {
executeTimeout(cc);
} else if (timeoutAt < min) {
min = timeoutAt;
}
}
}
}
minTimeoutAt = min;
} catch (InterruptedException ignored) {
}
}
STOPPED.set(true);
}
/**
* 执行 timeout()
* 注意:如果处于程序调试状态和CC.DEBUG是true,
* 两个都满足情况下,不执行超时 timeout()
* @param cc
*/
private void executeTimeout(CC cc) {
if (!CC.DEBUG) {
cc.timeout();
return;
}
if (!Debug.isDebuggerConnected()) {
cc.timeout();
}
}
}
/**
* activity lifecycle monitor
*
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
static class ActivityMonitor implements Application.ActivityLifecycleCallbacks {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { }
@Override public void onActivityStarted(Activity activity) { }
@Override public void onActivityResumed(Activity activity) { }
@Override public void onActivityPaused(Activity activity) { }
@Override public void onActivityStopped(Activity activity) { }
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { }
@Override public void onActivityDestroyed(Activity activity) {
Collection<CC> values = CC_MAP.values();
for (CC cc : values) {
if (!cc.isFinished() && cc.cancelOnDestroyActivity != null
&& cc.cancelOnDestroyActivity.get() == activity) {
cc.cancelOnDestroy(activity);
}
}
}
}
static class FragmentMonitor extends FragmentManager.FragmentLifecycleCallbacks {
WeakReference<CC> reference;
FragmentMonitor(CC cc) {
this.reference = new WeakReference<>(cc);
}
@Override
public void onFragmentDestroyed(FragmentManager fm, Fragment f) {
if (reference != null) {
CC cc = reference.get();
if (cc != null && !cc.isFinished()) {
WeakReference<Fragment> fragReference = cc.cancelOnDestroyFragment;
if (fragReference != null) {
Fragment fragment = fragReference.get();
if (f == fragment) {
cc.cancelOnDestroy(f);
}
}
}
}
}
}
}
\ No newline at end of file
package com.billy.cc.core.component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 组件调用链,用于管理拦截器的运行顺序
* @author billy.qi
*/
public class Chain {
private final List<ICCInterceptor> interceptors = new ArrayList<>();
private final CC cc;
private int index;
Chain(CC cc) {
this.cc = cc;
this.index = 0;
}
void addInterceptors(Collection<? extends ICCInterceptor> interceptors) {
if (interceptors != null && !interceptors.isEmpty()) {
this.interceptors.addAll(interceptors);
}
}
void addInterceptor(ICCInterceptor interceptor) {
if (interceptor != null) {
this.interceptors.add(interceptor);
}
}
public CCResult proceed() {
if (index >= interceptors.size()) {
return CCResult.defaultNullResult();
}
ICCInterceptor interceptor = interceptors.get(index++);
//处理异常情况:如果为拦截器为null,则执行下一个
if (interceptor == null) {
return proceed();
}
String name = interceptor.getClass().getName();
String callId = cc.getCallId();
CCResult result;
if (cc.isFinished()) {
//timeout, cancel, CC.sendCCResult(callId, ccResult), cc.setResult, etc...
result = cc.getResult();
} else {
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, "start interceptor:" + name + ", cc:" + cc);
}
try {
result = interceptor.intercept(this);
} catch(Throwable e) {
//防止拦截器抛出异常
result = CCResult.defaultExceptionResult(e);
}
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, "end interceptor:" + name + ".CCResult:" + result);
}
}
//拦截器理论上不应该返回null,但为了防止意外(自定义拦截器返回null,此处保持CCResult不为null
//消灭NPE
if (result == null) {
result = CCResult.defaultNullResult();
}
cc.setResult(result);
return result;
}
public CC getCC() {
return cc;
}
}
package com.billy.cc.core.component;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 启动拦截器调用链
* @author billy.qi
*/
class ChainProcessor implements Callable<CCResult> {
private final Chain chain;
ChainProcessor(Chain chain) {
this.chain = chain;
}
@Override
public CCResult call() throws Exception {
CC cc = chain.getCC();
String callId = cc.getCallId();
//从开始调用的时候就开始进行监控,也许时间设置的很短,可能都不需要执行拦截器调用链
CCMonitor.addMonitorFor(cc);
CCResult result;
try {
if (CC.VERBOSE_LOG) {
int poolSize = ((ThreadPoolExecutor) ComponentManager.CC_THREAD_POOL).getPoolSize();
CC.verboseLog(callId, "process cc at thread:"
+ Thread.currentThread().getName() + ", pool size=" + poolSize);
}
if (cc.isFinished()) {
//timeout, cancel, CC.sendCCResult(callId, ccResult)
result = cc.getResult();
} else {
try {
CC.verboseLog(callId, "start interceptor chain");
result = chain.proceed();
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, "end interceptor chain.CCResult:" + result);
}
} catch(Exception e) {
result = CCResult.defaultExceptionResult(e);
}
}
} catch(Exception e) {
result = CCResult.defaultExceptionResult(e);
} finally {
CCMonitor.removeById(callId);
}
//返回的结果,永不为null,默认为CCResult.defaultNullResult()
if (result == null) {
result = CCResult.defaultNullResult();
}
//调用请求处理完成后,CC对象中不存储CCResult
cc.setResult(null);
performCallback(cc, result);
return result;
}
private static void performCallback(CC cc, CCResult result) {
IComponentCallback callback = cc.getCallback();
if (CC.VERBOSE_LOG) {
CC.verboseLog(cc.getCallId(), "perform callback:" + cc.getCallback()
+ ", CCResult:" + result);
}
if (callback == null) {
return;
}
if (cc.isCallbackOnMainThread()) {
ComponentManager.mainThread(new CallbackRunnable(callback, cc, result));
} else {
try {
callback.onResult(cc, result);
} catch(Exception e) {
CCUtil.printStackTrace(e);
}
}
}
private static class CallbackRunnable implements Runnable {
private final CC cc;
private IComponentCallback callback;
private CCResult result;
CallbackRunnable(IComponentCallback callback, CC cc, CCResult result) {
this.cc = cc;
this.callback = callback;
this.result = result;
}
@Override
public void run() {
try {
callback.onResult(cc, result);
} catch(Exception e) {
CCUtil.printStackTrace(e);
}
}
}
}
package com.billy.cc.core.component;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 全局拦截器{@link IGlobalCCInterceptor}的管理类
* @author billy.qi
* @since 18/5/26 14:07
*/
class GlobalCCInterceptorManager {
static final CopyOnWriteArrayList<IGlobalCCInterceptor> INTERCEPTORS = new CopyOnWriteArrayList<>();
/*
加载类时自动调用初始化:注册所有全局拦截器 (实现 IGlobalCCInterceptor 接口的类)
通过auto-register插件生成拦截器注册代码
生成的代码如下:
static {
registerGlobalInterceptor(new InterceptorA());
registerGlobalInterceptor(new InterceptorB());
}
*/
/**
* 提前初始化所有全局拦截器
*/
static void init(){
//调用此方法时,虚拟机会加载GlobalCCInterceptorManager类
//会自动执行static块中的全局拦截器注册,调用拦截器类的无参构造方法
//如果不提前调用此方法,static块中的代码将在第一次进行组件调用时(cc.callXxx())执行
}
static void registerGlobalInterceptor(IGlobalCCInterceptor interceptor) {
if (interceptor == null) {
if (CC.DEBUG) {
CC.logError("register global interceptor is null!");
}
} else {
Class clazz = interceptor.getClass();
synchronized (INTERCEPTORS) {
int index = 0;
for (IGlobalCCInterceptor it : INTERCEPTORS) {
if (it.getClass() == clazz) {
if (CC.DEBUG) {
CC.logError("duplicate global interceptor:" + clazz.getName());
}
return;
}
if (it.priority() > interceptor.priority()) {
index++;
}
}
INTERCEPTORS.add(index,interceptor);
}
if (CC.DEBUG) {
CC.log("register global interceptor success! priority = "
+ interceptor.priority() + ", class = " + clazz.getName());
}
}
}
static void unregisterGlobalInterceptor(Class<? extends IGlobalCCInterceptor> clazz) {
synchronized (INTERCEPTORS) {
for (IGlobalCCInterceptor next : INTERCEPTORS) {
if (next.getClass() == clazz) {
INTERCEPTORS.remove(next);
if (CC.DEBUG) {
CC.log("unregister global interceptor success! class = " + clazz.getName());
}
break;
}
}
}
}
}
package com.billy.cc.core.component;
/**
* 拦截器接口
*
* @author billy.qi
*/
public interface ICCInterceptor {
/**
* 拦截器方法
* chain.getCC() 来获取cc对象
* 调用chain.proceed()来传递调用链
* 也可通过不调用chain.proceed()来中止调用链的传递
* 通过cc.getParams()等方法来获取参数信息,并可以修改params
* @param chain 链条
* @return 调用结果
*/
CCResult intercept(Chain chain);
}
package com.billy.cc.core.component;
/**
* 组件接口
* 注意:
* 1. 此接口的实现类代表的是一个组件暴露给外部调用的入口
* 2. 实现类必须含有一个无参构造方法,以供自动注册插件进行代码注入
* 3. 实现类有且只有一个对象会被注册到组件库中,故不能为Activity、Fragment等(可以改用动态组件注册{@link IDynamicComponent})
* 动态组件: {@link IDynamicComponent}
* @author billy.qi
*/
public interface IComponent {
/**
* 定义组件名称
* @return 组件的名称
*/
String getName();
/**
* 调用此组件时执行的方法(此方法只在LocalCCInterceptor中被调用)
* 注:执行完成后必须调用CC.sendCCResult(callId, CCResult.success(result));
* cc.getContext() android的context
* cc.getAction() 调用的action
* cc.getParams() 调用参数
* cc.getCallId() 调用id,用于通过CC向调用方发送调用结果
* @param cc 调用信息
* @return 是否延迟回调结果 {@link CC#sendCCResult(String, CCResult)}
* false:否(同步实现,在return之前回调结果)
* true:是(异步实现,本次CC调用将等待回调结果)
*/
boolean onCall(CC cc);
}
package com.billy.cc.core.component;
/**
* 组件回调
* @author billy.qi
* @since 17/6/29 11:34
*/
public interface IComponentCallback {
/**
* call when cc is received CCResult
* @param cc cc
* @param result the CCResult
*/
void onResult(CC cc, CCResult result);
}
package com.billy.cc.core.component;
/**
* 动态组件
* 静态组件会在编译时代码扫描生成注册代码,将以单例方式运行;
* 动态组件则可以在运行时动态地通过以下代码来进行注册与反注册
* {@link CC#registerComponent(IDynamicComponent)} {@link CC#unregisterComponent(IDynamicComponent)}
* @author billy.qi
* @since 17/7/24 16:34
*/
public interface IDynamicComponent extends IComponent {
}
package com.billy.cc.core.component;
/**
* 全局拦截器
* 注:为了防止开发阶段跨app调用组件时全局拦截器重复执行,全局拦截器应全部放在公共库中,供所有组件依赖,
* 跨app调用组件时,只会执行调用方app的全局拦截器,被调用方app内的全局拦截器不执行
* @author billy.qi
* @since 18/5/26 10:05
*/
public interface IGlobalCCInterceptor extends ICCInterceptor {
/**
* 优先级,(可重复,相同的优先级其执行顺序将得不到保障)
* @return 全局拦截器的优先级,按从大到小的顺序执行
*/
int priority();
}
package com.billy.cc.core.component;
/**
* 指定是否在主线程运行
* @author billy.qi
* @since 18/9/19 11:31
*/
public interface IMainThread {
/**
* 根据当前actionName确定组件的{@link IComponent#onCall(CC)} 方法是否在主线程运行
* @param actionName 当前CC的action名称
* @param cc 当前CC对象
* @return 3种返回值: <br>
* null:默认状态,不固定运行的线程(在主线程同步调用时在主线程运行,其它情况下在子线程运行)<br>
* true:固定在主线程运行<br>
* false:固定子线程运行<br>
*/
Boolean shouldActionRunOnMainThread(String actionName, CC cc);
}
package com.billy.cc.core.component;
/**
* app间传递参数时,用于将自定义类型转换为json进行传递,并在传递成功后复原为自定义类型
* @author billy.qi
* @since 18/5/28 16:11
*/
public interface IParamJsonConverter {
/**
* 将json字符串转换为对象
* @param json json字符串
* @param clazz 类型
* @param <T> 泛型
* @return json转换的对象
*/
<T> T json2Object(String json, Class<T> clazz);
/**
* Object to json
*
* @param instance 要跨app传递的对象
* @return json字符串
*/
String object2Json(Object instance);
}
package com.billy.cc.core.component;
import android.os.Looper;
/**
* 调用当前app内组件的拦截器<br>
* 如果本地找不到该组件,则添加{@link RemoteCCInterceptor}来处理<br>
* 如果组件onCall方法执行完之前未调用{@link CC#sendCCResult(String, CCResult)}方法,则按返回值来进行以下处理:<br>
* 返回值为false: 回调状态码为 {@link CCResult#CODE_ERROR_CALLBACK_NOT_INVOKED} 的错误结果给调用方<br>
* 返回值为true: 添加{@link Wait4ResultInterceptor}来等待组件调用{@link CC#sendCCResult(String, CCResult)}方法
* @author billy.qi
*/
class LocalCCInterceptor implements ICCInterceptor {
//-------------------------单例模式 start --------------
/** 单例模式Holder */
private static class LocalCCInterceptorHolder {
private static final LocalCCInterceptor INSTANCE = new LocalCCInterceptor();
}
private LocalCCInterceptor (){}
/** 获取LocalCCInterceptor的单例对象 */
static LocalCCInterceptor getInstance() {
return LocalCCInterceptorHolder.INSTANCE;
}
//-------------------------单例模式 end --------------
@Override
public CCResult intercept(Chain chain) {
CC cc = chain.getCC();
IComponent component = ComponentManager.getComponentByName(cc.getComponentName());
if (component == null) {
CC.verboseLog(cc.getCallId(), "component not found in this app. maybe 2 reasons:"
+ "\n1. CC.enableRemoteCC changed to false"
+ "\n2. Component named \"%s\" is a IDynamicComponent but now is unregistered"
);
return CCResult.error(CCResult.CODE_ERROR_NO_COMPONENT_FOUND);
}
try {
String callId = cc.getCallId();
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, "start component:%s, cc: %s", component.getClass().getName(), cc.toString());
}
boolean shouldSwitchThread = false;
LocalCCRunnable runnable = new LocalCCRunnable(cc, component);
if (component instanceof IMainThread) {
//当前是否在主线程
boolean curIsMainThread = Looper.myLooper() == Looper.getMainLooper();
//该action是否应该在主线程运行
Boolean runOnMainThread = ((IMainThread) component).shouldActionRunOnMainThread(cc.getActionName(), cc);
//是否需要切换线程执行 component.onCall(cc) 方法
shouldSwitchThread = runOnMainThread != null && runOnMainThread ^ curIsMainThread;
if (shouldSwitchThread) {
runnable.setShouldSwitchThread(true);
if (runOnMainThread) {
//需要在主线程运行,但是当前线程不是主线程
ComponentManager.mainThread(runnable);
} else {
//需要在子线程运行,但当前线程不是子线程
ComponentManager.threadPool(runnable);
}
}
}
if (!shouldSwitchThread) {
//不需要切换线程,直接运行
runnable.run();
}
//兼容以下情况:
// 1. 不需要切换线程,但需要等待异步实现调用CC.sendCCResult(...)
// 2. 需要切换线程,等待切换后的线程调用组件后调用CC.sendCCResult(...)
if (!cc.isFinished()) {
chain.proceed();
}
} catch(Exception e) {
return CCResult.defaultExceptionResult(e);
}
return cc.getResult();
}
static class LocalCCRunnable implements Runnable {
private final String callId;
private CC cc;
private IComponent component;
private boolean shouldSwitchThread;
LocalCCRunnable(CC cc, IComponent component) {
this.cc = cc;
this.callId = cc.getCallId();
this.component = component;
}
void setShouldSwitchThread(boolean shouldSwitchThread) {
this.shouldSwitchThread = shouldSwitchThread;
}
@Override
public void run() {
if (cc.isFinished()) {
return;
}
try {
boolean callbackDelay = component.onCall(cc);
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, component.getName() + ":"
+ component.getClass().getName()
+ ".onCall(cc) return:" + callbackDelay
);
}
if (!callbackDelay && !cc.isFinished()) {
CC.logError("component.onCall(cc) return false but CC.sendCCResult(...) not called!"
+ "\nmaybe: actionName error"
+ "\nor if-else not call CC.sendCCResult"
+ "\nor switch-case-default not call CC.sendCCResult"
+ "\nor try-catch block not call CC.sendCCResult."
);
//没有返回结果,且不是延时回调(也就是说不会收到结果了)
setResult(CCResult.error(CCResult.CODE_ERROR_CALLBACK_NOT_INVOKED));
}
} catch(Exception e) {
setResult(CCResult.defaultExceptionResult(e));
}
}
private void setResult(CCResult result) {
if (shouldSwitchThread) {
//若出现线程切换
// LocalCCInterceptor.intercept会执行chain.proceed()进入wait状态
// 需要解除wait状态
cc.setResult4Waiting(result);
} else {
cc.setResult(result);
}
}
}
}
package com.billy.cc.core.component;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.DeadObjectException;
import android.os.SystemClock;
import android.text.TextUtils;
import com.billy.cc.core.component.remote.IRemoteCCService;
import com.billy.cc.core.component.remote.RemoteConnection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 跨App调用组件
* 继承自 {@link SubProcessCCInterceptor}, 额外处理了跨App的进程连接
* @author billy.qi
* @since 18/6/24 00:25
*/
class RemoteCCInterceptor extends SubProcessCCInterceptor {
private static final ConcurrentHashMap<String, IRemoteCCService> REMOTE_CONNECTIONS = new ConcurrentHashMap<>();
//-------------------------单例模式 start --------------
/** 单例模式Holder */
private static class RemoteCCInterceptorHolder {
private static final RemoteCCInterceptor INSTANCE = new RemoteCCInterceptor();
}
private RemoteCCInterceptor(){}
/** 获取{@link RemoteCCInterceptor}的单例对象 */
static RemoteCCInterceptor getInstance() {
return RemoteCCInterceptorHolder.INSTANCE;
}
//-------------------------单例模式 end --------------
@Override
public CCResult intercept(Chain chain) {
String processName = getProcessName(chain.getCC().getComponentName());
if (!TextUtils.isEmpty(processName)) {
return multiProcessCall(chain, processName, REMOTE_CONNECTIONS);
}
return CCResult.error(CCResult.CODE_ERROR_NO_COMPONENT_FOUND);
}
private String getProcessName(String componentName) {
String processName = null;
try {
for (Map.Entry<String, IRemoteCCService> entry : REMOTE_CONNECTIONS.entrySet()) {
try {
processName = entry.getValue().getComponentProcessName(componentName);
} catch(DeadObjectException e) {
String processNameTo = entry.getKey();
RemoteCCService.remove(processNameTo);
IRemoteCCService service = RemoteCCService.get(processNameTo);
if (service == null) {
String packageName = processNameTo.split(":")[0];
boolean wakeup = RemoteConnection.tryWakeup(packageName);
CC.log("wakeup remote app '%s'. success=%b.", packageName, wakeup);
if (wakeup) {
service = getMultiProcessService(processNameTo);
}
}
if (service != null) {
try {
processName = service.getComponentProcessName(componentName);
REMOTE_CONNECTIONS.put(processNameTo, service);
} catch(Exception ex) {
CCUtil.printStackTrace(ex);
}
}
}
if (!TextUtils.isEmpty(processName)) {
return processName;
}
}
} catch(Exception e) {
CCUtil.printStackTrace(e);
}
return processName;
}
void enableRemoteCC() {
//监听设备上其它包含CC组件的app
listenComponentApps();
connect(RemoteConnection.scanComponentApps());
}
private static final String INTENT_FILTER_SCHEME = "package";
private void listenComponentApps() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addAction(Intent.ACTION_MY_PACKAGE_REPLACED);
intentFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
intentFilter.addDataScheme(INTENT_FILTER_SCHEME);
CC.getApplication().registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String packageName = intent.getDataString();
if (TextUtils.isEmpty(packageName)) {
return;
}
if (packageName.startsWith(INTENT_FILTER_SCHEME)) {
packageName = packageName.replace(INTENT_FILTER_SCHEME + ":", "");
}
String action = intent.getAction();
CC.log("onReceived.....pkg=" + packageName + ", action=" + action);
if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
REMOTE_CONNECTIONS.remove(packageName);
} else {
CC.log("start to wakeup remote app:%s", packageName);
if (RemoteConnection.tryWakeup(packageName)) {
ComponentManager.threadPool(new ConnectTask(packageName));
}
}
}
}, intentFilter);
}
private void connect(List<String> packageNames) {
if (packageNames == null || packageNames.isEmpty()) {
return;
}
for (String pkg : packageNames) {
ComponentManager.threadPool(new ConnectTask(pkg));
}
}
class ConnectTask implements Runnable {
String packageName;
ConnectTask(String packageName) {
this.packageName = packageName;
}
@Override
public void run() {
IRemoteCCService service = getMultiProcessService(packageName);
if (service != null) {
REMOTE_CONNECTIONS.put(packageName, service);
}
}
}
private static final int MAX_CONNECT_TIME_DURATION = 1000;
@Override
protected IRemoteCCService getMultiProcessService(String packageName) {
long start = SystemClock.elapsedRealtime();
IRemoteCCService service = null;
while (SystemClock.elapsedRealtime() - start < MAX_CONNECT_TIME_DURATION) {
service = RemoteCCService.get(packageName);
if (service != null) {
break;
}
SystemClock.sleep(50);
}
CC.log("connect remote app '%s' %s. cost time=%d"
, packageName
, service == null ? "failed" : "success"
, (SystemClock.elapsedRealtime() - start));
return service;
}
}
package com.billy.cc.core.component;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import com.billy.cc.core.component.remote.IRemoteCCService;
import com.billy.cc.core.component.remote.IRemoteCallback;
import com.billy.cc.core.component.remote.RemoteCC;
import com.billy.cc.core.component.remote.RemoteCCResult;
import com.billy.cc.core.component.remote.RemoteCursor;
import com.billy.cc.core.component.remote.RemoteProvider;
import java.util.concurrent.ConcurrentHashMap;
/**
* 跨进程调用组件的Binder
* @author billy.qi
* @since 18/6/24 11:31
*/
public class RemoteCCService extends IRemoteCCService.Stub {
private Handler mainThreadHandler;
//-------------------------单例模式 start --------------
/** 单例模式Holder */
private static class RemoteCCServiceHolder {
private static final RemoteCCService INSTANCE = new RemoteCCService();
}
private RemoteCCService(){
mainThreadHandler = new Handler(Looper.getMainLooper());
}
/** 获取RemoteCCService的单例对象 */
public static RemoteCCService getInstance() {
return RemoteCCService.RemoteCCServiceHolder.INSTANCE;
}
//-------------------------单例模式 end --------------
@Override
public void call(final RemoteCC remoteCC, final IRemoteCallback callback) throws RemoteException {
if (isInvalidate()) {
return;
}
String componentName = remoteCC.getComponentName();
final String callId = remoteCC.getCallId();
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, "receive call from other process. RemoteCC: %s", remoteCC.toString());
}
if (!ComponentManager.hasComponent(componentName)) {
CC.verboseLog(callId, "There is no component found for name:%s in process:%s", componentName, CCUtil.getCurProcessName());
doCallback(callback, callId, CCResult.error(CCResult.CODE_ERROR_NO_COMPONENT_FOUND));
return;
}
final CC cc = CC.obtainBuilder(componentName)
.setActionName(remoteCC.getActionName())
.setParams(remoteCC.getParams())
.setCallId(remoteCC.getCallId())
.withoutGlobalInterceptor() //为了不重复调用拦截器,全局拦截器需要下沉复用,只在调用方进程中执行
.setNoTimeout() //超时逻辑在调用方进程中处理
.build();
if (remoteCC.isMainThreadSyncCall()) {
mainThreadHandler.post(new Runnable() {
@Override
public void run() {
CCResult ccResult = cc.call();
doCallback(callback, callId, ccResult);
}
});
} else {
cc.callAsync(new IComponentCallback() {
@Override
public void onResult(CC cc, CCResult result) {
doCallback(callback, callId, result);
}
});
}
}
private boolean isInvalidate() {
//未开启跨app调用时进行跨app调用视为无效调用
return !CC.isRemoteCCEnabled() && getCallingUid() != Process.myUid();
}
private static void doCallback(IRemoteCallback callback, String callId, CCResult ccResult) {
try {
RemoteCCResult remoteCCResult;
try{
remoteCCResult = new RemoteCCResult(ccResult);
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, "callback to other process. RemoteCCResult: %s", remoteCCResult.toString());
}
}catch(Exception e){
remoteCCResult = new RemoteCCResult(CCResult.error(CCResult.CODE_ERROR_REMOTE_CC_DELIVERY_FAILED));
if (CC.VERBOSE_LOG) {
CC.verboseLog(callId, "remote CC success. But result can not be converted for IPC. RemoteCCResult: %s", remoteCCResult.toString());
}
}
callback.callback(remoteCCResult);
} catch (RemoteException e) {
CCUtil.printStackTrace(e);
CC.verboseLog(callId, "remote doCallback failed!");
}
}
@Override
public void cancel(String callId) throws RemoteException {
if (isInvalidate()) {
return;
}
CC.cancel(callId);
}
@Override
public void timeout(String callId) throws RemoteException {
if (isInvalidate()) {
return;
}
CC.timeout(callId);
}
@Override
public String getComponentProcessName(String componentName) throws RemoteException {
if (isInvalidate()) {
return null;
}
return ComponentManager.getComponentProcessName(componentName);
}
private static final ConcurrentHashMap<String, IRemoteCCService> CACHE = new ConcurrentHashMap<>();
private static final byte[] LOCK = new byte[0];
private static Uri getDispatcherProviderUri(String processName) {
return Uri.parse("content://" + processName + "." + RemoteProvider.URI_SUFFIX + "/cc");
}
static IRemoteCCService get(String processNameTo) {
IRemoteCCService service = CACHE.get(processNameTo);
if (service == null && CC.getApplication() != null) {
synchronized (LOCK) {
service = CACHE.get(processNameTo);
if (service == null) {
service = getService(processNameTo);
if (service != null) {
CACHE.put(processNameTo, service);
}
}
}
}
return service;
}
static void remove(String processName) {
CACHE.remove(processName);
}
private static IRemoteCCService getService(String processNameTo) {
Cursor cursor = null;
try {
cursor = CC.getApplication().getContentResolver()
.query(getDispatcherProviderUri(processNameTo)
, RemoteProvider.PROJECTION_MAIN, null
, null, null
);
if (cursor == null) {
return null;
}
return RemoteCursor.getRemoteCCService(cursor);
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
CCUtil.printStackTrace(e);
}
}
}
}
}
package com.billy.cc.core.component;
import android.os.DeadObjectException;
import android.os.Looper;
import android.os.RemoteException;
import com.billy.cc.core.component.remote.IRemoteCCService;
import com.billy.cc.core.component.remote.IRemoteCallback;
import com.billy.cc.core.component.remote.RemoteCC;
import com.billy.cc.core.component.remote.RemoteCCResult;
import java.util.concurrent.ConcurrentHashMap;
/**
* App内跨进程调用组件
* @author billy.qi
* @since 18/6/24 00:25
*/
class SubProcessCCInterceptor implements ICCInterceptor {
private static final ConcurrentHashMap<String, IRemoteCCService> CONNECTIONS = new ConcurrentHashMap<>();
//-------------------------单例模式 start --------------
/** 单例模式Holder */
private static class SubProcessCCInterceptorHolder {
private static final SubProcessCCInterceptor INSTANCE = new SubProcessCCInterceptor();
}
SubProcessCCInterceptor(){}
/** 获取SubProcessCCInterceptor的单例对象 */
static SubProcessCCInterceptor getInstance() {
return SubProcessCCInterceptorHolder.INSTANCE;
}
//-------------------------单例模式 end --------------
@Override
public CCResult intercept(Chain chain) {
String componentName = chain.getCC().getComponentName();
String processName = ComponentManager.getComponentProcessName(componentName);
return multiProcessCall(chain, processName, CONNECTIONS);
}
CCResult multiProcessCall(Chain chain, String processName
, ConcurrentHashMap<String, IRemoteCCService> connectionCache) {
if (processName == null) {
return CCResult.error(CCResult.CODE_ERROR_NO_COMPONENT_FOUND);
}
CC cc = chain.getCC();
//主线程同步调用时,跨进程也要在主线程同步调用
boolean isMainThreadSyncCall = !cc.isAsync() && Looper.getMainLooper() == Looper.myLooper();
ProcessCrossTask task = new ProcessCrossTask(cc, processName, connectionCache, isMainThreadSyncCall);
ComponentManager.threadPool(task);
if (!cc.isFinished()) {
//执行 Wait4ResultInterceptor
chain.proceed();
//如果是提前结束的,跨进程通知被调用方
if (cc.isCanceled()) {
task.cancel();
} else if (cc.isTimeout()) {
task.timeout();
}
}
return cc.getResult();
}
protected IRemoteCCService getMultiProcessService(String processName) {
CC.log("start to get RemoteService from process %s", processName);
IRemoteCCService service = RemoteCCService.get(processName);
CC.log("get RemoteService from process %s %s!", processName, (service != null ? "success" : "failed"));
return service;
}
class ProcessCrossTask implements Runnable {
private final CC cc;
private final String processName;
private final ConcurrentHashMap<String, IRemoteCCService> connectionCache;
private final boolean isMainThreadSyncCall;
private IRemoteCCService service;
ProcessCrossTask(CC cc, String processName, ConcurrentHashMap<String, IRemoteCCService> connectionCache, boolean isMainThreadSyncCall) {
this.cc = cc;
this.processName = processName;
this.connectionCache = connectionCache;
this.isMainThreadSyncCall = isMainThreadSyncCall;
}
@Override
public void run() {
RemoteCC processCrossCC = new RemoteCC(cc, isMainThreadSyncCall);
call(processCrossCC);
}
private void call(RemoteCC remoteCC) {
try {
service = connectionCache.get(processName);
if (service == null) {
//获取跨进程通信的binder
service = getMultiProcessService(processName);
if (service != null) {
connectionCache.put(processName, service);
}
}
if (cc.isFinished()) {
CC.verboseLog(cc.getCallId(), "cc is finished before call %s process", processName);
return;
}
if (service == null) {
CC.verboseLog(cc.getCallId(), "RemoteService is not found for process: %s", processName);
setResult(CCResult.error(CCResult.CODE_ERROR_NO_COMPONENT_FOUND));
return;
}
if (CC.VERBOSE_LOG) {
CC.verboseLog(cc.getCallId(), "start to call process:%s, RemoteCC: %s"
, processName, remoteCC.toString());
}
service.call(remoteCC, new IRemoteCallback.Stub() {
@Override
public void callback(RemoteCCResult remoteCCResult) throws RemoteException {
try {
if (CC.VERBOSE_LOG) {
CC.verboseLog(cc.getCallId(), "receive RemoteCCResult from process:%s, RemoteCCResult: %s"
, processName, remoteCCResult.toString());
}
setResult(remoteCCResult.toCCResult());
} catch(Exception e) {
CCUtil.printStackTrace(e);
setResult(CCResult.error(CCResult.CODE_ERROR_REMOTE_CC_DELIVERY_FAILED));
}
}
});
} catch (DeadObjectException e) {
RemoteCCService.remove(processName);
connectionCache.remove(processName);
call(remoteCC);
} catch (Exception e) {
CCUtil.printStackTrace(e);
setResult(CCResult.error(CCResult.CODE_ERROR_REMOTE_CC_DELIVERY_FAILED));
}
}
void setResult(CCResult result) {
cc.setResult4Waiting(result);
}
void cancel() {
try {
service.cancel(cc.getCallId());
} catch (Exception e) {
CCUtil.printStackTrace(e);
}
}
void timeout() {
try {
service.timeout(cc.getCallId());
} catch (Exception e) {
CCUtil.printStackTrace(e);
}
}
}
}
package com.billy.cc.core.component;
import android.text.TextUtils;
/**
* 检查cc是否合法
* @author billy.qi
*/
class ValidateInterceptor implements ICCInterceptor {
//-------------------------单例模式 start --------------
/** 单例模式Holder */
private static class ValidateInterceptorHolder {
private static final ValidateInterceptor INSTANCE = new ValidateInterceptor();
}
private ValidateInterceptor (){}
/** 获取ValidateInterceptor的单例对象 */
static ValidateInterceptor getInstance() {
return ValidateInterceptorHolder.INSTANCE;
}
//-------------------------单例模式 end --------------
@Override
public CCResult intercept(Chain chain) {
CC cc = chain.getCC();
String componentName = cc.getComponentName();
int code = 0;
Boolean notFoundInCurApp = null;
if (TextUtils.isEmpty(componentName)) {
//没有指定要调用的组件名称,中止运行
code = CCResult.CODE_ERROR_COMPONENT_NAME_EMPTY;
} else if (cc.getContext() == null) {
//context为null (没有设置context 且 CC中获取application失败)
code = CCResult.CODE_ERROR_CONTEXT_NULL;
} else {
if (!ComponentManager.hasComponent(componentName)) {
//当前进程中不包含此组件,查看一下其它进程中是否包含此组件
notFoundInCurApp = TextUtils.isEmpty(ComponentManager.getComponentProcessName(componentName));
if (notFoundInCurApp && !CC.isRemoteCCEnabled()) {
//本app内所有进程均没有指定的组件,并且设置了不会调用外部app的组件
code = CCResult.CODE_ERROR_NO_COMPONENT_FOUND;
CC.verboseLog(cc.getCallId(),"componentName=" + componentName
+ " is not exists and CC.enableRemoteCC is " + CC.isRemoteCCEnabled());
}
}
}
if (code != 0) {
return CCResult.error(code);
}
//执行完自定义拦截器,并且通过有效性校验后,再确定具体调用组件的方式
if (ComponentManager.hasComponent(componentName)) {
//调用当前进程中的组件
chain.addInterceptor(LocalCCInterceptor.getInstance());
} else {
if (notFoundInCurApp == null) {
notFoundInCurApp = TextUtils.isEmpty(ComponentManager.getComponentProcessName(componentName));
}
if (notFoundInCurApp) {
//调用设备上安装的其它app(组件单独运行的app)中的组件
chain.addInterceptor(RemoteCCInterceptor.getInstance());
} else {
//调用app内部子进程中的组件
chain.addInterceptor(SubProcessCCInterceptor.getInstance());
}
}
chain.addInterceptor(Wait4ResultInterceptor.getInstance());
// 执行上面添加的拦截器,开始执行组件调用
return chain.proceed();
}
}
package com.billy.cc.core.component;
/**
* 等待异步调用CC.sendCCResult(callId, ccResult)
* @author billy.qi
*/
class Wait4ResultInterceptor implements ICCInterceptor {
//-------------------------单例模式 start --------------
/** 单例模式Holder */
private static class Wait4ResultInterceptorHolder {
private static final Wait4ResultInterceptor INSTANCE = new Wait4ResultInterceptor();
}
private Wait4ResultInterceptor (){}
/** 获取Wait4ResultInterceptor的单例对象 */
static Wait4ResultInterceptor getInstance() {
return Wait4ResultInterceptorHolder.INSTANCE;
}
//-------------------------单例模式 end --------------
@Override
public CCResult intercept(Chain chain) {
CC cc = chain.getCC();
cc.wait4Result();
return cc.getResult();
}
}
package com.billy.cc.core.component.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记组件在所有进程内各有一个该组件对象,每个进程调用自身进程内部的对象,从而达到所有进程共用的目的(不会导致跨进程调用)
* @author billy.qi
* @since 18/9/14 23:21
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface AllProcess {
}
package com.billy.cc.core.component.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author billy.qi
* @since 18/6/28 23:00
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SubProcess {
String value() default "";
}
package com.billy.cc.core.component.remote;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import com.billy.cc.core.component.RemoteCCService;
/**
* 封装IBinder
* 用于在{@link RemoteProvider}中通过{@link RemoteCursor}跨进程传递{@link RemoteCCService}对象
* @author billy.qi
*/
public class BinderWrapper implements Parcelable {
private final IBinder binder;
public BinderWrapper(IBinder binder) {
this.binder = binder;
}
public BinderWrapper(Parcel in) {
this.binder = in.readStrongBinder();
}
public IBinder getBinder() {
return binder;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeStrongBinder(binder);
}
public static final Creator<BinderWrapper> CREATOR = new Creator<BinderWrapper>() {
@Override
public BinderWrapper createFromParcel(Parcel source) {
return new BinderWrapper(source);
}
@Override
public BinderWrapper[] newArray(int size) {
return new BinderWrapper[size];
}
};
}
package com.billy.cc.core.component.remote;
import android.os.Parcel;
import android.os.Parcelable;
import com.billy.cc.core.component.CC;
import com.billy.cc.core.component.CCUtil;
import org.json.JSONObject;
import java.util.Map;
import static com.billy.cc.core.component.CCUtil.put;
/**
* 跨进程传递的CC对象
* @author billy.qi
* @since 18/6/24 11:29
*/
public class RemoteCC implements Parcelable {
private Map<String, Object> params;
private String componentName;
private String actionName;
private String callId;
private boolean isMainThreadSyncCall;
private Map<String, Object> localParams;
public RemoteCC(CC cc) {
this(cc, false);
}
public RemoteCC(CC cc, boolean isMainThreadSyncCall) {
this.componentName = cc.getComponentName();
this.actionName = cc.getActionName();
this.callId = cc.getCallId();
this.params = RemoteParamUtil.toRemoteMap(cc.getParams());
this.isMainThreadSyncCall = isMainThreadSyncCall;
}
public Map<String, Object> getParams() {
if (localParams == null) {
localParams = RemoteParamUtil.toLocalMap(params);
}
return localParams;
}
protected RemoteCC(Parcel in) {
componentName = in.readString();
actionName = in.readString();
callId = in.readString();
isMainThreadSyncCall = in.readByte() != 0;
params = in.readHashMap(getClass().getClassLoader());
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(componentName);
dest.writeString(actionName);
dest.writeString(callId);
dest.writeByte((byte) (isMainThreadSyncCall ? 1 : 0));
dest.writeMap(params);
}
@Override
public String toString() {
JSONObject json = new JSONObject();
put(json, "componentName", componentName);
put(json, "actionName", actionName);
put(json, "callId", callId);
put(json, "isMainThreadSyncCall", isMainThreadSyncCall);
put(json, "params", CCUtil.convertToJson(params));
return json.toString();
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<RemoteCC> CREATOR = new Creator<RemoteCC>() {
@Override
public RemoteCC createFromParcel(Parcel in) {
return new RemoteCC(in);
}
@Override
public RemoteCC[] newArray(int size) {
return new RemoteCC[size];
}
};
public String getComponentName() {
return componentName;
}
public void setComponentName(String componentName) {
this.componentName = componentName;
}
public String getActionName() {
return actionName;
}
public void setActionName(String actionName) {
this.actionName = actionName;
}
public String getCallId() {
return callId;
}
public void setCallId(String callId) {
this.callId = callId;
}
public boolean isMainThreadSyncCall() {
return isMainThreadSyncCall;
}
public void setMainThreadSyncCall(boolean mainThreadSyncCall) {
isMainThreadSyncCall = mainThreadSyncCall;
}
}
package com.billy.cc.core.component.remote;
import android.os.Parcel;
import android.os.Parcelable;
import com.billy.cc.core.component.CCResult;
import com.billy.cc.core.component.CCUtil;
import org.json.JSONObject;
import java.util.Map;
import static com.billy.cc.core.component.CCUtil.put;
/**
* 用于跨进程传递的CCResult
* @author billy.qi
* @since 18/6/3 02:22
*/
public class RemoteCCResult implements Parcelable {
private Map<String, Object> data;
private boolean success;
private String errorMessage;
private int code;
public RemoteCCResult(CCResult result) {
setCode(result.getCode());
setErrorMessage(result.getErrorMessage());
setSuccess(result.isSuccess());
data = RemoteParamUtil.toRemoteMap(result.getDataMap());
}
public CCResult toCCResult() {
CCResult result = new CCResult();
result.setCode(getCode());
result.setErrorMessage(getErrorMessage());
result.setSuccess(isSuccess());
result.setDataMap(RemoteParamUtil.toLocalMap(data));
return result;
}
@Override
public String toString() {
JSONObject json = new JSONObject();
put(json, "success", success);
put(json, "code", code);
put(json, "errorMessage", errorMessage);
put(json, "data", CCUtil.convertToJson(data));
return json.toString();
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByte((byte) (success ? 1 : 0));
dest.writeString(errorMessage);
dest.writeInt(code);
dest.writeMap(data);
}
private RemoteCCResult(Parcel in) {
success = in.readByte() != 0;
errorMessage = in.readString();
code = in.readInt();
data = in.readHashMap(getClass().getClassLoader());
}
public static final Creator<RemoteCCResult> CREATOR = new Creator<RemoteCCResult>() {
@Override
public RemoteCCResult createFromParcel(Parcel in) {
return new RemoteCCResult(in);
}
@Override
public RemoteCCResult[] newArray(int size) {
return new RemoteCCResult[size];
}
};
}
package com.billy.cc.core.component.remote;
import android.app.Application;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.SystemClock;
import com.billy.cc.core.component.CC;
import com.billy.cc.core.component.CCUtil;
import java.util.ArrayList;
import java.util.List;
/**
* @author billy.qi
* @since 18/7/3 22:39
*/
public class RemoteConnection {
/**
* 获取当前设备上安装的可供跨app调用组件的App列表
* @return 包名集合
*/
public static List<String> scanComponentApps() {
Application application = CC.getApplication();
String curPkg = application.getPackageName();
PackageManager pm = application.getPackageManager();
// 查询所有已经安装的应用程序
Intent intent = new Intent("action.com.billy.cc.connection");
List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
List<String> packageNames = new ArrayList<>();
for (ResolveInfo info : list) {
ActivityInfo activityInfo = info.activityInfo;
String packageName = activityInfo.packageName;
if (curPkg.equals(packageName)) {
continue;
}
if (tryWakeup(packageName)) {
packageNames.add(packageName);
}
}
return packageNames;
}
/**
* 检测组件App是否存在,并顺便唤醒App
* @param packageName app的包名
* @return 成功与否(true:app存在,false: 不存在)
*/
public static boolean tryWakeup(String packageName) {
long time = SystemClock.elapsedRealtime();
Intent intent = new Intent();
intent.setClassName(packageName, RemoteConnectionActivity.class.getName());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
CC.getApplication().startActivity(intent);
CC.log("wakeup remote app '%s' success. time=%d", packageName, (SystemClock.elapsedRealtime() - time));
return true;
} catch(Exception e) {
CCUtil.printStackTrace(e);
CC.log("wakeup remote app '%s' failed. time=%d", packageName, (SystemClock.elapsedRealtime() - time));
return false;
}
}
}
package com.billy.cc.core.component.remote;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
/**
* 用于跨app探索组件及唤醒app的activity
* @author billy.qi
* @since 18/7/2 23:38
*/
public class RemoteConnectionActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finish();
}
}
package com.billy.cc.core.component.remote;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.Bundle;
import android.os.IBinder;
import com.billy.cc.core.component.RemoteCCService;
/**
* 用于跨进程通信的游标,通过Extras跨进程传递bundle,在bundle中传递IBinder
* @author billy.qi
* @since 18/6/24 11:40
*/
public class RemoteCursor extends MatrixCursor {
private static final String KEY_BINDER_WRAPPER = "BinderWrapper";
static final String[] DEFAULT_COLUMNS = {"cc"};
//-------------------------单例模式 start --------------
/** 单例模式Holder */
private static class CCCursorHolder {
private static final RemoteCursor INSTANCE = new RemoteCursor(DEFAULT_COLUMNS, RemoteCCService.getInstance());
}
private RemoteCursor(String[] columnNames, IBinder binder) {
super(columnNames);
binderExtras.putParcelable(KEY_BINDER_WRAPPER, new BinderWrapper(binder));
}
/** 获取CCCursor在当前进程中的单例对象 */
public static RemoteCursor getInstance() {
return RemoteCursor.CCCursorHolder.INSTANCE;
}
//-------------------------单例模式 end --------------
private Bundle binderExtras = new Bundle();
@Override
public Bundle getExtras() {
return binderExtras;
}
public static IRemoteCCService getRemoteCCService(Cursor cursor) {
if (null == cursor) {
return null;
}
Bundle bundle = cursor.getExtras();
bundle.setClassLoader(BinderWrapper.class.getClassLoader());
BinderWrapper binderWrapper = bundle.getParcelable(KEY_BINDER_WRAPPER);
if (binderWrapper != null) {
IBinder binder = binderWrapper.getBinder();
return IRemoteCCService.Stub.asInterface(binder);
}
return null;
}
}
package com.billy.cc.core.component.remote;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Process;
import com.billy.cc.core.component.CC;
import static android.os.Binder.getCallingUid;
/**
* 通过ContentProvider实现跨进程通信 <br>
* 通信原理: <br>
* 1. 通过ContentProvider的query获取RemoteCursor单例对象 <br>
* 2. 通过RemoteCursor.getExtras()获取bundle <br>
* 3. 通过bundle传递Parcelable <br>
* 4. 通过Parcelable封装IBinder (RemoteCCService) <br>
* 5. 最终跨进程将RemoteCCService对象传递给调用方 <br>
* @author billy.qi
* @since 18/6/24 11:38
*/
public class RemoteProvider extends ContentProvider {
public static final String[] PROJECTION_MAIN = {"cc"};
public static final String URI_SUFFIX = "com.billy.cc.core.remote";
@Override
public boolean onCreate() {
CC.log("RemoteProvider onCreated! class:%s", this.getClass().getName());
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
if (CC.isRemoteCCEnabled() || getCallingUid() == Process.myUid()) {
//获取当前ContentProvider所在进程中的RemoteCursor单例对象
return RemoteCursor.getInstance();
}
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
ext {
android = [
compileSdkVersion: 28,
buildToolsVersion: '28.0.0',
minSdkVersion : 19, //N5收銀機應用安卓開發環境要求Android OS Version: 5.1Android SDK Min Version: 19
targetSdkVersion : 28,
versionCode : 1,
versionName : "1.0"
]
version = [
androidSupportSdkVersion: "28+",
retrofitSdkVersion : "2.3.0",
dagger2SdkVersion : "2.14.1",
glideSdkVersion : "4.5.0",
butterknifeSdkVersion : "8.8.1",
rxlifecycleSdkVersion : "1.0",
rxlifecycle2SdkVersion : "2.2.1",
espressoSdkVersion : "3.0.1",
// fragmentationVersion : "1.1.6",
canarySdkVersion : "1.5.4"
]
dependencies = [
//support
"appcompat-v7" : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
"design" : "com.android.support:design:${version["androidSupportSdkVersion"]}",
"support-v4" : "com.android.support:support-v4:${version["androidSupportSdkVersion"]}",
"cardview-v7" : "com.android.support:cardview-v7:${version["androidSupportSdkVersion"]}",
"annotations" : "com.android.support:support-annotations:${version["androidSupportSdkVersion"]}",
"recyclerview-v7" : "com.android.support:recyclerview-v7:${version["androidSupportSdkVersion"]}",
//network
"retrofit" : "com.squareup.retrofit2:retrofit:${version["retrofitSdkVersion"]}",
"retrofit-converter-gson" : "com.squareup.retrofit2:converter-gson:${version["retrofitSdkVersion"]}",
"retrofit-adapter-rxjava" : "com.squareup.retrofit2:adapter-rxjava:${version["retrofitSdkVersion"]}",
"retrofit-adapter-rxjava2" : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitSdkVersion"]}",
"okhttp3" : "com.squareup.okhttp3:okhttp:3.8.1",
"okhttp-urlconnection" : "com.squareup.okhttp:okhttp-urlconnection:2.0.0",
"glide" : "com.github.bumptech.glide:glide:${version["glideSdkVersion"]}",
"glide-compiler" : "com.github.bumptech.glide:compiler:${version["glideSdkVersion"]}",
"glide-loader-okhttp3" : "com.github.bumptech.glide:okhttp3-integration:${version["glideSdkVersion"]}",
"picasso" : "com.squareup.picasso:picasso:2.5.2",
//view
"autolayout" : "com.zhy:autolayout:1.4.5",
"butterknife" : "com.jakewharton:butterknife:${version["butterknifeSdkVersion"]}",
"butterknife-compiler" : "com.jakewharton:butterknife-compiler:${version["butterknifeSdkVersion"]}",
"pickerview" : "com.contrarywind:Android-PickerView:3.2.5",
"photoview" : "com.github.chrisbanes.photoview:library:1.2.3",
"numberprogressbar" : "com.daimajia.numberprogressbar:library:1.2@aar",
"nineoldandroids" : "com.nineoldandroids:library:2.4.0",
"paginate" : "com.github.markomilos:paginate:0.5.1",
"vlayout" : "com.alibaba.android:vlayout:1.1.0@aar",
//rx1
"rxandroid" : "io.reactivex:rxandroid:1.2.1",
"rxjava" : "io.reactivex:rxjava:1.3.0",
"rxlifecycle" : "com.trello:rxlifecycle:${version["rxlifecycleSdkVersion"]}",
"rxlifecycle-components" : "com.trello:rxlifecycle-components:${version["rxlifecycleSdkVersion"]}",
"rxcache" : "com.github.VictorAlbertos.RxCache:runtime:1.7.0-1.x",
"rxcache-jolyglot-gson" : "com.github.VictorAlbertos.Jolyglot:gson:0.0.3",
"rxbinding-recyclerview-v7": "com.jakewharton.rxbinding:rxbinding-recyclerview-v7:1.0.1",
"rxpermissions" : "com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar",
"rxerrorhandler" : "me.jessyan:rxerrorhandler:1.0.1",
//rx2
"rxandroid2" : "io.reactivex.rxjava2:rxandroid:2.0.1",
"rxjava2" : "io.reactivex.rxjava2:rxjava:2.1.8",
"rxlifecycle2" : "com.trello.rxlifecycle2:rxlifecycle:${version["rxlifecycle2SdkVersion"]}",
"rxlifecycle2-android" : "com.trello.rxlifecycle2:rxlifecycle-android:${version["rxlifecycle2SdkVersion"]}",
"rxlifecycle2-components" : "com.trello.rxlifecycle2:rxlifecycle-components:${version["rxlifecycle2SdkVersion"]}",
"rxcache2" : "com.github.VictorAlbertos.RxCache:runtime:1.8.3-2.x",
"rxpermissions2" : "com.tbruyelle.rxpermissions2:rxpermissions:0.9.5@aar",
"rxerrorhandler2" : "me.jessyan:rxerrorhandler:2.1.1",
//tools
"dagger2" : "com.google.dagger:dagger:${version["dagger2SdkVersion"]}",
"dagger2-android" : "com.google.dagger:dagger-android:${version["dagger2SdkVersion"]}",
"dagger2-android-support" : "com.google.dagger:dagger-android-support:${version["dagger2SdkVersion"]}",
"dagger2-compiler" : "com.google.dagger:dagger-compiler:${version["dagger2SdkVersion"]}",
"dagger2-android-processor": "com.google.dagger:dagger-android-processor:${version["dagger2SdkVersion"]}",
"androideventbus" : "org.simple:androideventbus:1.0.5.1",
"otto" : "com.squareup:otto:1.3.8",
"gson" : "com.google.code.gson:gson:2.8.2",
"multidex" : "com.android.support:multidex:1.0.1",
"javax.annotation" : "javax.annotation:jsr250-api:1.0",
"arouter" : "com.alibaba:arouter-api:1.3.0",
"arouter-compiler" : "com.alibaba:arouter-compiler:1.1.4",
"progressmanager" : "me.jessyan:progressmanager:1.5.0",
"retrofit-url-manager" : "me.jessyan:retrofit-url-manager:1.4.0",
"lifecyclemodel" : "me.jessyan:lifecyclemodel:1.0.1",
// "fragmentation" : "me.yokeyword:fragmentation:${version["fragmentationVersion"]}",
// "fragmentation-core" : "me.yokeyword:fragmentation-core:${version["fragmentationVersion"]}",
// "fragmentation-swipeback" : "me.yokeyword:fragmentation-swipeback:${version["fragmentationVersion"]}",
//test
"junit" : "junit:junit:4.12",
"androidJUnitRunner" : "android.support.test.runner.AndroidJUnitRunner",
"runner" : "com.android.support.test:runner:1.0.1",
"espresso-core" : "com.android.support.test.espresso:espresso-core:${version["espressoSdkVersion"]}",
"espresso-contrib" : "com.android.support.test.espresso:espresso-contrib:${version["espressoSdkVersion"]}",
"espresso-intents" : "com.android.support.test.espresso:espresso-intents:${version["espressoSdkVersion"]}",
"mockito-core" : "org.mockito:mockito-core:1.+",
"timber" : "com.jakewharton.timber:timber:4.6.0",
"logger" : "com.orhanobut:logger:2.1.1",
"canary-debug" : "com.squareup.leakcanary:leakcanary-android:${version["canarySdkVersion"]}",
"canary-release" : "com.squareup.leakcanary:leakcanary-android-no-op:${version["canarySdkVersion"]}",
"umeng-analytics" : "com.umeng.analytics:analytics:6.0.1"
]
}
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.github.dcendents.android-maven'
Properties properties = new Properties()
boolean isHasFile = false
if (project.rootProject.findProject('local.properties') != null){
isHasFile = true
properties.load(project.rootProject.file('local.properties').newDataInputStream())
}
def gitUrl = 'https://github.com/JessYanCoding/MVPArms.git' // Git仓库的url
def siteUrl = 'https://github.com/JessYanCoding/MVPArms' // 项目的主页
version = rootProject.ext.android["versionName"]
group = "me.jessyan"
bintray {
user = isHasFile ? properties.getProperty("bintray.user") : System.getenv("bintray_user")
key = isHasFile ? properties.getProperty("bintray.apikey") : System.getenv("bintray_apikey")
pkg {
repo = 'maven'
name = 'MVPArms'
licenses = ["Apache-2.0"]
websiteUrl = siteUrl
vcsUrl = gitUrl
publish = true // 是否是公开项目。
version {
name = rootProject.ext.android["versionName"]
desc = 'A common Architecture for Android Applications developing based on MVP,integrates many Open Source Projects( like Dagger2,RxJava,Retrofit... ),to make your developing quicker and easier.'
released = new Date()
vcsTag = 'v' + rootProject.ext.android["versionName"]
attributes = ['gradle-plugin': 'com.use.less:com.use.less.gradle:gradle-useless-plugin']
}
}
configurations = ['archives']
}
install {
repositories.mavenInstaller {
// This generates POM.xml with proper parameters
pom {
project {
packaging 'aar'
// Add your description here
name 'MVPArms'
description 'A common Architecture for Android Applications developing based on MVP,integrates many Open Source Projects( like Dagger2,RxJava,Retrofit... ),to make your developing quicker and easier.'
url siteUrl
// Set your license
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id 'JessYanCoding' //填写bintray或者github的用户名
name 'jessyan' //姓名,可以是中文
email 'jess.yan.effort@gmail.com'
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
failOnError false
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
useLibrary 'org.apache.http.legacy'
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
}
buildTypes {
Test {
buildConfigField "boolean", "LOG_DEBUG", "false"
buildConfigField "boolean", "USE_CANARY", "false"
minifyEnabled false
shrinkResources false
zipAlignEnabled false
proguardFiles 'proguard.cfg'
}
debug {
buildConfigField "boolean", "LOG_DEBUG", "true"
buildConfigField "boolean", "USE_CANARY", "true"
minifyEnabled false
proguardFiles 'proguard.cfg'
}
release {
buildConfigField "boolean", "LOG_DEBUG", "false"
buildConfigField "boolean", "USE_CANARY", "false"
minifyEnabled true
// shrinkResources true
zipAlignEnabled true
proguardFiles 'proguard.cfg'
}
}
}
buildscript {
repositories {
jcenter()
}
}
dependencies {
//support
api(rootProject.ext.dependencies["support-v4"]) {
exclude module: 'support-annotations'
}
api(rootProject.ext.dependencies["appcompat-v7"]) {
exclude module: 'support-annotations'
exclude module: 'support-v4'
}
api(rootProject.ext.dependencies["design"]) {
exclude module: 'support-annotations'
exclude module: 'appcompat-v7'
exclude module: 'support-v4'
}
api rootProject.ext.dependencies["annotations"]
//view
api(rootProject.ext.dependencies["autolayout"]) {
exclude group: 'com.android.support'
}
api(rootProject.ext.dependencies["butterknife"]) {
exclude module: 'support-annotations'
}
//rx
api rootProject.ext.dependencies["rxjava2"]
api(rootProject.ext.dependencies["rxandroid2"]) {
exclude module: 'rxjava'
}
api(rootProject.ext.dependencies["rxcache2"]) {
exclude module: 'rxjava'
exclude module: 'dagger'
}
implementation(rootProject.ext.dependencies["rxcache-jolyglot-gson"]) {
exclude module: 'gson'
}
api(rootProject.ext.dependencies["rxlifecycle2"]) {
exclude module: 'rxjava'
exclude module: 'jsr305'
}
api(rootProject.ext.dependencies["rxlifecycle2-android"]) {
exclude module: 'support-annotations'
exclude module: 'rxjava'
exclude module: 'rxandroid'
exclude module: 'rxlifecycle'
}
api(rootProject.ext.dependencies["rxpermissions2"]) {
exclude module: 'rxjava'
exclude module: 'support-annotations'
}
api rootProject.ext.dependencies['rxerrorhandler2']
//network
api(rootProject.ext.dependencies["retrofit"]) {
exclude module: 'okhttp'
exclude module: 'okio'
}
implementation(rootProject.ext.dependencies["retrofit-converter-gson"]) {
exclude module: 'gson'
exclude module: 'okhttp'
exclude module: 'okio'
exclude module: 'retrofit'
}
implementation(rootProject.ext.dependencies["retrofit-adapter-rxjava2"]) {
exclude module: 'rxjava'
exclude module: 'okhttp'
exclude module: 'retrofit'
exclude module: 'okio'
}
api rootProject.ext.dependencies["okhttp3"]
api(rootProject.ext.dependencies["glide"]) {
exclude module: 'support-v4'
}
annotationProcessor(rootProject.ext.dependencies["glide-compiler"]) {
exclude module: 'jsr305'
}
//tools
compileOnly rootProject.ext.dependencies["javax.annotation"]
api rootProject.ext.dependencies["dagger2"]
annotationProcessor rootProject.ext.dependencies["dagger2-compiler"]
api rootProject.ext.dependencies["androideventbus"]
api rootProject.ext.dependencies["gson"]
api rootProject.ext.dependencies["butterknife"]
implementation rootProject.ext.dependencies["multidex"]
implementation rootProject.ext.dependencies["recyclerview-v7"]
implementation rootProject.ext.dependencies["cardview-v7"]
//test
api rootProject.ext.dependencies["timber"]
// testApi rootProject.ext.dependencies["junit"]
}
//apply from: 'bintray.gradle'
\ No newline at end of file
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment