├── .gitignore
├── DEVNOTES.txt
├── LICENSE.txt
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── meta
├── flavor-matrix.gif
├── plugin-repo.png
└── variant-hassle.gif
├── settings.gradle
└── src
├── main
├── java
│ └── com
│ │ └── nilsenlabs
│ │ └── flavormatrix
│ │ └── actions
│ │ ├── AndroidModuleHelper.kt
│ │ ├── Common.kt
│ │ ├── DimensionList.kt
│ │ ├── SelectBuildVariantMatrixAction.kt
│ │ ├── VariantSelectorDialog.kt
│ │ └── old
│ │ ├── BuildVariantSelectionChangeListener.java
│ │ └── LegacyBuildVariantUpdater.java
└── resources
│ └── META-INF
│ └── plugin.xml
└── test
└── java
└── com
└── nilsenlabs
└── flavormatrix
└── DimensionListTest.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | .idea
3 | .gradle
4 | out
5 | local.properties
6 |
--------------------------------------------------------------------------------
/DEVNOTES.txt:
--------------------------------------------------------------------------------
1 | ## Bulding
2 |
3 | run `gradlew buildPlugin`
4 | the `jar` task does not build full jar anymore
5 |
6 | Documentationn:
7 |
8 | https://plugins.jetbrains.com/docs/intellij/android-studio.html#pluginxml
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build Variant Matrix Selector
2 |
3 | A plugin for Android Studio making it easier to select variants / flavors
4 |
5 | ## Introduction
6 |
7 | |Tired of this?|Try this!|
8 | |---|---|
9 | |||
10 |
11 | This plugin replaces the variant selector in Android Studio with a popup that lets you select the
12 | flavors in a matrix instead of using drop down lists. It's much more efficient when working on large projects
13 | in Android Studio.
14 |
15 | Here's what I didn't like about Android Studio's solution and why I chose to build this plugin:
16 |
17 | - If you have multiple leaf modules, there was no
18 | way to select the same flavor for all modules in one operation
19 | - Drop downs lists the options inefficiently*
20 | - Android Studio's conflict resolution didn't always work**
21 |
22 |
23 | \*) While drop downs have to list A x B x C selections, where A, B and C are number of
24 | flavors per dimension, radio buttons can do with A + B + C
25 |
26 | \**) Anecdotal, I haven't investigated deeply.
27 |
28 | ## Installation
29 |
30 | Requirement: Android Studio 4.0 or newer.
31 |
32 | Go to File - Settings - Plugins, Search for plugin "Build Variant Matrix Selector"
33 |
34 | 
35 |
36 | Optionally download and install from disk from either [Jetbrains](https://plugins.jetbrains.com/plugin/15632-build-variant-matrix-selector) or [Gitlab](https://github.com/Nilzor/build-variant-matrix/releases)
37 |
38 | ## Usage
39 |
40 | Go to File - Settings - Keymap - map a key to the action "Select Build Variant by Matrix". Start clicking the button after a proect has loaded, simple as that!
41 |
42 | ## Known limitations
43 |
44 | - Not currently supporting modules that have incomplete dimension configuration.
45 | If you have flavor A, B & C for dimension X, you need to have flavor A, B & C in all modules.
46 |
47 | ## Acknowledgements
48 |
49 | Thants to PandoraMedia's [variant-helper-plugin](https://github.com/PandoraMedia/variant-helper-plugin) for inspiration
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | maven { url 'https://www.jetbrains.com/intellij-repository/releases' }
5 | }
6 | }
7 |
8 | plugins {
9 | id 'org.jetbrains.kotlin.jvm' version '2.0.0'
10 | id 'org.jetbrains.intellij.platform' version '2.2.1'
11 | }
12 |
13 | group 'com.nilsenlabs.flavormatrix'
14 | version '1.4.2'
15 |
16 | Properties properties = new Properties()
17 | properties.load(project.rootProject.file("local.properties").newDataInputStream())
18 |
19 | // Any developer must change this to his/her installation folder
20 | // of Android Studio of the targetIdeVersion for which to build for
21 | // When updating, delete all source jar references manually from Project Structure > Libraries that reference
22 | // the old version of Android Studio. E.g::
23 | // plugins\properties\lib\platform-images.jar
24 | // plugins\properties\lib\properties.jar
25 | // plugins\smali\lib\smali.jar
26 | def defaultLocalStudioPath = 'd:/coding/android-studio-meerkat'
27 |
28 | // Specify the path in your local.properties file as
29 | // localStudioPath=your/path/to/androidstudio
30 | // Also see https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-localpath
31 | def localStudioPath = properties.get("localStudioPath") ?: defaultLocalStudioPath
32 |
33 | // Below: Meerkat 2024.3.1 Canary 9. Remember to update plugin.xml
34 | def targetIdeVersion = "243.22562.218"
35 |
36 |
37 | patchPluginXml {
38 | changeNotes = """
39 |
40 | - 1.4.2 Publishing technicalities - no functionality change
41 | - 1.4.0 Support for Android Studio Meerkat (from Canary 9)
42 | - 1.3.0 Support for Android Studio Ladybug
43 | - 1.2.0 Support for Android Studio Giraffe and Hedgehog
44 | - 1.1.0 Support for Android Studio Electric Eel
45 | - 1.0.0 Support for Android Studio Chipmunk (RC2+)
46 | - 0.7.0 Support for Android studio Arctic Fox 2020.3.1
47 |
48 | """
49 | }
50 |
51 | repositories {
52 | mavenLocal()
53 | mavenCentral()
54 | intellijPlatform {
55 | defaultRepositories()
56 | }
57 | maven { url 'https://www.jetbrains.com/intellij-repository/releases' }
58 | }
59 |
60 | dependencies {
61 | testImplementation group: 'junit', name: 'junit', version: '4.12'
62 | testImplementation "org.mockito:mockito-core:2.+"
63 | intellijPlatform {
64 | local(localStudioPath)
65 | //androidStudio("$targetIdeVersion") // If you want IntelliJ to download AS. instead of "local"
66 | bundledPlugin('com.intellij.gradle')
67 | plugin("org.jetbrains.android:$targetIdeVersion")
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nilzor/build-variant-matrix/b1689aeba275f4b400f1bd2dc7ee5e2fa53bc527/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jun 03 08:21:31 EDT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/meta/flavor-matrix.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nilzor/build-variant-matrix/b1689aeba275f4b400f1bd2dc7ee5e2fa53bc527/meta/flavor-matrix.gif
--------------------------------------------------------------------------------
/meta/plugin-repo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nilzor/build-variant-matrix/b1689aeba275f4b400f1bd2dc7ee5e2fa53bc527/meta/plugin-repo.png
--------------------------------------------------------------------------------
/meta/variant-hassle.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nilzor/build-variant-matrix/b1689aeba275f4b400f1bd2dc7ee5e2fa53bc527/meta/variant-hassle.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'build-variant-matrix'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/com/nilsenlabs/flavormatrix/actions/AndroidModuleHelper.kt:
--------------------------------------------------------------------------------
1 | package com.nilsenlabs.flavormatrix.actions
2 |
3 | import com.android.tools.idea.gradle.project.model.GradleAndroidModel
4 | import com.intellij.openapi.module.Module
5 |
6 | object AndroidModuleHelper {
7 | fun createDimensionTable(androidModules: List, modules: Array): DimensionList {
8 | val dimensions = createMergedDimensionList(androidModules)
9 | addBuildTypes(modules, dimensions)
10 | dimensions.createOrderedDimensionMaps(androidModules)
11 | dimensions.selectFrom(androidModules)
12 | dimensions.deselectDuplicates()
13 | return dimensions
14 | }
15 |
16 | private fun addBuildTypes(modules: Array, dimensions: DimensionList) {
17 | dimensions.dimensions.add(
18 | Dimension(DimensionList.BUILD_TYPE_NAME).also { dim ->
19 | getBuildTypes(modules).forEach {
20 | dim.addUniqueVariant(it)
21 | }
22 | }
23 | )
24 | }
25 |
26 | /**
27 | * Fills the list with a merged set of dimensions for all modules
28 | */
29 | private fun createMergedDimensionList(modules: List): DimensionList {
30 | val dimensionList = DimensionList()
31 | for (module in modules) {
32 | val flavors = module.androidProject.multiVariantData?.productFlavors?.toList() ?: continue
33 | for (flavorObj in flavors) {
34 | // getLog.info("Processing flavorName: ${flavorObj.productFlavor.name}: ${flavorObj.productFlavor.dimension}")
35 | /*
36 | Expecting a log like like this for my test project:
37 | Processing flavorName: qa: environment
38 | Processing flavorName: prod: environment
39 | Processing flavorName: dev: environment
40 | Processing flavorName: paid: paymentmodel
41 | Processing flavorName: free: paymentmodel
42 | */
43 | val flavor = flavorObj.productFlavor
44 | flavor.dimension?.let { dim ->
45 | val flavorsForDimension = dimensionList.getOrCreateDimension(dim)
46 | flavorsForDimension.addUniqueVariant(flavor.name)
47 | }
48 | }
49 | }
50 | return dimensionList
51 | }
52 |
53 | fun getBuildTypes(modules: Array): List {
54 | val buildTypes = mutableSetOf()
55 | for (module in modules) {
56 | val amod = GradleAndroidModel.get(module)
57 | buildTypes.addAll(amod?.buildTypeNames ?: emptyList())
58 | }
59 | val list = buildTypes.stream().toList()
60 | getLog().info("Build type list resolved after iterating ${modules.size} modules: $list")
61 | return list
62 | }
63 | }
64 |
65 |
66 | val Module.variantNames: Collection
67 | // Note: NDK part is untested
68 | get() = // NdkModuleModel.get(this)?.ndkModel?.allVariantAbis?.stream()?.map { it.displayName }?.toList() ?:
69 | // val variantList = module.androidProject.basicVariants.stream().toList()
70 | GradleAndroidModel.get(this)?.filteredVariantNames ?: emptyList()
71 |
72 | val Module.variantItems: ModuleBuildVariant
73 | get() = ModuleBuildVariant(name, variantNames.asSequence().filterNotNull().sorted().toList())
74 |
75 | data class ModuleBuildVariant(val moduleName: String, val buildVariants: List)
--------------------------------------------------------------------------------
/src/main/java/com/nilsenlabs/flavormatrix/actions/Common.kt:
--------------------------------------------------------------------------------
1 | package com.nilsenlabs.flavormatrix.actions
2 |
3 | import com.intellij.openapi.diagnostic.Logger
4 |
5 | fun getLog(): Logger {
6 | return Logger.getInstance(SelectBuildVariantMatrixAction::class.java)
7 | }
--------------------------------------------------------------------------------
/src/main/java/com/nilsenlabs/flavormatrix/actions/DimensionList.kt:
--------------------------------------------------------------------------------
1 | package com.nilsenlabs.flavormatrix.actions
2 |
3 | import com.android.tools.idea.gradle.project.model.GradleAndroidModel
4 | import kotlin.streams.toList
5 |
6 | class DimensionList {
7 | companion object {
8 | val BUILD_TYPE_NAME = "buildType"
9 |
10 | fun flavorsFromVariant(variantName: String): List {
11 | return variantName
12 | .split(Regex("(?=[A-Z])"))
13 | .toList()
14 | .map { it.toLowerCase() }
15 | }
16 | }
17 |
18 | /**
19 | * All dimensions from all modulese merged
20 | */
21 | val dimensions = mutableListOf()
22 |
23 | /**
24 | * Ordered list om dimensions per module
25 | */
26 | val moduleOrderedDimensionMap = mutableMapOf>()
27 |
28 | fun getOrCreateDimension(dimensionName: String): Dimension {
29 | return dimensions.firstOrNull { it.dimensionName == dimensionName} ?: Dimension(dimensionName).also {
30 | dimensions.add(it)
31 | }
32 | }
33 |
34 | fun getDimensionForFlavor(flavorName: String, selected: Boolean): Dimension? {
35 | val flavorSelectable = SelectableString(flavorName, selected)
36 | return dimensions.firstOrNull { it.flavors.contains(flavorSelectable) }
37 | }
38 |
39 | fun selectFrom(androidModules: List) {
40 | for (module in androidModules) {
41 | for (selectedFlavor in module.selectedVariant.productFlavors) {
42 | getFlavorByName(selectedFlavor)?.isSelected = true
43 | }
44 | val selectedBuild = module.selectedVariant.buildType
45 | getDimensionByName(BUILD_TYPE_NAME)?.flavors?.
46 | firstOrNull { it.title == selectedBuild }?.isSelected = true
47 |
48 | }
49 | }
50 |
51 | /** Generates a selectable variant string for the given module based on the selected items */
52 | fun getSelectedVariantFor(moduleName: String): String? {
53 | val dimensionList: MutableList = moduleOrderedDimensionMap[moduleName] ?: return null
54 | try {
55 | val variantString = dimensionList.joinToString("") {
56 | it.flavors
57 | .first { it.isSelected }
58 | .title.capitalize() // Lowercase first char
59 | }
60 | return variantString[0].toLowerCase().toString() + variantString.subSequence(1, variantString.length)
61 | } catch (ex: NoSuchElementException) {
62 | return null
63 | }
64 | }
65 |
66 | /** Make a map of Module => Ordered List Of Dimensions, where order
67 | * matches what Android Studio lists when string concatenating
68 | */
69 | fun createOrderedDimensionMaps(modules: List) {
70 | for (module in modules) {
71 | val variantList = module.androidProject.basicVariants.stream().toList()
72 | variantList.firstOrNull()?.let { firstVariant ->
73 | // The (first) named variant is always sorted the same way we need to sort the output
74 | // e.g. "alphaBravoCharlie" means the dimension for "alpha" always must come first
75 | val orderedFlavors = flavorsFromVariant(firstVariant.name)
76 | val dimensionsForFlavor = mutableListOf()
77 | moduleOrderedDimensionMap[module.moduleName] = dimensionsForFlavor
78 | for (flavor in orderedFlavors) {
79 | getDimensionForFlavor(flavor, false)?.let { dimension ->
80 | dimensionsForFlavor.add(dimension)
81 | }
82 | }
83 | }
84 | }
85 | }
86 |
87 | private fun getDimensionByName(name: String): Dimension? {
88 | return dimensions.firstOrNull { it.dimensionName == name }
89 | }
90 |
91 | private fun getFlavorByName(name: String): SelectableString? {
92 | return dimensions
93 | .flatMap { it.flavors }
94 | .firstOrNull { it.title == name }
95 | }
96 |
97 | fun deselectDuplicates() {
98 | for (dimension in dimensions) {
99 | val selectedFlavors = dimension.flavors.count { it.isSelected }
100 | if (selectedFlavors > 1) {
101 | dimension.flavors.forEach { it.isSelected = false }
102 | }
103 | }
104 | }
105 | }
106 |
107 | data class Dimension(
108 | val dimensionName: String,
109 | val flavors: MutableList = mutableListOf()
110 | ) {
111 | fun addUniqueVariant(variant: String) {
112 | val selString = SelectableString(variant, false)
113 | if (!flavors.contains(selString)) flavors.add(selString)
114 | }
115 |
116 | fun selectUniqueFlavor(text: String) {
117 | flavors.forEach { it.isSelected = false }
118 | flavors.firstOrNull { it.title == text }?.isSelected = true
119 | }
120 | }
121 |
122 | data class SelectableString(
123 | val title: String,
124 | var isSelected: Boolean = false
125 | )
--------------------------------------------------------------------------------
/src/main/java/com/nilsenlabs/flavormatrix/actions/SelectBuildVariantMatrixAction.kt:
--------------------------------------------------------------------------------
1 | package com.nilsenlabs.flavormatrix.actions
2 |
3 | import com.android.tools.idea.gradle.project.model.GradleAndroidModel
4 | import com.intellij.openapi.actionSystem.AnAction
5 | import com.intellij.openapi.actionSystem.AnActionEvent
6 | import com.intellij.openapi.diagnostic.Logger
7 | import com.intellij.openapi.module.Module
8 | import com.intellij.openapi.module.ModuleManager
9 | import com.nilsenlabs.flavormatrix.actions.old.LegacyBuildVariantUpdater
10 |
11 | class SelectBuildVariantMatrixAction : AnAction() {
12 | override fun actionPerformed(e: AnActionEvent) {
13 | val project = e.project ?: return
14 |
15 | val legacyUpdater = LegacyBuildVariantUpdater()
16 | val moduleManager = ModuleManager.getInstance(project)
17 |
18 | val unfilteredModuleList = moduleManager.modules
19 | .map { module ->
20 | GradleAndroidModel.get(module)?.let { Pair(it, module) }
21 | }
22 | .filterNotNull()
23 |
24 | getLog().info("Module list before filtering:")
25 | for (pair in unfilteredModuleList) {
26 | getLog().info("AndroidModule: ${pair.first.moduleName} -> PlainModule: ${pair.second.name}")
27 | }
28 |
29 | val androidModulePairs: Map = unfilteredModuleList
30 | .filter { it.first.moduleName.lowercase() == it.second.name.lowercase() }
31 | .distinct()
32 | .toMap()
33 | val androidModules = androidModulePairs.keys.filterNotNull()
34 |
35 | getLog().info("Module list after filtering:")
36 | for (pair in androidModulePairs) {
37 | getLog().info("AndroidModule: ${pair.key?.moduleName} -> PlainModule: ${pair.value.name}")
38 | }
39 |
40 | val dimensions = AndroidModuleHelper.createDimensionTable(androidModules, moduleManager.modules)
41 |
42 | getLog().info("Staring variant update...")
43 | val dialog = VariantSelectorDialog(dimensions, androidModules, project)
44 | if (dialog.showAndGet()) {
45 | // OK selected => Post variant selection back to Android Studio
46 | for (modulePair in androidModulePairs) {
47 | val andModule = modulePair.key ?: continue // it being null should not happen
48 | dimensions.getSelectedVariantFor(andModule.moduleName)?.let { vari ->
49 | getLog().info("Module: ${andModule.moduleName}. Updating to variant $vari")
50 | legacyUpdater.updateSelectedBuildVariant(project, modulePair.value, vari)
51 | }
52 | }
53 | LegacyBuildVariantUpdater.requestGradleSync(project)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/com/nilsenlabs/flavormatrix/actions/VariantSelectorDialog.kt:
--------------------------------------------------------------------------------
1 | package com.nilsenlabs.flavormatrix.actions
2 |
3 | import com.android.tools.idea.gradle.project.model.GradleAndroidModel
4 | import com.intellij.openapi.project.Project
5 | import com.intellij.openapi.ui.DialogWrapper
6 | import com.intellij.openapi.ui.VerticalFlowLayout
7 | import com.intellij.openapi.util.SystemInfo
8 | import java.awt.Color
9 | import java.awt.Font
10 | import java.awt.GridBagConstraints
11 | import java.awt.GridBagConstraints.WEST
12 | import java.awt.GridBagLayout
13 | import javax.swing.*
14 | import javax.swing.border.LineBorder
15 |
16 |
17 | class VariantSelectorDialog(
18 | private val dimensions: DimensionList,
19 | private val androidModules: List,
20 | project: Project
21 | ) : DialogWrapper(project) {
22 | init {
23 | this.title = "Select variant"
24 | if (!SystemInfo.isMac) {
25 | setButtonsAlignment(SwingConstants.CENTER)
26 | }
27 | init()
28 | }
29 |
30 | override fun createCenterPanel(): JComponent? {
31 | return createLayouts()
32 | }
33 |
34 | /**
35 | * Create the flavor-dimension selection grid
36 | */
37 | private fun createLayouts() : JComponent {
38 | val container = JPanel(
39 | VerticalFlowLayout(VerticalFlowLayout.TOP or VerticalFlowLayout.LEFT, true, false)
40 | )
41 | container.maximumSize = java.awt.Dimension(500, 300)
42 |
43 | val selectorContainer = JPanel(GridBagLayout())
44 | container.add(selectorContainer)
45 | val moduleListTextArea = JTextArea()
46 |
47 | for ((x, dimension) in dimensions.dimensions.withIndex()) {
48 |
49 | val constForLabel = GridBagConstraints().apply {
50 | gridx = x
51 | gridy = 0
52 | ipady = 8
53 | ipadx = 16
54 | anchor = WEST
55 | }
56 |
57 | val dimTitle = JTextField(dimension.dimensionName)
58 | dimTitle.font = Font(dimTitle.font.name, Font.BOLD, dimTitle.font.size)
59 | dimTitle.isEditable = false
60 | dimTitle.background = Color(0,0,0,0)
61 | dimTitle.border = LineBorder(Color.BLACK,0)
62 | selectorContainer.add(dimTitle, constForLabel)
63 |
64 | val rbGroup = ButtonGroup()
65 |
66 | for ((y, flavor) in dimension.flavors.withIndex()) {
67 | val constForButton = GridBagConstraints().apply {
68 | gridx = x
69 | gridy = y + 1
70 | anchor = WEST
71 | ipadx = 12
72 | ipady = 8
73 | }
74 |
75 | val flavorView = JRadioButton(flavor.title)
76 | flavorView.isSelected = flavor.isSelected
77 | selectorContainer.add(flavorView, constForButton)
78 | rbGroup.add(flavorView)
79 | flavorView.addActionListener {
80 | onRadioButtonChecked(dimension, flavorView, moduleListTextArea)
81 | }
82 | }
83 | }
84 | val allFlavorsSelected = populateVariantResult(moduleListTextArea)
85 | enableButtons(allFlavorsSelected)
86 | container.add(moduleListTextArea)
87 |
88 | selectorContainer.doLayout()
89 | return container
90 | }
91 |
92 | private fun populateVariantResult(variantResultList: JTextArea): Boolean {
93 | var text = ""
94 | var anyNull = false
95 | for (module in androidModules) {
96 | val variantName = dimensions.getSelectedVariantFor(module.moduleName)
97 | if (variantName == null) anyNull = true
98 | text += "${module.moduleName}: $variantName\n"
99 | }
100 | variantResultList.text = text
101 | return !anyNull
102 | }
103 |
104 | /**
105 | * Update the data model instantly each time a selection is made
106 | */
107 | private fun onRadioButtonChecked(dimension: Dimension, button: JRadioButton, variantResultList: JTextArea) {
108 | if (button.isSelected) {
109 | dimension.selectUniqueFlavor(button.text)
110 | }
111 | val allFlavorsSelected = populateVariantResult(variantResultList)
112 | enableButtons(allFlavorsSelected)
113 | }
114 |
115 | fun enableButtons(allFlavorsSelected: Boolean) {
116 | isOKActionEnabled = allFlavorsSelected
117 | }
118 | }
--------------------------------------------------------------------------------
/src/main/java/com/nilsenlabs/flavormatrix/actions/old/BuildVariantSelectionChangeListener.java:
--------------------------------------------------------------------------------
1 | package com.nilsenlabs.flavormatrix.actions.old;
2 |
3 | public interface BuildVariantSelectionChangeListener {
4 | void selectionChanged();
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/nilsenlabs/flavormatrix/actions/old/LegacyBuildVariantUpdater.java:
--------------------------------------------------------------------------------
1 | //
2 | // Source code recreated from a .class file by IntelliJ IDEA
3 | // (powered by FernFlower decompiler)
4 | //
5 |
6 | package com.nilsenlabs.flavormatrix.actions.old;
7 |
8 | import com.android.tools.idea.gradle.project.facet.ndk.NdkFacet;
9 | import com.android.tools.idea.gradle.project.model.NdkModuleModel;
10 | import com.android.tools.idea.gradle.project.model.VariantAbi;
11 | import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
12 | import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
13 | import com.android.tools.idea.gradle.project.sync.GradleSyncState;
14 | import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker.Request;
15 | import com.android.tools.idea.gradle.project.sync.GradleSyncStateHolder;
16 | import com.android.tools.idea.gradle.project.sync.idea.AndroidGradleProjectResolver;
17 | import com.android.tools.idea.gradle.project.sync.idea.GradleSyncExecutor;
18 | import com.android.tools.idea.gradle.project.sync.idea.VariantAndAbi;
19 | import com.android.tools.idea.gradle.project.sync.idea.VariantSwitcher;
20 | import com.android.tools.idea.projectsystem.gradle.GradleProjectPath;
21 | import com.google.wireless.android.sdk.stats.GradleSyncStats.Trigger;
22 | import com.intellij.openapi.application.Application;
23 | import com.intellij.openapi.application.ApplicationManager;
24 | import com.intellij.openapi.diagnostic.Logger;
25 | import com.intellij.openapi.externalSystem.model.DataNode;
26 | import com.intellij.openapi.externalSystem.model.ExternalProjectInfo;
27 | import com.intellij.openapi.externalSystem.model.project.ProjectData;
28 | import com.intellij.openapi.externalSystem.service.project.ProjectDataManager;
29 | import com.intellij.openapi.module.Module;
30 | import com.intellij.openapi.module.ModuleManager;
31 | import com.intellij.openapi.progress.EmptyProgressIndicator;
32 | import com.intellij.openapi.progress.ProgressIndicator;
33 | import com.intellij.openapi.progress.ProgressManager;
34 | import com.intellij.openapi.progress.Task.Backgroundable;
35 | import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
36 | import com.intellij.openapi.project.Project;
37 | import com.intellij.openapi.ui.Messages;
38 | import com.intellij.util.ThreeState;
39 | import com.intellij.util.containers.ContainerUtil;
40 | import java.util.Iterator;
41 | import java.util.List;
42 | import java.util.Map;
43 | import java.util.Objects;
44 | import org.jetbrains.android.facet.AndroidFacet;
45 | import org.jetbrains.annotations.NotNull;
46 | import org.jetbrains.annotations.Nullable;
47 | import org.jetbrains.plugins.gradle.util.GradleConstants;
48 |
49 | // Copied from android-studio-2021.2.1.13 \plugins\android\lib\android.jar\com\android\tools\idea\gradle\
50 | public class LegacyBuildVariantUpdater {
51 | @NotNull
52 | private final List mySelectionChangeListeners = ContainerUtil.createLockFreeCopyOnWriteList();
53 |
54 |
55 | public LegacyBuildVariantUpdater() {
56 | }
57 |
58 | void addSelectionChangeListener(@NotNull BuildVariantSelectionChangeListener listener) {
59 | this.mySelectionChangeListeners.add(listener);
60 | }
61 |
62 | public boolean updateSelectedBuildVariant(@NotNull Project project, Module moduleToUpdate, @NotNull String selectedBuildVariant) {
63 | String moduleName = moduleToUpdate.getName();
64 | NdkModuleModel ndkModuleModel = getNdkModelIfItHasNativeVariantAbis(moduleToUpdate);
65 | NdkFacet ndkFacet = NdkFacet.getInstance(moduleToUpdate);
66 | if (ndkModuleModel != null && ndkFacet != null) {
67 | // NDK
68 | VariantAbi newVariantAbi = resolveNewVariantAbi(ndkFacet, ndkModuleModel, selectedBuildVariant, (String)null);
69 | if (newVariantAbi == null) {
70 | logAndShowBuildVariantFailure(String.format("Cannot find suitable ABI for native module '%1$s'.", moduleName));
71 | return false;
72 | } else {
73 | return this.updateSelectedVariant(project, moduleToUpdate, VariantAndAbi.fromVariantAbi(newVariantAbi));
74 | }
75 | } else {
76 | // Android
77 | return this.updateSelectedVariant(project, moduleToUpdate, new VariantAndAbi(selectedBuildVariant, (String)null));
78 | }
79 | }
80 |
81 | public boolean updateSelectedAbi(@NotNull Project project, @NotNull String moduleName, @NotNull String selectedAbiName) {
82 | Module moduleToUpdate = findModule(project, moduleName);
83 | if (moduleToUpdate == null) {
84 | logAndShowAbiNameFailure(String.format("Cannot find module '%1$s'.", moduleName));
85 | return false;
86 | } else {
87 | NdkModuleModel ndkModuleModel = getNdkModelIfItHasNativeVariantAbis(moduleToUpdate);
88 | NdkFacet ndkFacet = NdkFacet.getInstance(moduleToUpdate);
89 | if (ndkModuleModel != null && ndkFacet != null) {
90 | VariantAbi currentSelectedVariantAbi = ndkFacet.getSelectedVariantAbi();
91 | VariantAbi newVariantAbi;
92 | if (currentSelectedVariantAbi == null) {
93 | newVariantAbi = null;
94 | } else {
95 | newVariantAbi = resolveNewVariantAbi(ndkFacet, ndkModuleModel, currentSelectedVariantAbi.getVariant(), selectedAbiName);
96 | }
97 |
98 | if (newVariantAbi == null) {
99 | logAndShowAbiNameFailure(String.format("Cannot find suitable ABI for native module '%1$s'.", moduleName));
100 | return false;
101 | } else {
102 | return this.updateSelectedVariant(project, moduleToUpdate, VariantAndAbi.fromVariantAbi(newVariantAbi));
103 | }
104 | } else {
105 | logAndShowAbiNameFailure(String.format("Cannot find native module model '%1$s'.", moduleName));
106 | return false;
107 | }
108 | }
109 | }
110 |
111 | private boolean updateSelectedVariant(@NotNull Project project, @NotNull Module module, @NotNull VariantAndAbi variantAndAbi) {
112 | if (!findAndUpdateAffectedFacets(module, variantAndAbi)) {
113 | return false;
114 | } else {
115 | ExternalProjectInfo data = ProjectDataManager.getInstance().getExternalProjectData(project, GradleConstants.SYSTEM_ID, project.getBasePath());
116 | // Commented out by Nilzor due to broken API in Electric Eel
117 | //Map variantsExpectedAfterSwitch = (Boolean)StudioFlags.GRADLE_SYNC_ENABLE_CACHED_VARIANTS.get() ? VariantSwitcher.computeExpectedVariantsAfterSwitch(module, variantAndAbi, data) : null;
118 | Map variantsExpectedAfterSwitch = null;
119 | Runnable invokeVariantSelectionChangeListeners = () -> {
120 | Iterator var1 = this.mySelectionChangeListeners.iterator();
121 |
122 | while(var1.hasNext()) {
123 | BuildVariantSelectionChangeListener listener = (BuildVariantSelectionChangeListener)var1.next();
124 | listener.selectionChanged();
125 | }
126 |
127 | };
128 | if (GradleSyncState.getInstance(project).isSyncNeeded().equals(ThreeState.YES)) {
129 | //requestGradleSync(project, module, invokeVariantSelectionChangeListeners);
130 | return true;
131 | } else {
132 | if (variantsExpectedAfterSwitch != null) {
133 | DataNode variantProjectDataNode = VariantSwitcher.findAndSetupSelectedCachedVariantData(data, variantsExpectedAfterSwitch);
134 | if (variantProjectDataNode != null) {
135 | setupCachedVariant(project, variantProjectDataNode, invokeVariantSelectionChangeListeners);
136 | return true;
137 | }
138 | }
139 |
140 | AndroidGradleProjectResolver.saveCurrentlySyncedVariantsForReuse(project);
141 | //requestGradleSync(project, module, invokeVariantSelectionChangeListeners);
142 | return true;
143 | }
144 | }
145 | }
146 |
147 | private static boolean findAndUpdateAffectedFacets(@NotNull Module moduleToUpdate, @NotNull VariantAndAbi variantToSelect) {
148 | AndroidFacet androidFacet = AndroidFacet.getInstance(moduleToUpdate);
149 | if (androidFacet == null) {
150 | throw new IllegalStateException(String.format("Cannot update the selected build variant. Module: %s Variant: %s", moduleToUpdate, variantToSelect));
151 | } else {
152 | NdkFacet ndkFacet = NdkFacet.getInstance(moduleToUpdate);
153 | VariantAbi selectedVariantAbi = ndkFacet != null ? ndkFacet.getSelectedVariantAbi() : null;
154 | String selectedAbi = selectedVariantAbi != null ? selectedVariantAbi.getAbi() : null;
155 | if (Objects.equals(variantToSelect.getVariant(), androidFacet.getProperties().SELECTED_BUILD_VARIANT) && Objects.equals(variantToSelect.getAbi(), selectedAbi)) {
156 | return false;
157 | } else {
158 | String variantName = variantToSelect.getVariant();
159 | if (ndkFacet != null) {
160 | NdkModuleModel ndkModuleModel = getNdkModelIfItHasNativeVariantAbis(ndkFacet);
161 | if (ndkModuleModel != null) {
162 | VariantAbi variantAbiToSelect = variantToSelect.toVariantAbi();
163 | if (variantAbiToSelect != null) {
164 | ndkFacet.setSelectedVariantAbi(variantAbiToSelect);
165 | }
166 | }
167 | }
168 |
169 | androidFacet.getProperties().SELECTED_BUILD_VARIANT = variantName;
170 | return true;
171 | }
172 | }
173 | }
174 |
175 | @Nullable
176 | private static VariantAbi resolveNewVariantAbi(@NotNull NdkFacet ndkFacet, @NotNull NdkModuleModel ndkModel, @NotNull String newVariant, @Nullable String userSelectedAbi) {
177 | VariantAbi selectedVariantAbi;
178 | if (userSelectedAbi != null) {
179 | selectedVariantAbi = new VariantAbi(newVariant, userSelectedAbi);
180 | if (ndkModel.getAllVariantAbis().contains(selectedVariantAbi)) {
181 | return selectedVariantAbi;
182 | }
183 | }
184 |
185 | selectedVariantAbi = ndkFacet.getSelectedVariantAbi();
186 | if (selectedVariantAbi == null) {
187 | return null;
188 | } else {
189 | String existingAbi = selectedVariantAbi.getAbi();
190 | return new VariantAbi(newVariant, existingAbi);
191 | }
192 | }
193 |
194 | @NotNull
195 | private static GradleSyncListener getSyncListener(@NotNull Runnable variantSelectionChangeListeners) {
196 | return new GradleSyncListener() {
197 | public void syncFailed(@NotNull Project project, @NotNull String errorMessage) {
198 | AndroidGradleProjectResolver.clearVariantsSavedForReuse(project);
199 | variantSelectionChangeListeners.run();
200 | }
201 |
202 | public void syncSucceeded(@NotNull Project project) {
203 | AndroidGradleProjectResolver.clearVariantsSavedForReuse(project);
204 | variantSelectionChangeListeners.run();
205 | }
206 |
207 | public void syncSkipped(@NotNull Project project) {
208 | if (project.getUserData(GradleSyncExecutor.ALWAYS_SKIP_SYNC) == null) {
209 | throw new IllegalStateException("Sync cannot complete with syncSkipped result when switching variants.");
210 | } else {
211 | AndroidGradleProjectResolver.clearVariantsSavedForReuse(project);
212 | variantSelectionChangeListeners.run();
213 | }
214 | }
215 | };
216 | }
217 |
218 | private static void requestGradleSync(@NotNull Project project, @NotNull Module module, @NotNull Runnable variantSelectionChangeListeners) {
219 | // Commented out by be, Nilzor because of broken APIs. Unsure of consequences.
220 | /*
221 | String moduleId = AndroidGradleProjectResolver.getModuleIdForModule(module);
222 | if (moduleId != null) {
223 | project.putUserData(MODULE_WITH_BUILD_VARIANT_SWITCHED_FROM_UI, moduleId);
224 | }
225 | */
226 |
227 | Request request = new Request(Trigger.TRIGGER_VARIANT_SELECTION_CHANGED_BY_USER);
228 | GradleSyncInvoker.getInstance().requestProjectSync(project, request, getSyncListener(variantSelectionChangeListeners));
229 | }
230 |
231 | public static void requestGradleSync(@NotNull Project project) {
232 | Request request = new Request(Trigger.TRIGGER_VARIANT_SELECTION_CHANGED_BY_USER);
233 | GradleSyncInvoker.getInstance().requestProjectSync(project, request, getSyncListener(new Runnable() {
234 | @Override
235 | public void run() {
236 |
237 | }
238 | }) );
239 | }
240 |
241 | private static void setupCachedVariant(@NotNull Project project, @NotNull DataNode variantData, @NotNull Runnable variantSelectionChangeListeners) {
242 | final Application application = ApplicationManager.getApplication();
243 | Backgroundable task = new Backgroundable(project, "Setting up Project", false) {
244 | public void run(@NotNull ProgressIndicator indicator) {
245 | LegacyBuildVariantUpdater.getLog().info("Starting setup of cached variant");
246 | VariantSwitcher.switchVariant(project, variantData);
247 |
248 | GradleSyncStateHolder.getInstance(project).syncSkipped(null);
249 | // Commented out by Nilzor due to broken API in Electric Eel. Function not in use anyhow:
250 | //GradleSyncState.getInstance(project).syncSkipped((GradleSyncListener)null);
251 | if (application.isUnitTestMode()) {
252 | variantSelectionChangeListeners.run();
253 | } else {
254 | application.invokeLater(variantSelectionChangeListeners);
255 | }
256 |
257 | LegacyBuildVariantUpdater.getLog().info("Finished setup of cached variant");
258 | }
259 | };
260 | if (application.isUnitTestMode()) {
261 | task.run(new EmptyProgressIndicator());
262 | } else {
263 | ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, new BackgroundableProcessIndicator(task));
264 | }
265 |
266 | }
267 |
268 | @Nullable
269 | private static Module findModule(@NotNull Project project, @NotNull String moduleName) {
270 | ModuleManager moduleManager = ModuleManager.getInstance(project);
271 | return moduleManager.findModuleByName(moduleName);
272 | }
273 |
274 | @Nullable
275 | private static NdkModuleModel getNdkModelIfItHasNativeVariantAbis(@NotNull NdkFacet facet) {
276 | NdkModuleModel ndkModuleModel = NdkModuleModel.get(facet);
277 | if (ndkModuleModel == null) {
278 | logAndShowBuildVariantFailure(String.format("Cannot find NativeAndroidProject for module '%1$s'.", facet.getModule().getName()));
279 | return null;
280 | } else {
281 | return ndkModuleModel.getAllVariantAbis().isEmpty() ? null : ndkModuleModel;
282 | }
283 | }
284 |
285 | @Nullable
286 | private static NdkModuleModel getNdkModelIfItHasNativeVariantAbis(@NotNull Module module) {
287 | NdkModuleModel ndkModuleModel = NdkModuleModel.get(module);
288 | if (ndkModuleModel == null) {
289 | return null;
290 | } else {
291 | return ndkModuleModel.getAllVariantAbis().isEmpty() ? null : ndkModuleModel;
292 | }
293 | }
294 |
295 | private static void logAndShowBuildVariantFailure(@NotNull String reason) {
296 | String prefix = "Unable to select build variant:\n";
297 | String msg = prefix + reason;
298 | getLog().error(msg);
299 | msg = msg + ".\n\nConsult IDE log for more details (Help | Show Log)";
300 | Messages.showErrorDialog(msg, "Error");
301 | }
302 |
303 | private static void logAndShowAbiNameFailure(@NotNull String reason) {
304 | String prefix = "Unable to select ABI:\n";
305 | String msg = prefix + reason;
306 | getLog().error(msg);
307 | msg = msg + ".\n\nConsult IDE log for more details (Help | Show Log)";
308 | Messages.showErrorDialog(msg, "Error");
309 | }
310 |
311 | @NotNull
312 | private static Logger getLog() {
313 | return Logger.getInstance(LegacyBuildVariantUpdater.class);
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/plugin.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 | com.nilsenlabs.flavormatrix
18 | Build Variant Matrix Selector
19 | Frode Nilsen
20 |
21 | Select variant by selecting flavors in a matrix instead of through the drop down lists provdided by IntelliJ/Android Studio
22 | in the "Build Variants" view. In addition this plugin selects variants for all modules at the same time, even
23 | when there are multiple leaf modules. This results in a very fast and intuitive way of switching variants
24 | for all modules.
25 |
26 |
27 | Usage: Bind a key to the action named "Select Build Variant by Matrix" and press that combination after loading an Android project.
28 | You will then be presented with the dialog to select the variant.
29 |
30 |
31 |
32 |
33 |
34 |
35 | Read more and contribute at the github repo page
36 |
37 |
38 | This is an open source project licensed under the Apache License v2.0.]]>
39 |
40 | org.jetbrains.android
41 | com.intellij.modules.androidstudio
42 |
43 |
44 |
47 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/test/java/com/nilsenlabs/flavormatrix/DimensionListTest.kt:
--------------------------------------------------------------------------------
1 | package com.nilsenlabs.flavormatrix
2 |
3 | import com.nilsenlabs.flavormatrix.actions.DimensionList
4 | import org.junit.Assert.assertEquals
5 | import org.junit.Test
6 |
7 | class DimensionListTest {
8 | @Test
9 | fun flavorsFromVariant_splitsCorrectly() {
10 | val res = DimensionList.flavorsFromVariant("alphaBravoCharlie")
11 | assertEquals(listOf("alpha", "bravo", "charlie"), res)
12 | }
13 |
14 | @Test
15 | fun flavorsFromVariant_singleItem() {
16 | val res = DimensionList.flavorsFromVariant("alpha")
17 | assertEquals(listOf("alpha"), res)
18 | }
19 | }
--------------------------------------------------------------------------------