1 /* 2 * Copyright 2013, Leanplum, Inc. All rights reserved. 3 * 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 package com.leanplum; 23 24 import android.app.Activity; 25 import android.app.Application; 26 import android.app.Application.ActivityLifecycleCallbacks; 27 import android.content.res.Resources; 28 import android.os.Build; 29 import android.os.Bundle; 30 31 import com.leanplum.annotations.Parser; 32 import com.leanplum.callbacks.PostponableAction; 33 import com.leanplum.internal.ActionManager; 34 import com.leanplum.internal.LeanplumInternal; 35 import com.leanplum.internal.LeanplumUIEditorWrapper; 36 import com.leanplum.internal.OsHandler; 37 import com.leanplum.internal.Util; 38 39 import java.util.Collections; 40 import java.util.HashSet; 41 import java.util.LinkedList; 42 import java.util.Queue; 43 import java.util.Set; 44 45 /** 46 * Utility class for handling activity lifecycle events. Call these methods from your activity if 47 * you don't extend one of the Leanplum*Activity classes. 48 * 49 * @author Andrew First 50 */ 51 public class LeanplumActivityHelper { 52 /** 53 * Whether any of the activities are paused. 54 */ 55 static boolean isActivityPaused; 56 private static Set<Class> ignoredActivityClasses; 57 58 /** 59 * Whether lifecycle callbacks were registered. This is only supported on Android OS >= 4.0. 60 */ 61 private static boolean registeredCallbacks; 62 63 static Activity currentActivity; 64 65 private final Activity activity; 66 private LeanplumResources res; 67 private LeanplumInflater inflater; 68 69 private static final Queue<Runnable> pendingActions = new LinkedList<>(); 70 private static final Runnable runPendingActionsRunnable = new Runnable() { 71 @Override 72 public void run() { 73 runPendingActions(); 74 } 75 }; 76 LeanplumActivityHelper(Activity activity)77 public LeanplumActivityHelper(Activity activity) { 78 this.activity = activity; 79 Leanplum.setApplicationContext(activity.getApplicationContext()); 80 Parser.parseVariables(activity); 81 } 82 83 /** 84 * Retrieves the currently active activity. 85 */ getCurrentActivity()86 public static Activity getCurrentActivity() { 87 return currentActivity; 88 } 89 90 /** 91 * Retrieves if the activity is paused. 92 */ isActivityPaused()93 public static boolean isActivityPaused() { 94 return isActivityPaused; 95 } 96 97 /** 98 * Enables lifecycle callbacks for Android devices with Android OS >= 4.0 99 */ enableLifecycleCallbacks(final Application app)100 public static void enableLifecycleCallbacks(final Application app) { 101 Leanplum.setApplicationContext(app.getApplicationContext()); 102 if (Build.VERSION.SDK_INT < 14) { 103 return; 104 } 105 app.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { 106 @Override 107 public void onActivityStopped(Activity activity) { 108 try { 109 onStop(activity); 110 } catch (Throwable t) { 111 Util.handleException(t); 112 } 113 } 114 115 @Override 116 public void onActivityResumed(final Activity activity) { 117 try { 118 if (Leanplum.isInterfaceEditingEnabled()) { 119 // Execute runnable in next frame to ensure that all system stuff is setup, before 120 // applying UI edits. 121 OsHandler.getInstance().post(new Runnable() { 122 @Override 123 public void run() { 124 LeanplumUIEditorWrapper.getInstance().applyInterfaceEdits(activity); 125 } 126 }); 127 } 128 onResume(activity); 129 if (Leanplum.isScreenTrackingEnabled()) { 130 Leanplum.advanceTo(activity.getLocalClassName()); 131 } 132 } catch (Throwable t) { 133 Util.handleException(t); 134 } 135 } 136 137 @Override 138 public void onActivityPaused(Activity activity) { 139 try { 140 onPause(activity); 141 } catch (Throwable t) { 142 Util.handleException(t); 143 } 144 } 145 146 @Override 147 public void onActivityStarted(Activity activity) { 148 } 149 150 @Override 151 public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 152 } 153 154 @Override 155 public void onActivityDestroyed(Activity activity) { 156 } 157 158 @Override 159 public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 160 } 161 162 }); 163 registeredCallbacks = true; 164 } 165 getLeanplumResources()166 public LeanplumResources getLeanplumResources() { 167 return getLeanplumResources(null); 168 } 169 getLeanplumResources(Resources baseResources)170 public LeanplumResources getLeanplumResources(Resources baseResources) { 171 if (res != null) { 172 return res; 173 } 174 if (baseResources == null) { 175 baseResources = activity.getResources(); 176 } 177 if (baseResources instanceof LeanplumResources) { 178 return (LeanplumResources) baseResources; 179 } 180 res = new LeanplumResources(baseResources); 181 return res; 182 } 183 184 /** 185 * Sets the view from a layout file. 186 */ setContentView(final int layoutResID)187 public void setContentView(final int layoutResID) { 188 if (inflater == null) { 189 inflater = LeanplumInflater.from(activity); 190 } 191 activity.setContentView(inflater.inflate(layoutResID)); 192 } 193 194 @SuppressWarnings("unused") onPause(Activity activity)195 private static void onPause(Activity activity) { 196 isActivityPaused = true; 197 } 198 199 /** 200 * Call this when your activity gets paused. 201 */ onPause()202 public void onPause() { 203 try { 204 if (!registeredCallbacks) { 205 onPause(activity); 206 } 207 } catch (Throwable t) { 208 Util.handleException(t); 209 } 210 } 211 onResume(Activity activity)212 public static void onResume(Activity activity) { 213 isActivityPaused = false; 214 currentActivity = activity; 215 if (LeanplumInternal.isPaused() || LeanplumInternal.hasStartedInBackground()) { 216 Leanplum.resume(); 217 LocationManager locationManager = ActionManager.getLocationManager(); 218 if (locationManager != null) { 219 locationManager.updateGeofencing(); 220 locationManager.updateUserLocation(); 221 } 222 } 223 224 // Pending actions execution triggered, but Leanplum.start() may not be done yet. 225 LeanplumInternal.addStartIssuedHandler(runPendingActionsRunnable); 226 } 227 228 /** 229 * Call this when your activity gets resumed. 230 */ onResume()231 public void onResume() { 232 try { 233 if (!registeredCallbacks) { 234 onResume(activity); 235 } 236 } catch (Throwable t) { 237 Util.handleException(t); 238 } 239 } 240 onStop(Activity activity)241 private static void onStop(Activity activity) { 242 // onStop is called when the activity gets hidden, and is 243 // called after onPause. 244 // 245 // However, if we're switching to another activity, that activity 246 // will call onResume, so we shouldn't pause if that's the case. 247 // 248 // Thus, we can call pause from here, only if all activities are paused. 249 if (isActivityPaused) { 250 Leanplum.pause(); 251 LocationManager locationManager = ActionManager.getLocationManager(); 252 if (locationManager != null) { 253 locationManager.updateGeofencing(); 254 } 255 } 256 if (currentActivity != null && currentActivity.equals(activity)) { 257 // Don't leak activities. 258 currentActivity = null; 259 } 260 } 261 262 /** 263 * Call this when your activity gets stopped. 264 */ onStop()265 public void onStop() { 266 try { 267 if (!registeredCallbacks) { 268 onStop(activity); 269 } 270 } catch (Throwable t) { 271 Util.handleException(t); 272 } 273 } 274 275 /** 276 * Enqueues a callback to invoke when an activity reaches in the foreground. 277 */ queueActionUponActive(Runnable action)278 public static void queueActionUponActive(Runnable action) { 279 try { 280 if (currentActivity != null && !currentActivity.isFinishing() && !isActivityPaused && 281 (!(action instanceof PostponableAction) || !isActivityClassIgnored(currentActivity))) { 282 action.run(); 283 } else { 284 synchronized (pendingActions) { 285 pendingActions.add(action); 286 } 287 } 288 } catch (Throwable t) { 289 Util.handleException(t); 290 } 291 } 292 293 /** 294 * Runs any pending actions that have been queued. 295 */ runPendingActions()296 private static void runPendingActions() { 297 if (isActivityPaused || currentActivity == null) { 298 // Trying to run pending actions, but no activity is resumed. Skip. 299 return; 300 } 301 302 Queue<Runnable> runningActions; 303 synchronized (pendingActions) { 304 runningActions = new LinkedList<>(pendingActions); 305 pendingActions.clear(); 306 } 307 for (Runnable action : runningActions) { 308 // If postponable callback and current activity should be skipped, then postpone. 309 if (action instanceof PostponableAction && isActivityClassIgnored(currentActivity)) { 310 pendingActions.add(action); 311 } else { 312 action.run(); 313 } 314 } 315 } 316 317 /** 318 * Whether or not an activity is configured to not show messages. 319 * 320 * @param activity The activity to check. 321 * @return Whether or not the activity is ignored. 322 */ isActivityClassIgnored(Activity activity)323 private static boolean isActivityClassIgnored(Activity activity) { 324 return ignoredActivityClasses != null && ignoredActivityClasses.contains(activity.getClass()); 325 } 326 327 /** 328 * Does not show messages for the provided activity classes. 329 * 330 * @param activityClasses The activity classes to not show messages on. 331 */ deferMessagesForActivities(Class... activityClasses)332 public static void deferMessagesForActivities(Class... activityClasses) { 333 // Check if valid arguments are provided. 334 if (activityClasses == null || activityClasses.length == 0) { 335 return; 336 } 337 // Lazy instantiate activityClasses set. 338 if (ignoredActivityClasses == null) { 339 ignoredActivityClasses = new HashSet<>(activityClasses.length); 340 } 341 // Add all class names to set. 342 Collections.addAll(ignoredActivityClasses, activityClasses); 343 } 344 } 345