├── .gitignore ├── LICENSE.txt ├── README.md ├── api ├── .classpath ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── snap │ └── stuffing │ └── api │ ├── AppSwitchConfiguration.java │ ├── AppSwitchHook.java │ ├── DynamicAppConfig.kt │ ├── DynamicAppManager.kt │ ├── StuffingApi.kt │ └── exopackage │ ├── ApplicationLike.java │ ├── DefaultApplicationLike.java │ └── ExopackageApplication.java ├── build.gradle ├── core ├── .classpath ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── snap │ │ └── stuffing │ │ ├── bindings │ │ ├── AppSwitchActivityModule.java │ │ ├── ApplicationComponentOwner.java │ │ ├── DaggerDynamicApplicationLike.java │ │ ├── DelegatingApplicationLike.java │ │ ├── DynamicActivityModule.java │ │ └── DynamicAppModule.java │ │ └── lib │ │ ├── AppComponentModifier.kt │ │ ├── AppSwitchActivity.kt │ │ ├── AppSwitcher.kt │ │ ├── BaseDynamicAppManager.kt │ │ ├── DynamicAppManagerPrefs.kt │ │ ├── DynamicLaunchActivity.java │ │ ├── MultiDynamicAppManager.kt │ │ ├── SingleDynamicAppManager.kt │ │ └── StateTrackingService.kt │ └── test │ ├── AndroidManifest.xml │ └── java │ └── com │ └── snap │ └── stuffing │ └── lib │ └── MultiDynamicAppManagerTest.kt ├── ext.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .classpath ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── AndroidManifestBuck.xml │ ├── java │ └── com │ │ └── snap │ │ └── stuffing │ │ └── sample │ │ ├── SampleAppShell.java │ │ ├── SampleAppSwitchConfiguration.java │ │ ├── SampleDelegatingApplicationLike.java │ │ ├── first │ │ ├── FirstActivity.java │ │ ├── FirstActivityModule.java │ │ ├── FirstApplication.java │ │ └── FirstApplicationComponent.java │ │ └── second │ │ ├── SecondActivity.java │ │ ├── SecondActivityModule.java │ │ ├── SecondApplication.java │ │ └── SecondApplicationComponent.java │ └── res │ ├── layout │ ├── activity_app_switch.xml │ └── main.xml │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle └── stuffing.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .project 5 | .settings 6 | /local.properties 7 | /.idea/caches 8 | /.idea/libraries 9 | /.idea/modules.xml 10 | /.idea/workspace.xml 11 | /.idea/navEditor.xml 12 | /.idea/assetWizardSettings.xml 13 | .DS_Store 14 | build 15 | /captures 16 | .externalNativeBuild 17 | .cxx 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Snap, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stuffing 2 | 3 | Stuffing is a small library that allows multiple [Applications](https://developer.android.com/reference/android/app/Application) to be _stuffed_ into a single [APK](https://en.wikipedia.org/wiki/Android_application_package). Since the Android 4 | operating system only supports a single `Application` declaration in each installed APK, this library uses a common 5 | top-level application and delegates to multiple ```ApplicationLike``` types. The ```ApplicationLike``` types adhere to same general 6 | interface as the [Application](https://developer.android.com/reference/android/app/Application) type, and are responsible for implementing all functionality that would otherwise but common 7 | for an ```Application``` class. 8 | 9 | There are two complications to hosting multiple applications in a single APK: 10 | 11 | 1. The Android operating system does not directly launch _Applications_, it launches Manifest Components: ```Activities```, ```Services```, ```Broadcast Receivers``` which are bound to a specific ```Application``` in the Manifest file. 12 | When a _Component_ is launched (via the Launcher, intent receiver, etc) the OS will create the parent ```Application``` and then the needed _Component_. There are no hooks available in this process. 13 | _This means that there are entry-points that need to be faked to simulate multiple applications 1) the application itself, 2) the manifest components._ 14 | 15 | 1. Android [APKs](https://en.wikipedia.org/wiki/Android```Application```package) are configured using a single [Manifest](https://developer.android.com/guide/topics/manifest/manifest-intro) file, which assumes the presence of a single `````` tag. 16 | All of the manifest components are child nodes of this ```Application```. When packaging an [APKs](https://en.wikipedia.org/wiki/Android```Application```package), the Android build tools merge the manifests of the libraries that the application depends on into a single top-level manifest. 17 | This causes the manifest components for **all** applications to be included in the single `````` tag, regardless if they are relevant for the active application. 18 | _A system is needed to disable manifest components based on the active application_. 19 | 20 | ## Diving into code 21 | 22 | If you are eager to jump into the code without reading the docs. Here is quick overview of the modules: 23 | - `:api` - various API classes of the Stuffing library. See below. 24 | - `:lib` - internal implementation of Stuffing 25 | - `:sample` - sample app that provides an example for using Stuffing. 26 | 27 | To build the sample, open the project in Android Studio or run `./gradlew :sample:installDebug`. 28 | 29 | ## How Stuffing Works 30 | 31 | ### The Structure of a Stuffed APK 32 | 33 | In a _stuffed_ APK, each child application is called an app family. An app family includes all AndroidManifest components that correspond to a given child application. Given that the APK is only allowed to have a single Application class, 34 | the *Stuffing* library uses a common top-level Application class or AppShell , which delegates to multiple ApplicationLike classes that represent individual app families. This concept is inspired by the [Exopackage](https://buck.build/article/exopackage.html) functionality from Facebook’s [buck build tool](https://buck.build/). 35 | 36 | ![Stuffing Architecture](stuffing.png) 37 | 38 | An `ApplicationLike` is type that adheres to roughly the same interface as the Android framework [Application](https://developer.android.com/reference/android/app/Application) type. It acts as a delegate for any application-related functionality in the application lifecycle and manages the global state for an app family. 39 | 40 | `DynamicLaunchActivity` serves as the primary entry point for the *stuffed* application. Given that the Android system doesn’t know which app family is active when launching the app, S*tuffing* provides a layer of indirection/routing via this special activity. When this activity starts, it uses the `DynamicAppManager` to determine which child Activity should be launched 41 | 42 | ### Managing The App Families 43 | 44 | `DynamicAppManager` is the heart of stuffing as it’s responsible for managing the multiple application stuffed into the APK: 45 | 46 | * It maintains the state of which application is currently active (via shared preferences in `DynamicAppManagerPrefs`) 47 | * It provides the hooks for switching between different app families. 48 | * It enables/disables AndroidManifest components depending on which app is active. 49 | * It does this at app start (to ensure any newly added components are in the right state) and when the app family is switched manually 50 | 51 | There are two discrete implementations of this interface: 52 | 53 | * `MultiDynamicAppMananger` - manages multiple applications 54 | * `SingleDynamicAppManager` - manages a single application 55 | 56 | The primary reason for have two implementation is provide an easy way to build multiple flavors of the app. For instance, while the primary flavor of the app could include both apps, it is useful to have an additional build flavor for testing that only include your new app. Swapping different implementations of DynamicAppManager makes this easy. 57 | 58 | ### Switching between app families 59 | 60 | When `DynamicAppManager.switchToAppFamily` is called, the class will iterate through all manifest components using `PackageManager` and disable/enable the right set of components depending on the target app family. Switching between app families can take a while if your app has a lot of components. While the switch is in progress, a special `AppSwitchActivity` is displayed to the user. 61 | 62 | This activity runs in a separate process, and it waits to receive a `Intent.ACTION_PACKAGE_CHANGED]` before allowing the transition to continue. This `Intent` signals that the package manager has finished updating with the app switch changes. If we don't wait for this signal to be received before switching to the new application, the OS might close that application once it receives that signal since it thinks the app has changed. 63 | 64 | `AppSwitchActivity` works around that by waiting for this signal, and then kicking off the launch of the new intended `Activity` once it has been processed. 65 | 66 | ## Using Stuffing in an Existing App 67 | 68 | #### Preparing your codebase 69 | 70 | Let’s assume that you have two `android-application` modules in your Gradle project: `:app` and `:new-app`. 71 | - `:new-app` contains the code for your new Android application that you would like to stuff into your primary application `:app`. 72 | 73 | You start by moving the code from from both of apps into separate `android-library` modules. 74 | - Code from `:app` is moved into `:old` 75 | - Code from `:new-app` is moved into `:new`. 76 | - `new-app` can now be deleted, and `:app` will contain no code for now, but it eventually serve as the binding layer between two apps. 77 | 78 | The next step would be to annotate all components in the `AndroidManifests` of both apps with `appFamiles` metadata. 79 | - The value will be one of two strings: `old` or `new`, depending on which app the component belongs to. 80 | - Side note: if had a module with some components that are being shared between both apps, the appFamilies metadata can be omitted, which would automatically include the Component into both application. 81 | - Example: 82 | ```xml 83 | 84 | 85 | 86 | ``` 87 | 88 | #### Adding new launcher `Activity` 89 | 90 | To create a new entry point for both apps, add `DynamicLaunchActivity` in the top level manifest of `:app`: 91 | > This will cause the `DynamicLaunchActivity` to be launched when the app's launcher button is tapped. The `DynamicLaunchActivity` then needs to route to the default activity for the active `appFamily` (see below). 92 | 93 | ```xml 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ``` 102 | 103 | Once `DynamicLaunchActivity` is added, define a default Activity for each app family in thier respective manifests: 104 | 105 | ```xml 106 | 107 | 108 | 109 | ``` 110 | 111 | #### Adding new launcher `Application` 112 | 113 | Create a new top level application in `:app`, which is responsible for creating and initializing `MultiDynamicAppManager` that will delegate execution to the right application. 114 | > In order to make the DynamicAppManager injectable with Dagger, some special treatment is needed. Since it is created very early in the application lifecycle, even before the Dagger application component is created, it needs to be manually bound to the DI graph 115 | 116 | ```java 117 | public class SampleDelegatingApplicationLike extends DelegatingApplicationLike { 118 | 119 | @NonNull 120 | @Override 121 | protected ApplicationLike createApplication() { 122 | final DynamicAppModule dynamicAppModule = 123 | DynamicAppModule.makeMultiAppModule(mApplication, "old"); 124 | 125 | dynamicAppModule.dynamicAppManager().initialize(); 126 | 127 | final ApplicationLike applicationLike; 128 | if ("new".equals(dynamicAppModule.dynamicAppManager().getApplicationFamily())) { 129 | applicationLike = new SecondApplication(mApplication); 130 | } else { 131 | applicationLike = new FirstApplication(mApplication); 132 | } 133 | 134 | if (applicationLike instanceof ApplicationComponentOwner) { 135 | ((ApplicationComponentOwner) applicationLike).attachDynamicAppModule(dynamicAppModule); 136 | } 137 | 138 | return applicationLike; 139 | } 140 | } 141 | ``` 142 | 143 | 144 | #### Change existing apps into Stuffing plugins 145 | 146 | Update the `Application` classes in the `:old` and `:new` apps to extend from `DefaultApplicationLike` and `ApplicationComponentOwner` in order in order to bind the `DynamicAppModule` to the Application's Dagger graph. 147 | 148 | ```java 149 | public class FirstApplication extends DefaultApplicationLike implements HasActivityInjector, ApplicationComponentOwner { 150 | private final Application app; 151 | 152 | private FirstApplicationComponent appComponent; 153 | private DynamicAppModule dynamicAppModule; 154 | 155 | @Inject DispatchingAndroidInjector dispatchingActivityInjector; 156 | 157 | public FirstApplication(Application app) { 158 | this.app = app; 159 | } 160 | 161 | @Override 162 | public void onCreate() { 163 | appComponent = DaggerFirstApplicationComponent 164 | .builder() 165 | .dynamicAppModule(dynamicAppModule) 166 | .build(); 167 | appComponent.inject(this); 168 | } 169 | 170 | @Override 171 | public AndroidInjector activityInjector() { 172 | return dispatchingActivityInjector; 173 | } 174 | 175 | @Override 176 | public void attachDynamicAppModule(@NonNull DynamicAppModule dynamicAppModule) { 177 | this.dynamicAppModule = dynamicAppModule; 178 | } 179 | } 180 | ``` 181 | 182 | #### Add a switcher for Applications 183 | 184 | Now that `DynamicAppManager` is available in your application’s dagger graph, it can be used to provide a way to manually switch from your old application to your new application. For instance, you can trigger this code when a use clicks on the “Try New App” somewhere in settings: 185 | 186 | ```java 187 | @Inject DynamicAppManager dynamicAppManager; 188 | 189 | ... 190 | 191 | dynamicAppMamager.switchToAppFamily("new") 192 | ``` -------------------------------------------------------------------------------- /api/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /api/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 28 6 | 7 | defaultConfig { 8 | minSdkVersion 19 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | } 18 | debug { 19 | minifyEnabled false 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | api deps.javax.inject 26 | api deps.support.annotations 27 | 28 | implementation deps.kotlin.stdLib_jdk7 29 | } 30 | -------------------------------------------------------------------------------- /api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/AppSwitchConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api; 2 | 3 | import android.support.annotation.LayoutRes; 4 | 5 | /** 6 | * An Optional configuration that indicates provides additional logic when switching applications. This configuration 7 | * applies to the Activity that is used when switching between two applications. 8 | */ 9 | public interface AppSwitchConfiguration { 10 | 11 | /** 12 | * The resource ID of a layout to apply to the Activity used when switching applications. 13 | */ 14 | @LayoutRes 15 | int getAppSwitchActivityResId(); 16 | 17 | /** 18 | * A hook to initialize the application that is being started. This method will be called from the interstitial 19 | * activity that handles application changes, and cannot invoke or display any UI. 20 | * 21 | * This is a good opportunity to run an app-specific initialization logic that might be relevant. 22 | */ 23 | void startAppWarmUp(); 24 | } 25 | -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/AppSwitchHook.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api; 2 | 3 | import android.support.annotation.LayoutRes; 4 | import android.support.annotation.NonNull; 5 | 6 | /** 7 | * Provides for an opportunity to execute code prior to an app family being changed. 8 | * 9 | * Unlike {@link AppSwitchConfiguration}, this is invoked prior to restarting the process. 10 | */ 11 | public interface AppSwitchHook { 12 | 13 | /** 14 | * Opportunity to run code prior to an app family switch. 15 | * 16 | * @param fromAppFamily from app-family 17 | * @param toAppFamily to app-family 18 | */ 19 | void preAppFamilySwitch(@NonNull String fromAppFamily, @NonNull String toAppFamily); 20 | } 21 | -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/DynamicAppConfig.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api 2 | 3 | /** 4 | * Provides configuration for a [DynamicAppManager]. 5 | * 6 | * [events] specifies string-mapped runnables what should be executed when those events are invoked om the [DynamicAppManager]. 7 | */ 8 | data class DynamicAppConfig(val events: Map) -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/DynamicAppManager.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api 2 | 3 | import android.app.Application 4 | import android.content.Intent 5 | 6 | /** 7 | * Manages multiple "[Application]s" within a single APK. 8 | */ 9 | interface DynamicAppManager { 10 | /** 11 | * Indicates is this dynamic app manager is active (managing multiple applications). 12 | */ 13 | val active: Boolean 14 | 15 | /** 16 | * The currently active 'application family'. Can return an empty string if none is selected. 17 | */ 18 | val applicationFamily: String 19 | 20 | /** 21 | * Initializes this manager, setting up internal state based on persisted values. 22 | */ 23 | fun initialize() 24 | 25 | /** 26 | * Invoked when an event that might be of interest to the [DynamicAppManager] occurs. 27 | */ 28 | fun onEvent(eventName: String) 29 | 30 | /** 31 | * Gets the class name of the default [Activity] for the current [applicationFamily]. 32 | */ 33 | fun getDefaultActivityClassName(): String? 34 | 35 | /** 36 | * Clears internal state and returns to the default app family. Note that the process should eb restarted after 37 | * calling this method. 38 | */ 39 | fun returnToDefaultFamily() 40 | 41 | /** 42 | * Switch to the app family specified in [appFamily]. Note that the process should eb restarted after 43 | * calling this method. 44 | */ 45 | fun switchToAppFamily(appFamily: String, useSwitchActivity: Boolean, launchIntent: Intent?) 46 | 47 | /** 48 | * Indicates that the current application session is the first application session following an app family change. 49 | */ 50 | fun hasPendingAppFamilyChangeSignal(): Boolean 51 | 52 | /** 53 | * Consume a pending app change signal such that [hasPendingAppFamilyChangeSignal] will return false until the next 54 | * application change event. 55 | */ 56 | fun consumePendingAppFamilyChangeSignal() 57 | } -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/StuffingApi.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api 2 | 3 | /** 4 | * Sample API for module Stuffing. 5 | */ 6 | interface StuffingApi { 7 | 8 | /** 9 | * Returns a text description of the module. 10 | */ 11 | val moduleDescription: String 12 | } -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/exopackage/ApplicationLike.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api.exopackage; 2 | 3 | import android.app.Application; 4 | import android.content.res.Configuration; 5 | 6 | /** 7 | * This interface is used to delegate calls from main Application object. 8 | * 9 | *

Implementations of this interface must have a one-argument constructor that takes an argument 10 | * of type {@link Application}. 11 | * 12 | * Copy of https://github.com/facebook/buck/blob/master/android/com/facebook/buck/android/support/exopackage/ApplicationLike.java 13 | */ 14 | public interface ApplicationLike { 15 | 16 | /** Same as {@link Application#onCreate()}. */ 17 | void onCreate(); 18 | 19 | /** Same as {@link Application#onLowMemory()}. */ 20 | void onLowMemory(); 21 | 22 | /** 23 | * Same as {@link Application#onTrimMemory(int level)}. 24 | * 25 | * @param level 26 | */ 27 | void onTrimMemory(int level); 28 | 29 | /** Same as {@link Application#onTerminate()}. */ 30 | void onTerminate(); 31 | 32 | /** Same as {@link Application#onConfigurationChanged(Configuration newconfig)}. */ 33 | void onConfigurationChanged(Configuration newConfig); 34 | 35 | /** Same as {@link Application#getSystemService(String name)}. */ 36 | Object getSystemService(String name); 37 | } -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/exopackage/DefaultApplicationLike.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api.exopackage; 2 | 3 | import android.app.Application; 4 | import android.content.res.Configuration; 5 | 6 | /** 7 | * Empty implementation of {@link ApplicationLike}. 8 | * 9 | * Copy of https://github.com/facebook/buck/blob/master/android/com/facebook/buck/android/support/exopackage/DefaultApplicationLike.java 10 | */ 11 | public class DefaultApplicationLike implements ApplicationLike { 12 | public DefaultApplicationLike() {} 13 | 14 | @SuppressWarnings("unused") 15 | public DefaultApplicationLike(Application application) {} 16 | 17 | @Override 18 | public void onCreate() {} 19 | 20 | @Override 21 | public void onLowMemory() {} 22 | 23 | @Override 24 | public void onTrimMemory(int level) {} 25 | 26 | @Override 27 | public void onTerminate() {} 28 | 29 | @Override 30 | public void onConfigurationChanged(Configuration newConfig) {} 31 | 32 | @Override 33 | public Object getSystemService(String name) { 34 | return null; 35 | } 36 | } -------------------------------------------------------------------------------- /api/src/main/java/com/snap/stuffing/api/exopackage/ExopackageApplication.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.api.exopackage; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.content.res.Configuration; 7 | import java.lang.reflect.Constructor; 8 | 9 | /** 10 | * A base class for implementing an Application that delegates to an {@link ApplicationLike} 11 | * instance. This is used in conjunction with secondary dex files so that the logic that would 12 | * normally live in the Application class is loaded after the secondary dexes are loaded. 13 | * 14 | * Forked from https://github.com/facebook/buck/blob/master/android/com/facebook/buck/android/support/exopackage/ExopackageApplication.java 15 | */ 16 | public abstract class ExopackageApplication extends Application { 17 | 18 | private static final int SECONDARY_DEX_MASK = 1; 19 | private static final int NATIVE_LIBRARY_MASK = 2; 20 | private static final int RESOURCES_MASK = 4; 21 | private static final int MODULES_MASK = 8; 22 | 23 | private final String delegateClassName; 24 | private final int exopackageFlags; 25 | private T delegate; 26 | 27 | /** 28 | * @param exopackageFlags Bitmask used to determine which exopackage feature is enabled in the 29 | * current build. This should usually be {@code BuildConfig.EXOPACKAGE_FLAGS}. 30 | */ 31 | protected ExopackageApplication(int exopackageFlags) { 32 | this(DefaultApplicationLike.class.getName(), exopackageFlags); 33 | } 34 | 35 | /** 36 | * @param delegateClassName The fully-qualified name of the {@link ApplicationLike} class that 37 | * will act as the delegate for application lifecycle callbacks. 38 | * @param exopackageFlags Bitmask used to determine which exopackage feature is enabled in the 39 | * current build. This should usually be {@code BuildConfig.EXOPACKAGE_FLAGS}. 40 | */ 41 | protected ExopackageApplication(String delegateClassName, int exopackageFlags) { 42 | this.delegateClassName = delegateClassName; 43 | this.exopackageFlags = exopackageFlags; 44 | } 45 | 46 | private boolean isExopackageEnabledForSecondaryDex() { 47 | return (exopackageFlags & SECONDARY_DEX_MASK) != 0; 48 | } 49 | 50 | private boolean isExopackageEnabledForNativeLibraries() { 51 | return (exopackageFlags & NATIVE_LIBRARY_MASK) != 0; 52 | } 53 | 54 | private boolean isExopackageEnabledForResources() { 55 | return (exopackageFlags & RESOURCES_MASK) != 0; 56 | } 57 | 58 | private boolean isExopackageEnabledForModules() { 59 | return (exopackageFlags & MODULES_MASK) != 0; 60 | } 61 | 62 | private T createDelegate() { 63 | // if (isExopackageEnabledForSecondaryDex()) { 64 | // ExopackageDexLoader.loadExopackageJars(this, isExopackageEnabledForModules()); 65 | // if (isExopackageEnabledForModules()) { 66 | // ExoHelper.setupHotswap(this); 67 | // } 68 | // } 69 | // 70 | // if (isExopackageEnabledForNativeLibraries()) { 71 | // ExopackageSoLoader.init(this); 72 | // } 73 | // 74 | // if (isExopackageEnabledForResources()) { 75 | // ResourcesLoader.init(this); 76 | // } 77 | 78 | try { 79 | // Use reflection to create the delegate so it doesn't need to go into the primary dex. 80 | Class implClass = (Class) Class.forName(delegateClassName); 81 | Constructor constructor = implClass.getConstructor(Application.class); 82 | return constructor.newInstance(this); 83 | } catch (Exception e) { 84 | throw new RuntimeException(e); 85 | } 86 | } 87 | 88 | private synchronized void ensureDelegate() { 89 | if (delegate == null) { 90 | delegate = createDelegate(); 91 | } 92 | } 93 | 94 | /** 95 | * Hook for sub-classes to run logic after the {@link Application#attachBaseContext} has been 96 | * called but before the delegate is created. Implementors should be very careful what they do 97 | * here since {@link android.app.Application#onCreate} will not have yet been called. 98 | */ 99 | protected void onBaseContextAttached() {} 100 | 101 | /** @return the delegate, or {@code null} if not set up. */ 102 | // @Nullable - Don't want to force a reference to that annotation in the primary dex. 103 | public final T getDelegateIfPresent() { 104 | return delegate; 105 | } 106 | 107 | @Override 108 | protected final void attachBaseContext(Context base) { 109 | super.attachBaseContext(base); 110 | onBaseContextAttached(); 111 | ensureDelegate(); 112 | } 113 | 114 | @Override 115 | public final void onCreate() { 116 | super.onCreate(); 117 | // if (isExopackageEnabledForResources()) { 118 | // ResourcesLoader.onAppCreated(this); 119 | // } 120 | ensureDelegate(); 121 | delegate.onCreate(); 122 | } 123 | 124 | @Override 125 | public final void onTerminate() { 126 | super.onTerminate(); 127 | if (delegate != null) { 128 | delegate.onTerminate(); 129 | } 130 | } 131 | 132 | @Override 133 | public final void onLowMemory() { 134 | super.onLowMemory(); 135 | if (delegate != null) { 136 | delegate.onLowMemory(); 137 | } 138 | } 139 | 140 | @TargetApi(14) 141 | @Override 142 | public final void onTrimMemory(int level) { 143 | super.onTrimMemory(level); 144 | if (delegate != null) { 145 | delegate.onTrimMemory(level); 146 | } 147 | } 148 | 149 | @Override 150 | public void onConfigurationChanged(Configuration newConfig) { 151 | super.onConfigurationChanged(newConfig); 152 | if (delegate != null) { 153 | delegate.onConfigurationChanged(newConfig); 154 | } 155 | } 156 | 157 | @Override 158 | public Object getSystemService(String name) { 159 | if (delegate != null) { 160 | Object service = delegate.getSystemService(name); 161 | if (service != null) { 162 | return service; 163 | } 164 | } 165 | return super.getSystemService(name); 166 | } 167 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.31' 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.5.0-beta05' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | 30 | apply from: 'ext.gradle' -------------------------------------------------------------------------------- /core/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 28 7 | 8 | defaultConfig { 9 | minSdkVersion 19 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | } 19 | debug { 20 | minifyEnabled false 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | kapt deps.dagger.android_processor 27 | kapt deps.dagger.compiler 28 | 29 | api project(':api') 30 | api deps.dagger.android 31 | api deps.dagger.android_support 32 | api deps.dagger.runtime 33 | 34 | implementation deps.javax.inject 35 | implementation deps.dagger.runtime 36 | implementation deps.kotlin.stdLib_jdk7 37 | implementation deps.processPhoenix 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/bindings/AppSwitchActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.bindings; 2 | 3 | import com.snap.stuffing.api.AppSwitchConfiguration; 4 | import com.snap.stuffing.lib.AppSwitchActivity; 5 | import dagger.BindsOptionalOf; 6 | import dagger.Module; 7 | import dagger.android.ContributesAndroidInjector; 8 | 9 | @Module 10 | public abstract class AppSwitchActivityModule { 11 | 12 | @BindsOptionalOf 13 | abstract AppSwitchConfiguration appSwitchConfiguration(); 14 | 15 | @ContributesAndroidInjector 16 | public abstract AppSwitchActivity contributeAppSwitchActivity(); 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/bindings/ApplicationComponentOwner.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.bindings; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | /** 6 | * An entity that owns a Dagger {@link dagger.Component} for an {@link android.app.Application}. 7 | */ 8 | public interface ApplicationComponentOwner { 9 | 10 | /** 11 | * Attaches a {@link DynamicAppModule} to this ApplicationComponentOwner such that it can be included in the 12 | * application's top-level Dagger component and bound to the DI graph. 13 | */ 14 | void attachDynamicAppModule(@NonNull DynamicAppModule dynamicAppModule); 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/bindings/DaggerDynamicApplicationLike.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.bindings; 2 | 3 | import com.snap.stuffing.api.exopackage.ApplicationLike; 4 | import com.snap.stuffing.api.DynamicAppManager; 5 | import dagger.android.HasActivityInjector; 6 | import dagger.android.HasBroadcastReceiverInjector; 7 | import dagger.android.HasServiceInjector; 8 | 9 | import android.app.Application; 10 | import android.content.res.Configuration; 11 | import android.support.annotation.NonNull; 12 | 13 | /** 14 | * A {@link ApplicationLike} implementation for applications that user Dagger. Requires a {@link DynamicAppModule} as 15 | * a parameter, which can be added to the main application {@link dagger.Component} to provider bindings for the 16 | * {@link DynamicAppManager} which is created VERY early in the process lifecycle. 17 | */ 18 | public abstract class DaggerDynamicApplicationLike implements 19 | ApplicationLike, 20 | ApplicationComponentOwner, 21 | HasActivityInjector, 22 | HasBroadcastReceiverInjector, 23 | HasServiceInjector { 24 | protected final Application application; 25 | protected DynamicAppModule dynamicAppModule; 26 | 27 | protected DaggerDynamicApplicationLike(@NonNull Application application) { 28 | this.application = application; 29 | } 30 | 31 | @Override 32 | public void attachDynamicAppModule(@NonNull DynamicAppModule dynamicAppModule) { 33 | this.dynamicAppModule = dynamicAppModule; 34 | } 35 | 36 | @Override 37 | public void onCreate() {} 38 | 39 | @Override 40 | public void onLowMemory() {} 41 | 42 | @Override 43 | public void onTrimMemory(int var1) {} 44 | 45 | @Override 46 | public void onTerminate() {} 47 | 48 | @Override 49 | public void onConfigurationChanged(Configuration var1) {} 50 | 51 | @Override 52 | public Object getSystemService(String var1) { return null; } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/bindings/DelegatingApplicationLike.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.bindings; 2 | 3 | import com.snap.stuffing.api.exopackage.ApplicationLike; 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.app.Service; 7 | import android.content.BroadcastReceiver; 8 | import android.content.ContentProvider; 9 | import android.content.res.Configuration; 10 | import android.support.annotation.NonNull; 11 | 12 | import dagger.android.AndroidInjector; 13 | import dagger.android.HasActivityInjector; 14 | import dagger.android.HasBroadcastReceiverInjector; 15 | import dagger.android.HasContentProviderInjector; 16 | import dagger.android.HasServiceInjector; 17 | 18 | import java.lang.reflect.Constructor; 19 | 20 | /** 21 | * An {@link ApplicationLike} implementation that allows delegation to difference underling applications. The 22 | * {@link ApplicationLike} types that this class delegates to must be configured for Dagger injection, and implement the 23 | * {@link HasServiceInjector}, {@link HasBroadcastReceiverInjector} and {@link HasActivityInjector} interfaces. 24 | */ 25 | public abstract class DelegatingApplicationLike implements 26 | ApplicationLike, 27 | HasActivityInjector, 28 | HasBroadcastReceiverInjector, 29 | HasServiceInjector, 30 | HasContentProviderInjector { 31 | 32 | private volatile ApplicationLike mApplicationLike = null; 33 | protected final Application mApplication; 34 | 35 | protected DelegatingApplicationLike(@NonNull Application application) { 36 | mApplication = application; 37 | } 38 | 39 | @Override 40 | public void onCreate() { 41 | getApplication().onCreate(); 42 | } 43 | 44 | @Override 45 | public void onLowMemory() { 46 | getApplication().onLowMemory(); 47 | } 48 | 49 | @Override 50 | public void onTrimMemory(int i) { 51 | getApplication().onTrimMemory(i); 52 | } 53 | 54 | @Override 55 | public void onTerminate() { 56 | getApplication().onTerminate(); 57 | } 58 | 59 | @Override 60 | public void onConfigurationChanged(Configuration configuration) { 61 | getApplication().onConfigurationChanged(configuration); 62 | } 63 | 64 | @Override 65 | public Object getSystemService(String s) { 66 | return getApplication().getSystemService(s); 67 | } 68 | 69 | public synchronized ApplicationLike getApplication() { 70 | if (mApplicationLike == null) { 71 | mApplicationLike = createApplication(); 72 | } 73 | return mApplicationLike; 74 | } 75 | 76 | @NonNull 77 | protected abstract ApplicationLike createApplication(); 78 | 79 | @Override 80 | public AndroidInjector activityInjector() { 81 | return ((HasActivityInjector) getApplication()).activityInjector(); 82 | } 83 | 84 | @Override 85 | public AndroidInjector broadcastReceiverInjector() { 86 | return ((HasBroadcastReceiverInjector) getApplication()).broadcastReceiverInjector(); 87 | } 88 | 89 | @Override 90 | public AndroidInjector serviceInjector() { 91 | return ((HasServiceInjector) getApplication()).serviceInjector(); 92 | } 93 | 94 | @Override 95 | public AndroidInjector contentProviderInjector() { 96 | return ((HasContentProviderInjector) getApplication()).contentProviderInjector(); 97 | } 98 | 99 | @NonNull 100 | protected ApplicationLike instantiateApplicationLikeClass(@NonNull String applicationClassName) { 101 | try { 102 | Class implClass = Class.forName(applicationClassName); 103 | Constructor constructor = implClass.getConstructor(new Class[]{ Application.class }); 104 | return (ApplicationLike) constructor.newInstance(new Object[]{ mApplication }); 105 | } catch (Exception e) { 106 | throw new RuntimeException(e); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/bindings/DynamicActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.bindings; 2 | 3 | import com.snap.stuffing.lib.DynamicLaunchActivity; 4 | import dagger.Module; 5 | import dagger.android.ContributesAndroidInjector; 6 | 7 | @Module 8 | public abstract class DynamicActivityModule { 9 | @ContributesAndroidInjector 10 | public abstract DynamicLaunchActivity contributeDynamicLaunchActivity(); 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/bindings/DynamicAppModule.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.bindings; 2 | 3 | import com.snap.stuffing.api.AppSwitchHook; 4 | import com.snap.stuffing.api.DynamicAppConfig; 5 | import com.snap.stuffing.api.DynamicAppManager; 6 | import com.snap.stuffing.lib.MultiDynamicAppManager; 7 | import com.snap.stuffing.lib.SingleDynamicAppManager; 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | import android.app.Application; 12 | import android.support.annotation.NonNull; 13 | 14 | import javax.inject.Singleton; 15 | 16 | /** 17 | * A Dagger {@link Module} that can be used to install the {@link DynamicAppManager} into the application object graph. 18 | * Typically the {@link DynamicAppManager} is created before the Dagger graph. 19 | */ 20 | @Module 21 | public class DynamicAppModule { 22 | private final DynamicAppManager dynamicAppManager; 23 | 24 | private DynamicAppModule(@NonNull DynamicAppManager dynamicAppManager) { 25 | this.dynamicAppManager = dynamicAppManager; 26 | } 27 | 28 | @Singleton 29 | @Provides 30 | public DynamicAppManager dynamicAppManager() { 31 | return dynamicAppManager; 32 | } 33 | 34 | @NonNull 35 | public static DynamicAppModule makeSingleAppModule(@NonNull Application app, 36 | @NonNull String defaultActivityClassName, 37 | @NonNull String defaultAppFamily) { 38 | return new DynamicAppModule(new SingleDynamicAppManager(app, defaultActivityClassName, 39 | defaultAppFamily)); 40 | } 41 | 42 | @NonNull 43 | public static DynamicAppModule makeMultiAppModule( 44 | @NonNull Application app, 45 | @NonNull String defaultAppFamily, 46 | @NonNull DynamicAppConfig dynamicAppConfig, 47 | @NonNull AppSwitchHook appSwitchHook) { 48 | return new DynamicAppModule(new MultiDynamicAppManager(app, defaultAppFamily, dynamicAppConfig, 49 | appSwitchHook)); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/AppComponentModifier.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.pm.ComponentInfo 6 | import android.content.pm.PackageInfo 7 | import android.content.pm.PackageManager 8 | import android.app.Application 9 | import android.app.Activity 10 | import android.content.Intent 11 | import android.os.Build 12 | import android.util.Log 13 | import java.util.Arrays 14 | 15 | private val TAG = "AppComponentModifier" 16 | 17 | /** 18 | * Responsible for enabling and disabling [Application] components based on which 'appFamily' is enabled. Components 19 | * in the application manifest can be tagged with an 'appFamilies' meta-data attribute used to inform which appFamilies 20 | * the component should be enabled for. 21 | */ 22 | internal class AppComponentModifier(private val context: Context) { 23 | private val packageManager: PackageManager by lazy { context.packageManager } 24 | 25 | private val queryDisableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 26 | PackageManager.MATCH_DISABLED_COMPONENTS 27 | } else { 28 | PackageManager.GET_DISABLED_COMPONENTS 29 | } 30 | 31 | /** 32 | * Gets the default [Activity] class name for the [appFamily] as specified in the application's manifest. 33 | * 34 | * Returns null if none is specified in the manifest. 35 | */ 36 | fun getDefaultActivityClassNameForAppFamily(appFamily: String): String? { 37 | return getMainActivityForAppFamily(appFamily)?.name 38 | } 39 | 40 | /** 41 | * Gets an [Intent] to launch the default activity for the specified [appFamily]. 42 | * 43 | * Returns null if no default activity is specified in the manifest for this appFamily. 44 | */ 45 | fun getLaunchIntentForAppFamily(appFamily: String): Intent? { 46 | val launchActivity = getMainActivityForAppFamily(appFamily) ?: return null 47 | 48 | return Intent.makeMainActivity(launchActivity.componentName()).apply { 49 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 50 | } 51 | } 52 | 53 | /** 54 | * Determines if the app's component list was ever modified by checking the state of the [StateTrackingService] 55 | * component. 56 | * 57 | * @return True if this component was ever modified, false otherwise. 58 | */ 59 | fun checkComponentListModifiedState(): Boolean { 60 | return packageManager.getComponentEnabledSetting( 61 | ComponentName(context.packageName, StateTrackingService::class.java.name)) != 62 | PackageManager.COMPONENT_ENABLED_STATE_DEFAULT 63 | } 64 | 65 | private fun getMainActivityForAppFamily(appFamily: String): ComponentInfo? { 66 | return listActivities().findLast { 67 | it.metaData?.getString("mainForAppFamilies")?.toLowerCase().equals(appFamily.toLowerCase()) 68 | } 69 | } 70 | 71 | /** 72 | * Switch the specified app family, enabling [Application] components that belong to that family and disabling those 73 | * that don't. Components with no listed appFamilies meta-data attribute will be enabled for any appFamily. 74 | * 75 | * @return True if any component was modified as a result of this call, false otherwise. 76 | */ 77 | fun switchToAppFamily(appFamily: String, tag: String): Boolean { 78 | Log.d(tag, "STUFFING -- Switching to app family: $appFamily...") 79 | 80 | var componentsChanged = false 81 | 82 | listComponents().forEach { 83 | val appFamilies = 84 | it.metaData?.getString("appFamilies")?.split(',')?.map { f -> f.trim().toLowerCase() } ?: listOf() 85 | 86 | if (!appFamilies.isEmpty()) { 87 | if (appFamilies.contains(appFamily.toLowerCase())) { 88 | Log.d(tag, "STUFFING -- Enabling component: ${it.name} with appFamilies $appFamilies") 89 | if (it.enable()) componentsChanged = true 90 | } else { 91 | Log.d(tag, "STUFFING -- Disabling component: ${it.name} with appFamilies $appFamilies") 92 | if (it.disable()) componentsChanged = true 93 | } 94 | } else { 95 | if (it.applyDefaultEnabledState()) componentsChanged = true 96 | } 97 | } 98 | 99 | Log.d(tag, "STUFFING -- Switching to app family: $appFamily complete. componentsChanged=$componentsChanged") 100 | return componentsChanged 101 | 102 | //printComponentEnabledStates(tag) 103 | } 104 | 105 | /** 106 | * Enable all [Application] components that belong to any family. 107 | * @return True if any component was modified as a result of this call, false otherwise. 108 | */ 109 | fun resetAllComponents(tag: String): Boolean { 110 | Log.d(tag, "STUFFING -- Enabling all components...") 111 | 112 | var componentsChanged = false 113 | 114 | listComponents().forEach { 115 | Log.d(tag, "STUFFING -- Enabling component to default state: ${it.name}") 116 | if (it.applyDefaultEnabledState()) componentsChanged = true 117 | } 118 | 119 | Log.d(tag, "STUFFING -- Enabling all complete. componentsChanged=$componentsChanged") 120 | return componentsChanged 121 | 122 | //printComponentEnabledStates(tag) 123 | } 124 | 125 | /** 126 | * Logs the components in the manifest, stating whether they are enabled or disabled. 127 | */ 128 | fun printComponentEnabledStates(tag: String = TAG) { 129 | val componentGroups = listComponents().groupBy { it.getEnabledSetting() } 130 | for (entry in componentGroups) { 131 | Log.d(tag, "STUFFING -- Enabled (${convertEnabledSettingToString(entry.key)}):\n" 132 | + (entry.value.map { it.name }).joinToString("\n")) 133 | } 134 | } 135 | 136 | private fun convertEnabledSettingToString(componentEnabledSetting: Int): String { 137 | return when (componentEnabledSetting) { 138 | PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> "DEFAULT" 139 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> "DISABLED" 140 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> "DISABLED_UNTIL_USED" 141 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> "DISABLED_USER" 142 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> "ENABLED" 143 | else -> "UNKNOWN" 144 | } 145 | } 146 | 147 | private fun listComponents(): List { 148 | val packageInfo = getPackageInfo(PackageManager.GET_RECEIVERS or 149 | PackageManager.GET_SERVICES or 150 | PackageManager.GET_PROVIDERS or 151 | PackageManager.GET_META_DATA or 152 | PackageManager.GET_ACTIVITIES) 153 | 154 | val componentInfos = mutableListOf() 155 | packageInfo.services?.let { componentInfos.addAll(Arrays.asList(*packageInfo.services)) } 156 | packageInfo.receivers?.let { componentInfos.addAll(Arrays.asList(*packageInfo.receivers)) } 157 | packageInfo.providers?.let { componentInfos.addAll(Arrays.asList(*packageInfo.providers)) } 158 | packageInfo.activities?.let { componentInfos.addAll(Arrays.asList(*packageInfo.activities)) } 159 | return componentInfos 160 | } 161 | 162 | private fun listActivities(): List { 163 | val packageInfo = getPackageInfo(PackageManager.GET_ACTIVITIES or PackageManager.GET_META_DATA) 164 | val componentInfos = mutableListOf() 165 | componentInfos.addAll(Arrays.asList(*packageInfo.activities)) 166 | return componentInfos 167 | } 168 | 169 | private fun getPackageInfo(flags: Int): PackageInfo { 170 | val packageInfo: PackageInfo 171 | try { 172 | packageInfo = packageManager.getPackageInfo(context.packageName, flags or queryDisableFlag) 173 | } catch (e: PackageManager.NameNotFoundException) { 174 | Log.e(TAG, "Couldn't query app components with flags $flags", e) 175 | throw RuntimeException(e) 176 | } 177 | return packageInfo 178 | } 179 | 180 | private fun ComponentInfo.getEnabledSetting(): Int { 181 | return packageManager.getComponentEnabledSetting(componentName()) 182 | } 183 | 184 | private fun ComponentInfo.disable(): Boolean { 185 | return setComponentEnabledSetting(PackageManager.COMPONENT_ENABLED_STATE_DISABLED) 186 | } 187 | 188 | private fun ComponentInfo.enable(): Boolean { 189 | return setComponentEnabledSetting(PackageManager.COMPONENT_ENABLED_STATE_ENABLED) 190 | } 191 | 192 | private fun ComponentInfo.applyDefaultEnabledState(): Boolean { 193 | return setComponentEnabledSetting(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) 194 | } 195 | 196 | private fun ComponentInfo.setComponentEnabledSetting(value: Int): Boolean { 197 | val componentName = componentName() 198 | val previousValue = packageManager.getComponentEnabledSetting(componentName) 199 | 200 | if (previousValue != value) { 201 | packageManager.setComponentEnabledSetting( 202 | componentName, 203 | value, 204 | PackageManager.DONT_KILL_APP) 205 | 206 | Log.d(TAG, "STUFFING -- Setting component $name state from ${convertEnabledSettingToString(previousValue)} " 207 | + "to ${convertEnabledSettingToString(value)}") 208 | return true 209 | } 210 | 211 | return false 212 | } 213 | 214 | private fun ComponentInfo.componentName(): ComponentName { 215 | return ComponentName(packageName, name) 216 | } 217 | } 218 | 219 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/AppSwitchActivity.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.os.Bundle 8 | import android.os.Handler 9 | import android.os.Looper 10 | import android.app.Activity 11 | import android.support.v4.app.FragmentActivity 12 | import android.util.Log 13 | import com.snap.stuffing.api.AppSwitchConfiguration 14 | import dagger.android.AndroidInjection 15 | import javax.inject.Inject 16 | import javax.inject.Provider 17 | 18 | private const val TAG = "AppToggleActivity" 19 | 20 | /** 21 | * 11 second timeout. The PACKAGE_CHANGED intent should be received within 10s based on the constant in PackageManager, 22 | * so if it isn't received in 11s, then start the next activity. 23 | */ 24 | private const val TOGGLE_TIMEOUT_MILLISECONDS = 11 * 1000L 25 | 26 | /** 27 | * An [Activity] responsible for managing the application switch process. It will run in a separate process, and wait 28 | * for to receive a [Intent.ACTION_PACKAGE_CHANGED] before allowing the transition to continue. This [Intent] signals 29 | * that the package manager has finished updating with the app switch changes. 30 | * 31 | * If we don't wait for this signal to be received before switching to the new application, the OS might close that 32 | * application once it receives that signal since it thinks the app has changed. 33 | * 34 | * This [Activity] implementation works around that by waiting for this signal, and then kicking off the launch of the 35 | * new intended [Activity] once it has been processed. 36 | * 37 | * This [Activity] should be launched with an [Intent] containing an extra property of type [Intent] keyed by 38 | * [AppSwitcher.KEY_RESTART_INTENT]. This [Intent] should be the intent that signals how to launch the new application 39 | * once this [Activity] is finished. 40 | * 41 | * Since the [Intent.ACTION_PACKAGE_CHANGED] is sometimes unreliable, this [Activity] also maintains a timer to time 42 | * out of the process after 11 seconds, which is one second longer than it usually takes to receive the 43 | * [Intent.ACTION_PACKAGE_CHANGED] Intent. 44 | */ 45 | class AppSwitchActivity : FragmentActivity() { 46 | 47 | private val handler = Handler(Looper.getMainLooper()) 48 | 49 | private lateinit var relaunchIntent: Intent 50 | private var shouldRelaunch = false 51 | 52 | @Inject lateinit var configuration: Provider 53 | 54 | private val broadcastReceiver = object : BroadcastReceiver() { 55 | override fun onReceive(context: Context?, intent: Intent?) { 56 | Log.d(TAG, "STUFFING -- Activity intent received: $intent") 57 | shouldRelaunch = true 58 | } 59 | } 60 | 61 | private val finishRunnable = Runnable { 62 | Log.d(TAG, "STUFFING -- timer expired") 63 | shouldRelaunch = true 64 | finish() 65 | } 66 | 67 | override fun onCreate(savedInstanceState: Bundle?) { 68 | AndroidInjection.inject(this) 69 | 70 | super.onCreate(savedInstanceState) 71 | 72 | relaunchIntent = intent.getParcelableExtra(AppSwitcher.KEY_RESTART_INTENT) 73 | 74 | Log.d(TAG, "STUFFING -- activity created") 75 | 76 | val intentFilter = IntentFilter() 77 | intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED) 78 | intentFilter.addDataScheme("package"); 79 | 80 | applicationContext.registerReceiver(broadcastReceiver, intentFilter) 81 | handler.postDelayed(finishRunnable, TOGGLE_TIMEOUT_MILLISECONDS) 82 | 83 | configuration.get()?.let { 84 | setContentView(it.appSwitchActivityResId) 85 | configuration.get().startAppWarmUp() 86 | } 87 | } 88 | 89 | override fun onPause() { 90 | super.onPause() 91 | 92 | finish() 93 | } 94 | 95 | override fun onDestroy() { 96 | super.onDestroy() 97 | 98 | Log.d(TAG, "STUFFING -- activity onDestroy shouldRelaunch=$shouldRelaunch relaunchIntent=$relaunchIntent") 99 | 100 | handler.removeCallbacks(finishRunnable) 101 | applicationContext.unregisterReceiver(broadcastReceiver) 102 | 103 | if (shouldRelaunch) { 104 | AppSwitcher.endIntentToSwitchApp(applicationContext, relaunchIntent) 105 | } else { 106 | AppSwitcher.abortIntentToSwitchApp() 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/AppSwitcher.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.Intent.FLAG_ACTIVITY_NEW_TASK 6 | 7 | /** 8 | * Provides an entry point for invoking the flow to switch applications. 9 | * 10 | * [beginIntentToSwitchApp] should be invoked from the initial Application, and [endIntentToSwitchApp] will be invoked 11 | * by the [AppSwitchActivity] when it determines that the OS has completed the necessary updates to switch applications. 12 | */ 13 | internal class AppSwitcher { 14 | 15 | companion object { 16 | const val KEY_RESTART_INTENT = "RESTART_INTENTS" 17 | 18 | /** 19 | * Invoke from the initial application to start the app switch flow. The calling process will be killed, and 20 | * a [AppSwitchActivity] will be launched in a new process to manage the transition to the new activity. 21 | */ 22 | fun beginIntentToSwitchApp(context: Context, intent: Intent? = null) { 23 | val switcherIntent = Intent(context, AppSwitchActivity::class.java) 24 | 25 | switcherIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) 26 | intent?.let { 27 | switcherIntent.putExtra(KEY_RESTART_INTENT, it) 28 | } 29 | 30 | context.startActivity(switcherIntent) 31 | Runtime.getRuntime().exit(0) 32 | } 33 | 34 | /** 35 | * Invoked from the [AppSwitchActivity] process to complete the transition to the new Application. 36 | */ 37 | fun endIntentToSwitchApp(context: Context, intent: Intent) { 38 | context.startActivity(intent) 39 | Runtime.getRuntime().exit(0) 40 | } 41 | 42 | /** 43 | * Aborts the app switch intent. The app will still have switched, but it will not be launched automatically 44 | * after this point. 45 | */ 46 | fun abortIntentToSwitchApp() { 47 | Runtime.getRuntime().exit(0) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/BaseDynamicAppManager.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import com.snap.stuffing.api.DynamicAppManager 6 | 7 | /** 8 | * The maximum number of failed toggle attempt to retry. 9 | */ 10 | internal const val MAX_FAILED_ATTEMPT_COUNT = 3 11 | 12 | /** 13 | * Base for all dynamic app managers 14 | */ 15 | abstract class BaseDynamicAppManager(private val appContext: Context, private val tag: String): DynamicAppManager { 16 | 17 | internal val appComponentModifier = AppComponentModifier(appContext) 18 | 19 | internal val preferences: SharedPreferences by lazy { 20 | appContext.getSharedPreferences(DynamicAppManagerPrefs.dynamicAppConfig, Context.MODE_PRIVATE) 21 | } 22 | 23 | internal val appVersionCode: Int by lazy { 24 | appContext.packageManager.getPackageInfo(appContext.packageName, 0).versionCode 25 | } 26 | 27 | internal var failedToggleAttemptCount = 0 28 | 29 | } -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/DynamicAppManagerPrefs.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | /** 4 | * Prefs for the dynamic app manager 5 | */ 6 | internal object DynamicAppManagerPrefs { 7 | val dynamicAppConfig = "dynamicAppConfig" 8 | val systemVersionKey = "systemVersion" 9 | val appFamilyChangeSignalKey = "appFamilyChangeSignal" 10 | val appFamilyKey = "appFamily" 11 | val expectedAppFamilyKey = "expectedAppFamily" 12 | val previousAppFamilyKey = "previousAppFamily" 13 | val previousVersionKey = "previousAppVersion" 14 | val failedToggleAttemptCountKey = "failedToggleAttemptCount" 15 | } -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/DynamicLaunchActivity.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib; 2 | 3 | import android.app.Activity; 4 | import android.content.ComponentName; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.support.annotation.NonNull; 8 | import android.support.annotation.Nullable; 9 | import android.util.Log; 10 | 11 | import javax.inject.Inject; 12 | 13 | import com.snap.stuffing.api.DynamicAppManager; 14 | import dagger.android.AndroidInjection; 15 | 16 | /** 17 | * An {@link Activity} that will act as the primary activity of the application. 18 | * 19 | * The Android launcher launches a specific Activity. Given that the specific Activity type is bound to its parent application, 20 | * Stuffing provides a layer of indirection/routing when launching activities, which is this special activity. 21 | */ 22 | public class DynamicLaunchActivity extends Activity { 23 | private static final String TAG = "DynamicLaunchActivity"; 24 | 25 | @Inject DynamicAppManager dynamicAppManager; 26 | 27 | @Override 28 | protected void onCreate(@Nullable Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | 31 | Log.d(TAG, String.format("onCreate %s", savedInstanceState)); 32 | 33 | AndroidInjection.inject(this); 34 | 35 | final String defaultActivityClassName = dynamicAppManager.getDefaultActivityClassName(); 36 | if (defaultActivityClassName == null) { 37 | throw new IllegalArgumentException( 38 | "No default activity for appFamily " + dynamicAppManager.getApplicationFamily()); 39 | } else { 40 | launchActivity(defaultActivityClassName); 41 | } 42 | } 43 | 44 | @Override 45 | protected void onResume() { 46 | super.onResume(); 47 | Log.d(TAG, "onResume"); 48 | } 49 | 50 | @Override 51 | protected void onDestroy() { 52 | super.onDestroy(); 53 | Log.d(TAG, "onDestroy"); 54 | } 55 | 56 | @Override 57 | public void onNewIntent(Intent intent) { 58 | super.onNewIntent(intent); 59 | Log.d(TAG, String.format("onNewIntent %s", intent)); 60 | } 61 | 62 | private void launchActivity(@NonNull String activityClassName) { 63 | try { 64 | launchActivity(Class.forName(activityClassName)); 65 | } catch (ClassNotFoundException e) { 66 | Log.e(TAG, "Failed to start activity " + activityClassName, e); 67 | } 68 | } 69 | 70 | private void launchActivity(@NonNull Class activityClass) { 71 | final Intent intent; 72 | final Intent existingIntent = getIntent(); 73 | if (existingIntent != null) { 74 | // If this activity was launched with a specific intent, remap it to the target activity class and 75 | // launch it. 76 | intent = (Intent) existingIntent.clone(); 77 | intent.setComponent(new ComponentName(this, activityClass)); 78 | } else { 79 | // If the activity wasn't launched with a specific intent, then just launch the target activity. 80 | intent = new Intent(this, activityClass); 81 | } 82 | Log.d(TAG, String.format("Dynamically launching activity class: %s with intent %s", activityClass, intent)); 83 | startActivity(intent); 84 | finish(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/MultiDynamicAppManager.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import com.jakewharton.processphoenix.ProcessPhoenix 8 | import com.snap.stuffing.api.AppSwitchHook 9 | import com.snap.stuffing.api.DynamicAppConfig 10 | 11 | private const val TAG = "MultiDynamicAppManager" 12 | 13 | /** 14 | * Internal version code used to invalidate stored preferences for breaking changes. 15 | * 16 | * Version 1: Switched from using DynamicLaunchActivity to enabling/disabling activities. 17 | * Version 2: Add new metadata. 18 | * Version 1: Switched back to DynamicLaunchActivity. 19 | */ 20 | private const val SYSTEM_VERSION = 2 21 | 22 | /** 23 | * Manages multiple "[Application]s" within a single APK. 24 | */ 25 | class MultiDynamicAppManager( 26 | private val appContext: Context, 27 | private val defaultAppFamily: String, 28 | private val config: DynamicAppConfig, 29 | private val appSwitchHook: AppSwitchHook): BaseDynamicAppManager(appContext, TAG) { 30 | 31 | private var hasAppFamilyChangeSignal = false 32 | 33 | override val active: Boolean = true 34 | 35 | override var applicationFamily: String = "" 36 | private set 37 | 38 | override fun initialize() { 39 | hasAppFamilyChangeSignal = preferences.getBoolean(DynamicAppManagerPrefs.appFamilyChangeSignalKey, false) 40 | val currentAppFamily = preferences.getString(DynamicAppManagerPrefs.appFamilyKey, null) 41 | val expectedAppFamily = preferences.getString(DynamicAppManagerPrefs.expectedAppFamilyKey, null) 42 | val previousAppVersionCode = preferences.getInt(DynamicAppManagerPrefs.previousVersionKey, 0) 43 | val systemVersion = preferences.getInt(DynamicAppManagerPrefs.systemVersionKey, 0) 44 | val hadComponentsModified = appComponentModifier.checkComponentListModifiedState() 45 | failedToggleAttemptCount = preferences.getInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, 0) 46 | 47 | Log.d(TAG, "STUFFING -- Initializing with state: " + 48 | "previousAppVersionCode=$previousAppVersionCode, appVersionCode=$appVersionCode, " + 49 | "hasAppFamilyChangeSignal=$hasAppFamilyChangeSignal currentAppFamily=$currentAppFamily, " + 50 | "expectedAppFamily=$expectedAppFamily, " + 51 | "systemVersion=$systemVersion, hadComponentsModified=$hadComponentsModified, " + 52 | "failedToggleAttemptCount=$failedToggleAttemptCount") 53 | 54 | if (systemVersion != SYSTEM_VERSION || currentAppFamily.isNullOrEmpty() || 55 | (failedToggleAttemptCount in 1..MAX_FAILED_ATTEMPT_COUNT)) { 56 | 57 | val componentsModified = setCurrentAppFamily(defaultAppFamily, !hadComponentsModified) 58 | if (hadComponentsModified && componentsModified) { 59 | Log.d(TAG, "Rebooting the application since the appFamily changed some components") 60 | ProcessPhoenix.triggerRebirth(appContext) 61 | } 62 | 63 | } else { 64 | applicationFamily = currentAppFamily 65 | 66 | // When the app family hasn't changed, check if the app version was updated. 67 | // If it was, we might need to refresh the state of newly added or removed manifest components. 68 | if (appVersionCode != previousAppVersionCode) { 69 | Log.d(TAG, "STUFFING -- appVersionCode changed from $previousAppVersionCode to $appVersionCode, refreshing manifest components") 70 | refreshManifestComponentsOnInconsistency() 71 | } else if (currentAppFamily != expectedAppFamily) { 72 | Log.d(TAG, "STUFFING -- currentAppFamily $currentAppFamily does not match expectedAppFamily $expectedAppFamily, refreshing manifest components") 73 | refreshManifestComponentsOnInconsistency() 74 | } 75 | } 76 | 77 | // This method is costly, only enable it while debugging locally. 78 | //appComponentModifier.printComponentEnabledStates(TAG) 79 | } 80 | 81 | override fun onEvent(eventName: String) { 82 | config.events[eventName]?.run() 83 | } 84 | 85 | override fun getDefaultActivityClassName() = appComponentModifier.getDefaultActivityClassNameForAppFamily(applicationFamily) 86 | 87 | override fun returnToDefaultFamily() { 88 | Log.d(TAG, "STUFFING -- returnToDefaultFamily") 89 | 90 | setCurrentAppFamily(defaultAppFamily, false) 91 | } 92 | 93 | override fun switchToAppFamily(appFamily: String, useSwitchActivity: Boolean, launchIntent: Intent?) { 94 | Log.d(TAG, "STUFFING -- switchToAppFamily $appFamily") 95 | 96 | setCurrentAppFamily(appFamily, false) 97 | 98 | val relaunchIntent = appComponentModifier.getLaunchIntentForAppFamily(applicationFamily)?.apply { 99 | if (launchIntent != null && launchIntent.extras != null) { 100 | putExtras(launchIntent) 101 | } 102 | } 103 | 104 | if (useSwitchActivity) { 105 | if (relaunchIntent != null) { 106 | AppSwitcher.beginIntentToSwitchApp(appContext, relaunchIntent) 107 | } else { 108 | AppSwitcher.beginIntentToSwitchApp(appContext) 109 | } 110 | } else { 111 | if (relaunchIntent != null) { 112 | AppSwitcher.endIntentToSwitchApp(appContext, relaunchIntent) 113 | } else { 114 | ProcessPhoenix.triggerRebirth(appContext) 115 | } 116 | } 117 | } 118 | 119 | override fun hasPendingAppFamilyChangeSignal() = hasAppFamilyChangeSignal 120 | 121 | override fun consumePendingAppFamilyChangeSignal() { 122 | Log.d(TAG, "STUFFING -- Consuming pending appFamilyChangeSignal") 123 | 124 | hasAppFamilyChangeSignal = false 125 | preferences.edit().putBoolean(DynamicAppManagerPrefs.appFamilyChangeSignalKey, false).apply() 126 | } 127 | 128 | private fun setCurrentAppFamily(appFamily: String, isFirstTimeSettingAppFamily: Boolean): Boolean { 129 | // Already in this app family, return 130 | if (appFamily == this.applicationFamily) { 131 | Log.d(TAG, "STUFFING -- Already in ${this.applicationFamily}") 132 | return false 133 | } 134 | 135 | Log.d(TAG, "STUFFING -- Switching app families from ${this.applicationFamily} to $appFamily, appVersionCode=$appVersionCode") 136 | 137 | // Inform custom hook that we're about to switch 138 | appSwitchHook.preAppFamilySwitch(this.applicationFamily, appFamily) 139 | 140 | preferences.edit() 141 | .putInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, failedToggleAttemptCount + 1) 142 | .commit() 143 | 144 | // Update the manifest components for this app family 145 | val componentsModified = appComponentModifier.switchToAppFamily(appFamily, TAG) 146 | 147 | // Clear preferences and set all of the values to an initial state. 148 | preferences.edit() 149 | .clear() 150 | .putString(DynamicAppManagerPrefs.appFamilyKey, appFamily) 151 | .putString(DynamicAppManagerPrefs.expectedAppFamilyKey, appFamily) 152 | .putString(DynamicAppManagerPrefs.previousAppFamilyKey, this.applicationFamily) 153 | .putBoolean(DynamicAppManagerPrefs.appFamilyChangeSignalKey, !isFirstTimeSettingAppFamily) 154 | .putInt(DynamicAppManagerPrefs.previousVersionKey, appVersionCode) 155 | .putInt(DynamicAppManagerPrefs.systemVersionKey, SYSTEM_VERSION) 156 | .putInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, 0) 157 | .commit() 158 | 159 | applicationFamily = appFamily 160 | return componentsModified 161 | } 162 | 163 | /** 164 | * If the app version has changed, it's possible that manifest components were added that 165 | * don't belong to the current app family, and need to be updated. 166 | */ 167 | internal fun refreshManifestComponentsOnInconsistency() { 168 | Log.d(TAG, "refreshManifestComponentsOnInconsistency: $applicationFamily") 169 | 170 | preferences.edit() 171 | .putInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, failedToggleAttemptCount + 1) 172 | .commit() 173 | 174 | appComponentModifier.switchToAppFamily(applicationFamily, TAG) 175 | 176 | preferences.edit() 177 | .putString(DynamicAppManagerPrefs.expectedAppFamilyKey, applicationFamily) 178 | .putInt(DynamicAppManagerPrefs.previousVersionKey, appVersionCode) 179 | .putInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, 0) 180 | .commit() 181 | } 182 | } -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/SingleDynamicAppManager.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.util.Log 6 | import com.snap.stuffing.api.DynamicAppManager 7 | 8 | private const val TAG = "SingleDynamicAppManager" 9 | /** 10 | * A [DynamicAppManager] implementation for application that are not actually dynamic, and contain only a single 11 | * application. 12 | */ 13 | internal class SingleDynamicAppManager constructor(appContext: Context, 14 | private val defaultActivityClassName: String, 15 | private val defaultAppFamily: String 16 | ): BaseDynamicAppManager(appContext, TAG) { 17 | 18 | override val active: Boolean = false 19 | 20 | override val applicationFamily = defaultAppFamily 21 | 22 | override fun getDefaultActivityClassName(): String? = defaultActivityClassName 23 | 24 | override fun initialize() { 25 | val currentAppFamily = preferences.getString(DynamicAppManagerPrefs.appFamilyKey, "") 26 | failedToggleAttemptCount = preferences.getInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, 0) 27 | 28 | Log.d(TAG, "STUFFING -- Initializing with state: " + 29 | "currentAppFamily=$currentAppFamily, " + 30 | "expectedAppFamily=$applicationFamily, " + 31 | "failedToggleAttemptCount=$failedToggleAttemptCount") 32 | 33 | if (currentAppFamily != applicationFamily && failedToggleAttemptCount < MAX_FAILED_ATTEMPT_COUNT) { 34 | preferences.edit() 35 | .putInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, failedToggleAttemptCount + 1) 36 | .commit() 37 | 38 | appComponentModifier.resetAllComponents(TAG) 39 | 40 | preferences.edit() 41 | .putString(DynamicAppManagerPrefs.appFamilyKey, applicationFamily) 42 | .putString(DynamicAppManagerPrefs.expectedAppFamilyKey, applicationFamily) 43 | .putInt(DynamicAppManagerPrefs.previousVersionKey, appVersionCode) 44 | .putInt(DynamicAppManagerPrefs.failedToggleAttemptCountKey, 0) 45 | .putString(DynamicAppManagerPrefs.previousAppFamilyKey, currentAppFamily) 46 | .commit() 47 | } 48 | } 49 | 50 | override fun onEvent(eventName: String) { } 51 | 52 | override fun returnToDefaultFamily() { } 53 | 54 | override fun switchToAppFamily(appFamily: String, useSwitchActivity: Boolean, launchIntent: Intent?) { } 55 | 56 | override fun hasPendingAppFamilyChangeSignal() = false 57 | 58 | override fun consumePendingAppFamilyChangeSignal() { } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/com/snap/stuffing/lib/StateTrackingService.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.IBinder 7 | 8 | /** 9 | * An empty, no-op service that is simply used for help track whether the [AppComponentModifier] was ever used to 10 | * modify the state of components in the application. 11 | * 12 | * The service uses a dummy app family, so the first time [AppComponentModifier.switchToAppFamily] is called, the 13 | * user enabled state for this service will change from [PackageManager.COMPONENT_ENABLED_STATE_DEFAULT] to 14 | * [PackageManager.COMPONENT_ENABLED_STATE_DISABLED]. This is used as an indication that components have been modified. 15 | */ 16 | class StateTrackingService: Service() { 17 | 18 | override fun onBind(intent: Intent?): IBinder? { 19 | // Not needed -- this service is not actually used. 20 | return null 21 | } 22 | } -------------------------------------------------------------------------------- /core/src/test/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /core/src/test/java/com/snap/stuffing/lib/MultiDynamicAppManagerTest.kt: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.lib 2 | 3 | import android.content.Context 4 | import com.snap.framework.developer.BuildConfigInfo 5 | import com.snap.stuffing.api.AppSwitchHook 6 | import com.snap.stuffing.api.DynamicAppConfig 7 | import org.junit.Assert 8 | import org.junit.Test 9 | import org.mockito.Mockito.mock 10 | 11 | private const val TEST_EVENT = "test_event" 12 | 13 | class MultiDynamicAppManagerTest { 14 | 15 | private val context = mock(Context::class.java) 16 | 17 | @Test 18 | fun testEventInvocation() { 19 | var called = false 20 | val runnable = Runnable { 21 | called = true 22 | } 23 | val config = DynamicAppConfig(mapOf(TEST_EVENT to runnable)) 24 | val subject = MultiDynamicAppManager(context, BuildConfigInfo(), "", config, AppSwitchHook { fromAppFamily, toAppFamily -> }) 25 | subject.onEvent(TEST_EVENT) 26 | 27 | Assert.assertTrue(called) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ext.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.versions = [ 3 | 'supportLib' : '28.0.0', 4 | // This should be consistent with kotlin gradle plugin in root build.gradle 5 | 'kotlin' : '1.3.40', 6 | 'dagger' : '2.22', 7 | ] 8 | } 9 | 10 | allprojects { 11 | ext { 12 | deps = [ 13 | 'javax' : [ 14 | 'annotation': "javax.annotation:javax.annotation-api:1.3", 15 | 'inject' : 'javax.inject:javax.inject:1', 16 | ], 17 | 'support': [ 18 | 'annotations': "com.android.support:support-annotations:${versions.supportLib}", 19 | 'v7' : "com.android.support:appcompat-v7:${versions.supportLib}", 20 | ], 21 | 'kotlin': [ 22 | 'stdLib_jdk7': "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}", 23 | ], 24 | 'dagger': [ 25 | 'runtime': "com.google.dagger:dagger:${versions.dagger}", 26 | 'compiler': "com.google.dagger:dagger-compiler:${versions.dagger}", 27 | 'android': "com.google.dagger:dagger-android:${versions.dagger}", 28 | 'android_support': "com.google.dagger:dagger-android-support:${versions.dagger}", 29 | 'android_processor': "com.google.dagger:dagger-android-processor:${versions.dagger}", 30 | ], 31 | 'processPhoenix': "com.jakewharton:process-phoenix:2.0.0" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # Kotlin code style for this project: "official" or "obsolete": 20 | kotlin.code.style=official 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snapchat/stuffing/29a206b001e340cb04ee951f34551a1bb76e75e6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Aug 21 09:35:05 PDT 2019 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-5.4.1-all.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 | -------------------------------------------------------------------------------- /sample/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 28 7 | 8 | defaultConfig { 9 | versionCode 1 10 | versionName "10.30.0.12" 11 | minSdkVersion 19 12 | targetSdkVersion 28 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | } 19 | } 20 | 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | } 26 | 27 | android { 28 | defaultConfig { 29 | applicationId "com.snap.stuffing.sample" 30 | buildConfigField "int", "EXOPACKAGE_FLAGS", "0" 31 | } 32 | } 33 | 34 | dependencies { 35 | kapt deps.dagger.android_processor 36 | kapt deps.dagger.compiler 37 | 38 | implementation deps.support.v7 39 | implementation deps.dagger.android 40 | implementation deps.dagger.android_support 41 | implementation deps.kotlin.stdLib_jdk7 42 | 43 | implementation project(':core') 44 | } 45 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifestBuck.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/SampleAppShell.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample; 2 | 3 | import android.app.Activity; 4 | 5 | import com.snap.stuffing.api.exopackage.ExopackageApplication; 6 | 7 | import dagger.android.AndroidInjector; 8 | import dagger.android.HasActivityInjector; 9 | 10 | public class SampleAppShell extends ExopackageApplication implements HasActivityInjector { 11 | 12 | public SampleAppShell() { 13 | super("com.snap.stuffing.sample.SampleDelegatingApplicationLike", BuildConfig.EXOPACKAGE_FLAGS); 14 | } 15 | 16 | @Override 17 | public AndroidInjector activityInjector() { 18 | return ((HasActivityInjector) getDelegateIfPresent()).activityInjector(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/SampleAppSwitchConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample; 2 | 3 | import com.snap.stuffing.api.AppSwitchConfiguration; 4 | 5 | import android.support.annotation.NonNull; 6 | 7 | public class SampleAppSwitchConfiguration implements AppSwitchConfiguration { 8 | 9 | @NonNull private final String name; 10 | 11 | public SampleAppSwitchConfiguration(@NonNull String name) { 12 | this.name = name; 13 | } 14 | 15 | @Override 16 | public int getAppSwitchActivityResId() { 17 | return R.layout.activity_app_switch; 18 | } 19 | 20 | @Override 21 | public void startAppWarmUp() { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/SampleDelegatingApplicationLike.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample; 2 | 3 | import com.snap.stuffing.api.DynamicAppConfig; 4 | import com.snap.stuffing.api.exopackage.ApplicationLike; 5 | import com.snap.stuffing.bindings.ApplicationComponentOwner; 6 | import com.snap.stuffing.bindings.DelegatingApplicationLike; 7 | import com.snap.stuffing.bindings.DynamicAppModule; 8 | import com.snap.stuffing.sample.first.FirstApplication; 9 | import com.snap.stuffing.sample.second.SecondApplication; 10 | 11 | import android.app.Application; 12 | import android.support.annotation.NonNull; 13 | 14 | import java.util.Collections; 15 | 16 | /** 17 | * A {@link DelegatingApplicationLike} implementation used to select between the two sample applications. 18 | */ 19 | public class SampleDelegatingApplicationLike extends DelegatingApplicationLike { 20 | 21 | public SampleDelegatingApplicationLike(@NonNull Application application) { 22 | super(application); 23 | } 24 | 25 | @NonNull 26 | @Override 27 | protected ApplicationLike createApplication() { 28 | final DynamicAppModule dynamicAppModule = 29 | DynamicAppModule.makeMultiAppModule(mApplication, 30 | "first", 31 | new DynamicAppConfig(Collections.emptyMap()), 32 | (fromAppFamily, toAppFamily) -> { 33 | // no-op 34 | }); 35 | 36 | dynamicAppModule.dynamicAppManager().initialize(); 37 | 38 | final ApplicationLike applicationLike; 39 | if ("second".equals(dynamicAppModule.dynamicAppManager().getApplicationFamily())) { 40 | applicationLike = new SecondApplication(mApplication); 41 | } else { 42 | applicationLike = new FirstApplication(mApplication); 43 | } 44 | 45 | if (applicationLike instanceof ApplicationComponentOwner) { 46 | ((ApplicationComponentOwner) applicationLike).attachDynamicAppModule(dynamicAppModule); 47 | } 48 | 49 | return applicationLike; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/first/FirstActivity.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.first; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.widget.Button; 6 | import android.widget.TextView; 7 | 8 | import com.snap.stuffing.api.DynamicAppManager; 9 | import com.snap.stuffing.sample.R; 10 | import dagger.android.AndroidInjection; 11 | 12 | import javax.inject.Inject; 13 | 14 | public class FirstActivity extends AppCompatActivity { 15 | 16 | @Inject DynamicAppManager dynamicAppManager; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | AndroidInjection.inject(this); 22 | setContentView(R.layout.main); 23 | 24 | final TextView descriptionTextView = findViewById(R.id.module_description_text); 25 | descriptionTextView.setText("First"); 26 | 27 | final Button switchButton = findViewById(R.id.switch_app_button); 28 | switchButton.setOnClickListener(v -> dynamicAppManager.switchToAppFamily("second", true,null)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/first/FirstActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.first; 2 | 3 | import com.snap.stuffing.api.AppSwitchConfiguration; 4 | import com.snap.stuffing.bindings.AppSwitchActivityModule; 5 | import com.snap.stuffing.bindings.DynamicActivityModule; 6 | import com.snap.stuffing.sample.SampleAppSwitchConfiguration; 7 | import dagger.Module; 8 | import dagger.Provides; 9 | import dagger.android.ContributesAndroidInjector; 10 | 11 | @Module(includes = { DynamicActivityModule.class, AppSwitchActivityModule.class}) 12 | public abstract class FirstActivityModule { 13 | @ContributesAndroidInjector 14 | public abstract FirstActivity contributeMainActivityInjector(); 15 | 16 | @Provides 17 | public static AppSwitchConfiguration provideAppSwitchConfiguration() { 18 | return new SampleAppSwitchConfiguration("First"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/first/FirstApplication.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.first; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.support.annotation.NonNull; 6 | 7 | import com.snap.stuffing.api.exopackage.DefaultApplicationLike; 8 | import com.snap.stuffing.bindings.ApplicationComponentOwner; 9 | import com.snap.stuffing.bindings.DynamicAppModule; 10 | import dagger.android.AndroidInjector; 11 | import dagger.android.DispatchingAndroidInjector; 12 | import dagger.android.HasActivityInjector; 13 | 14 | import javax.inject.Inject; 15 | 16 | public class FirstApplication extends DefaultApplicationLike implements HasActivityInjector, ApplicationComponentOwner { 17 | private final Application app; 18 | 19 | private FirstApplicationComponent appComponent; 20 | private DynamicAppModule dynamicAppModule; 21 | 22 | @Inject DispatchingAndroidInjector dispatchingActivityInjector; 23 | 24 | public FirstApplication(Application app) { 25 | this.app = app; 26 | } 27 | 28 | @Override 29 | public void onCreate() { 30 | appComponent = DaggerFirstApplicationComponent 31 | .builder() 32 | .dynamicAppModule(dynamicAppModule) 33 | .build(); 34 | appComponent.inject(this); 35 | } 36 | 37 | @Override 38 | public AndroidInjector activityInjector() { 39 | return dispatchingActivityInjector; 40 | } 41 | 42 | @Override 43 | public void attachDynamicAppModule(@NonNull DynamicAppModule dynamicAppModule) { 44 | this.dynamicAppModule = dynamicAppModule; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/first/FirstApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.first; 2 | 3 | import com.snap.stuffing.bindings.DynamicAppModule; 4 | import dagger.Component; 5 | import dagger.android.AndroidInjectionModule; 6 | 7 | import javax.inject.Singleton; 8 | 9 | @Singleton 10 | @Component(modules = { 11 | DynamicAppModule.class, 12 | FirstActivityModule.class, 13 | AndroidInjectionModule.class 14 | }) 15 | public interface FirstApplicationComponent { 16 | 17 | void inject(FirstApplication application); 18 | } 19 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/second/SecondActivity.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.second; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.widget.Button; 6 | import android.widget.TextView; 7 | 8 | import com.snap.stuffing.api.DynamicAppManager; 9 | import com.snap.stuffing.sample.R; 10 | import dagger.android.AndroidInjection; 11 | 12 | import javax.inject.Inject; 13 | 14 | public class SecondActivity extends AppCompatActivity { 15 | 16 | @Inject DynamicAppManager dynamicAppManager; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | AndroidInjection.inject(this); 22 | setContentView(R.layout.main); 23 | 24 | final TextView descriptionTextView = findViewById(R.id.module_description_text); 25 | descriptionTextView.setText("Second"); 26 | 27 | final Button switchButton = findViewById(R.id.switch_app_button); 28 | switchButton.setOnClickListener(v -> { 29 | dynamicAppManager.switchToAppFamily("first", true, null); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/second/SecondActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.second; 2 | 3 | import com.snap.stuffing.api.AppSwitchConfiguration; 4 | import com.snap.stuffing.bindings.AppSwitchActivityModule; 5 | import com.snap.stuffing.bindings.DynamicActivityModule; 6 | import com.snap.stuffing.sample.SampleAppSwitchConfiguration; 7 | import dagger.Module; 8 | import dagger.Provides; 9 | import dagger.android.ContributesAndroidInjector; 10 | 11 | @Module(includes = { DynamicActivityModule.class, AppSwitchActivityModule.class}) 12 | public abstract class SecondActivityModule { 13 | @ContributesAndroidInjector 14 | public abstract SecondActivity contributeMainActivityInjector(); 15 | 16 | @Provides 17 | public static AppSwitchConfiguration provideAppSwitchConfiguration() { 18 | return new SampleAppSwitchConfiguration("Second"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/second/SecondApplication.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.second; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.support.annotation.NonNull; 6 | 7 | import javax.inject.Inject; 8 | 9 | import com.snap.stuffing.api.exopackage.DefaultApplicationLike; 10 | import com.snap.stuffing.bindings.ApplicationComponentOwner; 11 | import com.snap.stuffing.bindings.DynamicAppModule; 12 | import dagger.android.AndroidInjector; 13 | import dagger.android.DispatchingAndroidInjector; 14 | import dagger.android.HasActivityInjector; 15 | 16 | public class SecondApplication extends DefaultApplicationLike implements HasActivityInjector, ApplicationComponentOwner { 17 | private final Application app; 18 | 19 | private SecondApplicationComponent appComponent; 20 | private DynamicAppModule dynamicAppModule; 21 | 22 | @Inject DispatchingAndroidInjector dispatchingActivityInjector; 23 | 24 | public SecondApplication(Application app) { 25 | this.app = app; 26 | } 27 | 28 | @Override 29 | public void onCreate() { 30 | appComponent = DaggerSecondApplicationComponent 31 | .builder() 32 | .dynamicAppModule(dynamicAppModule) 33 | .build(); 34 | appComponent.inject(this); 35 | } 36 | 37 | @Override 38 | public AndroidInjector activityInjector() { 39 | return dispatchingActivityInjector; 40 | } 41 | 42 | @Override 43 | public void attachDynamicAppModule(@NonNull DynamicAppModule dynamicAppModule) { 44 | this.dynamicAppModule = dynamicAppModule; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sample/src/main/java/com/snap/stuffing/sample/second/SecondApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package com.snap.stuffing.sample.second; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import com.snap.stuffing.bindings.DynamicAppModule; 6 | import dagger.Component; 7 | import dagger.android.AndroidInjectionModule; 8 | 9 | @Singleton 10 | @Component(modules = { 11 | DynamicAppModule.class, 12 | SecondActivityModule.class, 13 | AndroidInjectionModule.class 14 | }) 15 | public interface SecondApplicationComponent { 16 | 17 | void inject(SecondApplication application); 18 | } 19 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_app_switch.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 |