Merge "Fix animation sequence for upload tasks"
diff --git a/core/java/android/os/HwBinder.java b/core/java/android/os/HwBinder.java
index 5ff79f7..0e7da63 100644
--- a/core/java/android/os/HwBinder.java
+++ b/core/java/android/os/HwBinder.java
@@ -38,8 +38,11 @@
     public abstract void onTransact(
             int code, HwParcel request, HwParcel reply, int flags);
 
-    public native final void registerService(String serviceName);
-    public static native final IHwBinder getService(String serviceName);
+    public native final void registerService(
+            String serviceName, int versionMajor, int versionMinor);
+
+    public static native final IHwBinder getService(
+            String serviceName, int versionMajor, int versionMinor);
 
     // Returns address of the "freeFunction".
     private static native final long native_init();
diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp
index 13e3f0d..816d5df 100644
--- a/core/jni/android_os_HwBinder.cpp
+++ b/core/jni/android_os_HwBinder.cpp
@@ -196,19 +196,32 @@
 }
 
 static void JHwBinder_native_registerService(
-        JNIEnv *env, jobject thiz, jstring serviceNameObj) {
+        JNIEnv *env,
+        jobject thiz,
+        jstring serviceNameObj,
+        jint versionMajor,
+        jint versionMinor) {
     if (serviceNameObj == NULL) {
         jniThrowException(env, "java/lang/NullPointerException", NULL);
         return;
     }
 
+    if (versionMajor < 0
+            || versionMajor > 65535
+            || versionMinor < 0
+            || versionMinor > 65535) {
+        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
+        return;
+    }
+
     const jchar *serviceName = env->GetStringCritical(serviceNameObj, NULL);
 
     if (serviceName == NULL) {
         return;  // XXX exception already pending?
     }
 
-    const hardware::hidl_version kVersion = hardware::make_hidl_version(1, 0);
+    const hardware::hidl_version kVersion =
+        hardware::make_hidl_version(versionMajor, versionMinor);
 
     sp<hardware::IBinder> binder = JHwBinder::GetNativeContext(env, thiz);
 
@@ -231,19 +244,32 @@
 }
 
 static jobject JHwBinder_native_getService(
-        JNIEnv *env, jclass /* clazzObj */, jstring serviceNameObj) {
+        JNIEnv *env,
+        jclass /* clazzObj */,
+        jstring serviceNameObj,
+        jint versionMajor,
+        jint versionMinor) {
     if (serviceNameObj == NULL) {
         jniThrowException(env, "java/lang/NullPointerException", NULL);
         return NULL;
     }
 
+    if (versionMajor < 0
+            || versionMajor > 65535
+            || versionMinor < 0
+            || versionMinor > 65535) {
+        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
+        return NULL;
+    }
+
     const jchar *serviceName = env->GetStringCritical(serviceNameObj, NULL);
 
     if (serviceName == NULL) {
         return NULL;  // XXX exception already pending?
     }
 
-    const hardware::hidl_version kVersion = hardware::make_hidl_version(1, 0);
+    const hardware::hidl_version kVersion =
+        hardware::make_hidl_version(versionMajor, versionMinor);
 
     LOG(INFO) << "looking for service '"
               << String8(String16(
@@ -280,10 +306,10 @@
         "(IL" PACKAGE_PATH "/HwParcel;L" PACKAGE_PATH "/HwParcel;I)V",
         (void *)JHwBinder_native_transact },
 
-    { "registerService", "(Ljava/lang/String;)V",
+    { "registerService", "(Ljava/lang/String;II)V",
         (void *)JHwBinder_native_registerService },
 
-    { "getService", "(Ljava/lang/String;)L" PACKAGE_PATH "/IHwBinder;",
+    { "getService", "(Ljava/lang/String;II)L" PACKAGE_PATH "/IHwBinder;",
         (void *)JHwBinder_native_getService },
 };
 
diff --git a/telecomm/java/android/telecom/Log.java b/telecomm/java/android/telecom/Log.java
index 249d32b..1e5769f 100644
--- a/telecomm/java/android/telecom/Log.java
+++ b/telecomm/java/android/telecom/Log.java
@@ -188,6 +188,15 @@
         getSessionManager().startSession(shortMethodName, callerIdentification);
     }
 
+    public static void startSession(Session.Info info, String shortMethodName,
+            String callerIdentification) {
+        getSessionManager().startSession(info, shortMethodName, callerIdentification);
+    }
+
+    public static void startExternalSession(Session.Info sessionInfo, String shortMethodName) {
+        getSessionManager().startExternalSession(sessionInfo, shortMethodName);
+    }
+
     public static Session createSubsession() {
         return getSessionManager().createSubsession();
     }
diff --git a/telecomm/java/android/telecom/Logging/Session.java b/telecomm/java/android/telecom/Logging/Session.java
index 510abdd..093c0f9 100644
--- a/telecomm/java/android/telecom/Logging/Session.java
+++ b/telecomm/java/android/telecom/Logging/Session.java
@@ -17,6 +17,11 @@
 package android.telecom.Logging;
 
 import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 
@@ -33,12 +38,58 @@
     public static final String END_SUBSESSION = "END_SUBSESSION";
     public static final String END_SESSION = "END_SESSION";
 
+    public static final String SUBSESSION_SEPARATION_CHAR = "->";
+    public static final String EXTERNAL_INDICATOR = "E-";
+
     /**
      * Initial value of mExecutionEndTimeMs and the final value of {@link #getLocalExecutionTime()}
      * if the Session is canceled.
      */
     public static final int UNDEFINED = -1;
 
+    public static class Info implements Parcelable {
+        public final String sessionId;
+        public final String shortMethodName;
+
+        private Info(String id, String methodName) {
+            sessionId = id;
+            shortMethodName = methodName;
+        }
+
+        public static Info getInfo (Session s) {
+            return new Info(s.getFullSessionId(), s.getShortMethodName());
+        }
+
+        /** Responsible for creating Info objects for deserialized Parcels. */
+        public static final Parcelable.Creator<Info> CREATOR =
+                new Parcelable.Creator<Info> () {
+                    @Override
+                    public Info createFromParcel(Parcel source) {
+                        String id = source.readString();
+                        String methodName = source.readString();
+                        return new Info(id, methodName);
+                    }
+
+                    @Override
+                    public Info[] newArray(int size) {
+                        return new Info[size];
+                    }
+                };
+
+        /** {@inheritDoc} */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Writes Info object into a Parcel. */
+        @Override
+        public void writeToParcel(Parcel destination, int flags) {
+            destination.writeString(sessionId);
+            destination.writeString(shortMethodName);
+        }
+    }
+
     private String mSessionId;
     private String mShortMethodName;
     private long mExecutionStartTimeMs;
@@ -46,6 +97,7 @@
     private Session mParentSession;
     private ArrayList<Session> mChildSessions;
     private boolean mIsCompleted = false;
+    private boolean mIsExternal = false;
     private int mChildCounter = 0;
     // True if this is a subsession that has been started from the same thread as the parent
     // session. This can happen if Log.startSession(...) is called multiple times on the same
@@ -56,8 +108,11 @@
     // Optionally provided info about the method/class/component that started the session in order
     // to make Logging easier. This info will be provided in parentheses along with the session.
     private String mOwnerInfo;
+    // Cache Full Method path so that recursive population of the full method path only needs to
+    // be calculated once.
+    private String mFullMethodPathCache;
 
-    public Session(String sessionId, String shortMethodName, long startTimeMs, long threadID,
+    public Session(String sessionId, String shortMethodName, long startTimeMs,
             boolean isStartedFromActiveSession, String ownerInfo) {
         setSessionId(sessionId);
         setShortMethodName(shortMethodName);
@@ -86,6 +141,14 @@
         mShortMethodName = shortMethodName;
     }
 
+    public void setIsExternal(boolean isExternal) {
+        mIsExternal = isExternal;
+    }
+
+    public boolean isExternal() {
+        return mIsExternal;
+    }
+
     public void setParentSession(Session parentSession) {
         mParentSession = parentSession;
     }
@@ -126,6 +189,15 @@
         return mIsStartedFromActiveSession;
     }
 
+    public Info getInfo() {
+        return Info.getInfo(this);
+    }
+
+    @VisibleForTesting
+    public String getSessionId() {
+        return mSessionId;
+    }
+
     // Mark this session complete. This will be deleted by Log when all subsessions are complete
     // as well.
     public void markSessionCompleted(long executionEndTimeMs) {
@@ -186,6 +258,38 @@
         }
     }
 
+    // Recursively concatenate mShortMethodName with the parent Sessions to create full method
+    // path. Caches this string so that multiple calls for the path will be quick.
+    public String getFullMethodPath() {
+        StringBuilder sb = new StringBuilder();
+        getFullMethodPath(sb);
+        return sb.toString();
+    }
+
+    private synchronized void getFullMethodPath(StringBuilder sb) {
+        // Don't calculate if we have already figured it out!
+        if (!TextUtils.isEmpty(mFullMethodPathCache)) {
+            sb.append(mFullMethodPathCache);
+            return;
+        }
+        Session parentSession = getParentSession();
+        boolean isSessionStarted = false;
+        if (parentSession != null) {
+            // Check to see if the session has been renamed yet. If it has not, then the session
+            // has not been continued.
+            isSessionStarted = !mShortMethodName.equals(parentSession.mShortMethodName);
+            parentSession.getFullMethodPath(sb);
+            sb.append(SUBSESSION_SEPARATION_CHAR);
+        }
+        sb.append(mShortMethodName);
+
+        if(isSessionStarted) {
+            // Cache this value so that we do not have to do this work next time!
+            // We do not cache the value if the session being evaluated hasn't been continued yet.
+            mFullMethodPathCache = sb.toString();
+        }
+    }
+
     @Override
     public int hashCode() {
         int result = mSessionId != null ? mSessionId.hashCode() : 0;
@@ -238,7 +342,7 @@
             return mParentSession.toString();
         } else {
             StringBuilder methodName = new StringBuilder();
-            methodName.append(mShortMethodName);
+            methodName.append(getFullMethodPath());
             if (mOwnerInfo != null && !mOwnerInfo.isEmpty()) {
                 methodName.append("(InCall package: ");
                 methodName.append(mOwnerInfo);
diff --git a/telecomm/java/android/telecom/Logging/SessionManager.java b/telecomm/java/android/telecom/Logging/SessionManager.java
index add1237..173e7ee 100644
--- a/telecomm/java/android/telecom/Logging/SessionManager.java
+++ b/telecomm/java/android/telecom/Logging/SessionManager.java
@@ -21,6 +21,7 @@
 import android.os.Looper;
 import android.os.Process;
 import android.provider.Settings;
+import android.telecom.Log;
 import android.util.Base64;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -41,12 +42,9 @@
 
     // Currently using 3 letters, So don't exceed 64^3
     private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144;
-
     // This parameter can be overridden in Telecom's Timeouts class.
-    public static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds
-
-    private static String LOGGING_TAG = "Logging";
-
+    private static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds
+    private static final String LOGGING_TAG = "Logging";
     private static final String TIMEOUTS_PREFIX = "telecom.";
 
     // Synchronized in all method calls
@@ -56,10 +54,9 @@
     @VisibleForTesting
     public ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(100);
     @VisibleForTesting
-    public Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper());
-    @VisibleForTesting
     public java.lang.Runnable mCleanStaleSessions = () ->
             cleanupStaleSessions(getSessionCleanupTimeoutMs());
+    private Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper());
 
     // Overridden in LogTest to skip query to ContentProvider
     private interface ISessionCleanupTimeoutMs {
@@ -74,8 +71,7 @@
     @VisibleForTesting
     public ICurrentThreadId mCurrentThreadId = Process::myTid;
 
-    @VisibleForTesting
-    public ISessionCleanupTimeoutMs mSessionCleanupTimeoutMs = () -> {
+    private ISessionCleanupTimeoutMs mSessionCleanupTimeoutMs = () -> {
         // mContext may be null in some cases, such as testing. For these cases, use the
         // default value.
         if (mContext == null) {
@@ -120,6 +116,21 @@
     }
 
     /**
+     * Determines whether or not to start a new session or continue an existing session based on
+     * the {@link Session.Info} info passed into startSession. If info is null, a new Session is
+     * created. This code must be accompanied by endSession() at the end of the Session.
+     */
+    public synchronized void startSession(Session.Info info, String shortMethodName,
+            String callerIdentification) {
+        // Start a new session normally if the
+        if(info == null) {
+            startSession(shortMethodName, callerIdentification);
+        } else {
+            startExternalSession(info, shortMethodName);
+        }
+    }
+
+    /**
      * Call at an entry point to the Telecom code to track the session. This code must be
      * accompanied by a Log.endSession().
      */
@@ -134,14 +145,54 @@
             Session childSession = createSubsession(true);
             continueSession(childSession, shortMethodName);
             return;
+        } else {
+            // Only Log that we are starting the parent session.
+            Log.d(LOGGING_TAG, Session.START_SESSION);
         }
         Session newSession = new Session(getNextSessionID(), shortMethodName,
-                System.currentTimeMillis(), threadId, false, callerIdentification);
+                System.currentTimeMillis(), false, callerIdentification);
         mSessionMapper.put(threadId, newSession);
-
-        android.util.Slog.v(LOGGING_TAG, Session.START_SESSION);
     }
 
+    /**
+     * Registers an external Session with the Manager using that external Session's sessionInfo.
+     * Log.endSession will still need to be called at the end of the session.
+     * @param sessionInfo Describes the external Session's information.
+     * @param shortMethodName The method name of the new session that is being started.
+     */
+    public synchronized void startExternalSession(Session.Info sessionInfo,
+            String shortMethodName) {
+        if(sessionInfo == null) {
+            return;
+        }
+
+        int threadId = getCallingThreadId();
+        Session threadSession = mSessionMapper.get(threadId);
+        if (threadSession != null) {
+            // We should never get into a situation where there is already an active session AND
+            // an external session is added. We are just using that active session.
+            Log.w(LOGGING_TAG, "trying to start an external session with a session " +
+                    "already active.");
+            return;
+        }
+
+        // Create Session from Info and add to the sessionMapper under this ID.
+        Session externalSession = new Session(Session.EXTERNAL_INDICATOR + sessionInfo.sessionId,
+                sessionInfo.shortMethodName, System.currentTimeMillis(),
+                false /*isStartedFromActiveSession*/, null);
+        externalSession.setIsExternal(true);
+        // Mark the external session as already completed, since we have no way of knowing when
+        // the external session actually has completed.
+        externalSession.markSessionCompleted(Session.UNDEFINED);
+        // Track the external session with the SessionMapper so that we can create and continue
+        // an active subsession based on it.
+        mSessionMapper.put(threadId, externalSession);
+        // Create a subsession from this external Session parent node
+        Session childSession = createSubsession();
+        continueSession(childSession, shortMethodName);
+
+        Log.d(LOGGING_TAG, Session.START_SESSION);
+    }
 
     /**
      * Notifies the logging system that a subsession will be run at a later point and
@@ -156,22 +207,22 @@
         int threadId = getCallingThreadId();
         Session threadSession = mSessionMapper.get(threadId);
         if (threadSession == null) {
-            android.util.Slog.d(LOGGING_TAG, "Log.createSubsession was called with no session " +
+            Log.d(LOGGING_TAG, "Log.createSubsession was called with no session " +
                     "active.");
             return null;
         }
         // Start execution time of the session will be overwritten in continueSession(...).
         Session newSubsession = new Session(threadSession.getNextChildId(),
-                threadSession.getShortMethodName(), System.currentTimeMillis(), threadId,
+                threadSession.getShortMethodName(), System.currentTimeMillis(),
                 isStartedFromActiveSession, null);
         threadSession.addChild(newSubsession);
         newSubsession.setParentSession(threadSession);
 
         if (!isStartedFromActiveSession) {
-            android.util.Slog.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " +
+            Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " +
                     newSubsession.toString());
         } else {
-            android.util.Slog.v(LOGGING_TAG, Session.CREATE_SUBSESSION +
+            Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION +
                     " (Invisible subsession)");
         }
         return newSubsession;
@@ -201,21 +252,20 @@
             return;
         }
         resetStaleSessionTimer();
-        String callingMethodName = subsession.getShortMethodName();
-        subsession.setShortMethodName(callingMethodName + "->" + shortMethodName);
+        subsession.setShortMethodName(shortMethodName);
         subsession.setExecutionStartTimeMs(System.currentTimeMillis());
         Session parentSession = subsession.getParentSession();
         if (parentSession == null) {
-            android.util.Slog.d(LOGGING_TAG, "Log.continueSession was called with no session " +
+            Log.i(LOGGING_TAG, "Log.continueSession was called with no session " +
                     "active for method " + shortMethodName);
             return;
         }
 
         mSessionMapper.put(getCallingThreadId(), subsession);
         if (!subsession.isStartedFromActiveSession()) {
-            android.util.Slog.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
+            Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
         } else {
-            android.util.Slog.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION +
+            Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION +
                     " (Invisible Subsession) with Method " + shortMethodName);
         }
     }
@@ -228,16 +278,16 @@
         int threadId = getCallingThreadId();
         Session completedSession = mSessionMapper.get(threadId);
         if (completedSession == null) {
-            android.util.Slog.w(LOGGING_TAG, "Log.endSession was called with no session active.");
+            Log.w(LOGGING_TAG, "Log.endSession was called with no session active.");
             return;
         }
 
         completedSession.markSessionCompleted(System.currentTimeMillis());
         if (!completedSession.isStartedFromActiveSession()) {
-            android.util.Slog.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
+            Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
                     completedSession.getLocalExecutionTime() + " mS)");
         } else {
-            android.util.Slog.v(LOGGING_TAG, Session.END_SUBSESSION +
+            Log.v(LOGGING_TAG, Session.END_SUBSESSION +
                     " (Invisible Subsession) (dur: " + completedSession.getLocalExecutionTime() +
                     " ms)");
         }
@@ -260,25 +310,37 @@
         if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) {
             return;
         }
-
         Session parentSession = subsession.getParentSession();
         if (parentSession != null) {
             subsession.setParentSession(null);
             parentSession.removeChild(subsession);
+            // Report the child session of the external session as being complete to the listeners,
+            // not the external session itself.
+            if (parentSession.isExternal()) {
+                long fullSessionTimeMs =
+                        System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
+                notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
+            }
             endParentSessions(parentSession);
         } else {
             // All of the subsessions have been completed and it is time to report on the full
             // running time of the session.
             long fullSessionTimeMs =
                     System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
-            android.util.Slog.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs
+            Log.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs
                     + " ms): " + subsession.toString());
-            for (ISessionListener l : mSessionListeners) {
-                l.sessionComplete(subsession.getShortMethodName(), fullSessionTimeMs);
+            if (!subsession.isExternal()) {
+                notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
             }
         }
     }
 
+    private void notifySessionCompleteListeners(String methodName, long sessionTimeMs) {
+        for (ISessionListener l : mSessionListeners) {
+            l.sessionComplete(methodName, sessionTimeMs);
+        }
+    }
+
     public String getSessionId() {
         Session currentSession = mSessionMapper.get(getCallingThreadId());
         return currentSession != null ? currentSession.toString() : "";
@@ -299,24 +361,22 @@
         return getBase64Encoding(nextId);
     }
 
-    @VisibleForTesting
-    public synchronized void restartSessionCounter() {
+    private synchronized void restartSessionCounter() {
         sCodeEntryCounter = 0;
     }
 
-    @VisibleForTesting
-    public String getBase64Encoding(int number) {
+    private String getBase64Encoding(int number) {
         byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array();
         idByteArray = Arrays.copyOfRange(idByteArray, 2, 4);
         return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING);
     }
 
-    public int getCallingThreadId() {
+    private int getCallingThreadId() {
         return mCurrentThreadId.get();
     }
 
     @VisibleForTesting
-    private synchronized void cleanupStaleSessions(long timeoutMs) {
+    public synchronized void cleanupStaleSessions(long timeoutMs) {
         String logMessage = "Stale Sessions Cleaned:\n";
         boolean isSessionsStale = false;
         long currentTimeMs = System.currentTimeMillis();
@@ -335,9 +395,9 @@
             }
         }
         if (isSessionsStale) {
-            android.util.Slog.w(LOGGING_TAG, logMessage);
+            Log.w(LOGGING_TAG, logMessage);
         } else {
-            android.util.Slog.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
+            Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
         }
     }