diff --git a/annotation/src/main/java/com/android/designcompose/annotation/Builder.kt b/annotation/src/main/java/com/android/designcompose/annotation/Builder.kt index fc995e862..9052daef0 100644 --- a/annotation/src/main/java/com/android/designcompose/annotation/Builder.kt +++ b/annotation/src/main/java/com/android/designcompose/annotation/Builder.kt @@ -20,11 +20,16 @@ package com.android.designcompose.annotation * Generate an interface that contains functions to render various nodes in a Figma document * * @param id the id of the Figma document. This can be found in the url, e.g. figma.com/file/ - * @param version a version string that gets written to a generated JSON file used for the Design - * Compose Figma plugin + * @param designVersion version id of the Figma document. + * @param customizationInterfaceVersion a version string that gets written to a generated JSON file + * describing the customization interface which is used for the Design Compose Figma plugin. */ @Target(AnnotationTarget.CLASS) -annotation class DesignDoc(val id: String, val version: String = "0") +annotation class DesignDoc( + val id: String, + val designVersion: String = "", + val customizationInterfaceVersion: String = "0" +) /** * Generate a @Composable function that renders the given node diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 90fafbf07..2da2f7690 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -26,7 +26,8 @@ spotless { } kotlinGradle { ktfmt(libs.versions.ktfmt.get()).kotlinlangStyle() } } -kotlin { jvmToolchain(libs.versions.jvmToolchain.get().toInt())} + +kotlin { jvmToolchain(libs.versions.jvmToolchain.get().toInt()) } dependencies { implementation(libs.android.gradlePlugin) diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index b388c5b6b..7bc82eb0c 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -24,8 +24,8 @@ dependencyResolutionManagement { } versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } } + plugins { // Downloads the required Java Toolchain, if needed. id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } - diff --git a/codegen/src/main/kotlin/com/android/designcompose/codegen/BuilderProcessor.kt b/codegen/src/main/kotlin/com/android/designcompose/codegen/BuilderProcessor.kt index 0d64eb7d2..dfa95268f 100644 --- a/codegen/src/main/kotlin/com/android/designcompose/codegen/BuilderProcessor.kt +++ b/codegen/src/main/kotlin/com/android/designcompose/codegen/BuilderProcessor.kt @@ -131,6 +131,7 @@ class BuilderProcessor(private val codeGenerator: CodeGenerator, val logger: KSP ) : KSVisitorVoid() { private var docName: String = "" private var docId: String = "" + private var designVersion: String = "" private var currentFunc = "" private var textCustomizations: HashMap>> = HashMap() private var textFunctionCustomizations: HashMap>> = @@ -209,6 +210,10 @@ class BuilderProcessor(private val codeGenerator: CodeGenerator, val logger: KSP docName = className + "Doc" docId = idArg.value as String + val designVersionArg: KSValueArgument = + annotation.arguments.first { arg -> arg.name?.asString() == "designVersion" } + designVersion = designVersionArg.value as String + // Declare a global document ID that can be changed by the Design Switcher val docIdVarName = className + "GenId" out.appendText("private var $docIdVarName: String = \"$docId\"\n\n") @@ -251,7 +256,7 @@ class BuilderProcessor(private val codeGenerator: CodeGenerator, val logger: KSP out.appendText(" @Composable\n") out.appendText(" final fun DesignSwitcher(modifier: Modifier = Modifier) {\n") out.appendText( - " val (docId, setDocId) = remember { mutableStateOf(\"$docId\") }\n" + " val (docId, setDocId) = remember { mutableStateOf(DesignDocId(\"$docId\", \"$designVersion\")) }\n" ) out.appendText(" DesignDoc(\"$docName\", docId, NodeQuery.NodeName(\"\"),\n") out.appendText(" modifier = modifier,\n") @@ -312,7 +317,7 @@ class BuilderProcessor(private val codeGenerator: CodeGenerator, val logger: KSP out.appendText(" customizations.setTapCallback(nodeName, tapCallback)\n") out.appendText(" customizations.mergeFrom(LocalCustomizationContext.current)\n") out.appendText( - " val (docId, setDocId) = remember { mutableStateOf(\"$docId\") }\n" + " val (docId, setDocId) = remember { mutableStateOf(DesignDocId(\"$docId\", \"$designVersion\")) }\n" ) out.appendText(" val queries = queries()\n") out.appendText(" queries.add(nodeName)\n") @@ -342,10 +347,13 @@ class BuilderProcessor(private val codeGenerator: CodeGenerator, val logger: KSP // Write the design doc JSON for our plugin designDocJson.addProperty("name", className) designDocJson.add("components", jsonComponents) - val versionArg = annotation.arguments.find { arg -> arg.name?.asString() == "version" } - if (versionArg != null) { - val versionString = versionArg.value as String - designDocJson.addProperty("version", versionString) + val customizationInterfaceVersionArg = + annotation.arguments.find { arg -> + arg.name?.asString() == "customizationInterfaceVersion" + } + if (customizationInterfaceVersionArg != null) { + val customizationInterfaceVersion = customizationInterfaceVersionArg.value as String + designDocJson.addProperty("version", customizationInterfaceVersion) } val gson = GsonBuilder().setPrettyPrinting().create() @@ -760,7 +768,7 @@ class BuilderProcessor(private val codeGenerator: CodeGenerator, val logger: KSP // Create a mutable state so that the Design Switcher can dynamically change the // document ID out.appendText( - " val (docId, setDocId) = remember { mutableStateOf(\"$docId\") }\n" + " val (docId, setDocId) = remember { mutableStateOf(DesignDocId(\"$docId\", \"$designVersion\")) }\n" ) // If there are variants, add the variant name to the list of queries diff --git a/codegen/src/main/kotlin/com/android/designcompose/codegen/Common.kt b/codegen/src/main/kotlin/com/android/designcompose/codegen/Common.kt index 6fd86b511..7a73d8b5b 100644 --- a/codegen/src/main/kotlin/com/android/designcompose/codegen/Common.kt +++ b/codegen/src/main/kotlin/com/android/designcompose/codegen/Common.kt @@ -97,6 +97,7 @@ internal fun createNewFile( file += "import com.android.designcompose.annotation.DesignMetaKey\n" file += "import com.android.designcompose.serdegen.NodeQuery\n" + file += "import com.android.designcompose.common.DesignDocId\n" file += "import com.android.designcompose.common.DocumentServerParams\n" file += "import com.android.designcompose.ComponentReplacementContext\n" file += "import com.android.designcompose.ImageReplacementContext\n" diff --git a/common/src/main/java/com/android/designcompose/common/DesignDocId.kt b/common/src/main/java/com/android/designcompose/common/DesignDocId.kt new file mode 100644 index 000000000..e61f51706 --- /dev/null +++ b/common/src/main/java/com/android/designcompose/common/DesignDocId.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.designcompose.common + +/** + * Id for the figma design doc with file id and version id. When version id is not specified, it + * will load head of the figma doc. + */ +data class DesignDocId(var id: String, var versionId: String = "") { + fun isValid(): Boolean { + return id.isNotEmpty() + } + + override fun toString(): String { + return if (versionId.isEmpty()) id else "${id}_$versionId" + } +} diff --git a/common/src/main/java/com/android/designcompose/common/Feedback.kt b/common/src/main/java/com/android/designcompose/common/Feedback.kt index 36e2e59f2..6a732b3e0 100644 --- a/common/src/main/java/com/android/designcompose/common/Feedback.kt +++ b/common/src/main/java/com/android/designcompose/common/Feedback.kt @@ -35,7 +35,7 @@ class FeedbackMessage( // Basic implementation of the Feedback class, used by docloader and Design Compose abstract class FeedbackImpl { private val messages: ArrayDeque = ArrayDeque() - private val ignoredDocuments: HashSet = HashSet() + private val ignoredDocuments: HashSet = HashSet() private var logLevel: FeedbackLevel = FeedbackLevel.Info private var maxMessages = 20 var messagesListId = 0 // Change this every time the list changes so we can update subscribers @@ -52,12 +52,12 @@ abstract class FeedbackImpl { maxMessages = num } - fun addIgnoredDocument(docId: String): Boolean { + fun addIgnoredDocument(docId: DesignDocId): Boolean { ignoredDocuments.add(docId) return true } - fun isDocumentIgnored(docId: String): Boolean { + fun isDocumentIgnored(docId: DesignDocId): Boolean { return ignoredDocuments.contains(docId) } @@ -70,7 +70,7 @@ abstract class FeedbackImpl { // fun newDocServer(url: String){ // } - fun diskLoadFail(id: String, docId: String) { + fun diskLoadFail(id: String, docId: DesignDocId) { setStatus( "Unable to open $id from disk; will try live and from assets", FeedbackLevel.Debug, @@ -78,12 +78,12 @@ abstract class FeedbackImpl { ) } - fun documentUnchanged(docId: String) { + fun documentUnchanged(docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Live update for $truncatedId unchanged...", FeedbackLevel.Info, docId) } - fun documentUpdated(docId: String, numSubscribers: Int) { + fun documentUpdated(docId: DesignDocId, numSubscribers: Int) { val truncatedId = shortDocId(docId) setStatus( "Live update for $truncatedId fetched and informed $numSubscribers subscribers", @@ -92,7 +92,7 @@ abstract class FeedbackImpl { ) } - fun documentUpdateCode(docId: String, code: Int) { + fun documentUpdateCode(docId: DesignDocId, code: Int) { val truncatedId = shortDocId(docId) setStatus( "Live update for $truncatedId unexpected server response: $code", @@ -101,17 +101,17 @@ abstract class FeedbackImpl { ) } - fun documentUpdateWarnings(docId: String, msg: String) { + fun documentUpdateWarnings(docId: DesignDocId, msg: String) { val truncatedId = shortDocId(docId) setStatus("Live update for $truncatedId warning: $msg", FeedbackLevel.Warn, docId) } - fun documentUpdateError(docId: String, msg: String) { + fun documentUpdateError(docId: DesignDocId, msg: String) { val truncatedId = shortDocId(docId) setStatus("Live update for $truncatedId failed: $msg", FeedbackLevel.Error, docId) } - fun documentUpdateErrorRevert(docId: String, msg: String) { + fun documentUpdateErrorRevert(docId: DesignDocId, msg: String) { val truncatedId = shortDocId(docId) setStatus( "Live update for $truncatedId failed: $msg, reverting to original doc ID", @@ -120,22 +120,22 @@ abstract class FeedbackImpl { ) } - fun documentDecodeStart(docId: String) { + fun documentDecodeStart(docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Starting to read doc $truncatedId...", FeedbackLevel.Debug, docId) } - fun documentDecodeReadBytes(size: Int, docId: String) { + fun documentDecodeReadBytes(size: Int, docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Read $size bytes of doc $truncatedId", FeedbackLevel.Info, docId) } - fun documentDecodeError(docId: String) { + fun documentDecodeError(docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Error decoding doc $truncatedId", FeedbackLevel.Warn, docId) } - fun documentDecodeVersionMismatch(expected: Int, actual: Int, docId: String) { + fun documentDecodeVersionMismatch(expected: Int, actual: Int, docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus( "Wrong version in doc $truncatedId: Expected $expected but found $actual", @@ -144,7 +144,12 @@ abstract class FeedbackImpl { ) } - fun documentDecodeSuccess(version: Int, name: String, lastModified: String, docId: String) { + fun documentDecodeSuccess( + version: Int, + name: String, + lastModified: String, + docId: DesignDocId + ) { setStatus( "Successfully deserialized V$version doc. Name: $name, last modified: $lastModified", FeedbackLevel.Info, @@ -152,22 +157,22 @@ abstract class FeedbackImpl { ) } - fun documentSaveTo(path: String, docId: String) { + fun documentSaveTo(path: String, docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Saving doc $truncatedId to $path", FeedbackLevel.Info, docId) } - fun documentSaveSuccess(docId: String) { + fun documentSaveSuccess(docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Save doc $truncatedId success", FeedbackLevel.Info, docId) } - fun documentSaveError(error: String, docId: String) { + fun documentSaveError(error: String, docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Unable to save doc $truncatedId: $error", FeedbackLevel.Error, docId) } - open fun setStatus(str: String, level: FeedbackLevel, docId: String) { + open fun setStatus(str: String, level: FeedbackLevel, docId: DesignDocId) { // Ignore log levels we don't care about if (level < logLevel) return @@ -189,8 +194,12 @@ abstract class FeedbackImpl { ++messagesListId } - protected fun shortDocId(docId: String): String { - return if (docId.length > 7) docId.substring(0, 7) else docId + protected fun shortDocId(docId: DesignDocId): String { + val id = if (docId.id.length > 7) docId.id.substring(0, 7) else docId.id + val versionId = + if (docId.versionId.length > 4) docId.versionId.substring(docId.versionId.length - 4) + else docId.versionId + return if (versionId.isEmpty()) id else "${id}/${versionId}" } } diff --git a/common/src/main/java/com/android/designcompose/common/GenericDocContent.kt b/common/src/main/java/com/android/designcompose/common/GenericDocContent.kt index 5a8fe22f9..da63a098b 100644 --- a/common/src/main/java/com/android/designcompose/common/GenericDocContent.kt +++ b/common/src/main/java/com/android/designcompose/common/GenericDocContent.kt @@ -30,7 +30,7 @@ import java.io.FileOutputStream import java.io.InputStream class GenericDocContent( - var docId: String, + var docId: DesignDocId, private val header: SerializedDesignDocHeader, val document: SerializedDesignDoc, val variantViewMap: HashMap>, @@ -70,7 +70,7 @@ class GenericDocContent( /// Read a serialized server document from the given stream. Deserialize it and save it to disk. fun decodeServerBaseDoc( docBytes: ByteArray, - docId: String, + docId: DesignDocId, feedback: FeedbackImpl ): GenericDocContent? { val deserializer = BincodeDeserializer(docBytes) @@ -99,7 +99,11 @@ fun decodeServerBaseDoc( } /// Read a serialized disk document from the given stream. Deserialize it and deserialize its images -fun decodeDiskBaseDoc(doc: InputStream, docId: String, feedback: FeedbackImpl): GenericDocContent? { +fun decodeDiskBaseDoc( + doc: InputStream, + docId: DesignDocId, + feedback: FeedbackImpl +): GenericDocContent? { val docBytes = readDocBytes(doc, docId, feedback) val deserializer = BincodeDeserializer(docBytes) @@ -166,7 +170,7 @@ private fun createVariantPropertyMap(nodes: Map?): VariantPrope return propertyMap } -fun readDocBytes(doc: InputStream, docId: String, feedback: FeedbackImpl): ByteArray { +fun readDocBytes(doc: InputStream, docId: DesignDocId, feedback: FeedbackImpl): ByteArray { // Read the doc from assets or network... feedback.documentDecodeStart(docId) val buffer = ByteArrayOutputStream() @@ -201,7 +205,7 @@ fun readErrorBytes(errorStream: InputStream?): String { private fun decodeHeader( deserializer: BincodeDeserializer, - docId: String, + docId: DesignDocId, feedback: FeedbackImpl ): SerializedDesignDocHeader? { // Now attempt to deserialize the doc) diff --git a/crates/dc_jni/src/jni.rs b/crates/dc_jni/src/jni.rs index d671501a2..996906610 100644 --- a/crates/dc_jni/src/jni.rs +++ b/crates/dc_jni/src/jni.rs @@ -90,6 +90,7 @@ fn jni_fetch_doc<'local>( mut env: JNIEnv<'local>, _class: JClass, jdoc_id: JString, + jversion_id: JString, jrequest_json: JString, jproxy_config: JObject, ) -> JByteArray<'local> { @@ -101,6 +102,14 @@ fn jni_fetch_doc<'local>( } }; + let version_id: String = match env.get_string(&jversion_id) { + Ok(it) => it.into(), + Err(_) => { + throw_basic_exception(&mut env, "Internal JNI Error".to_string()); + return JObject::null().into(); + } + }; + let request_json: String = match env.get_string(&jrequest_json) { Ok(it) => it.into(), Err(_) => { @@ -114,12 +123,13 @@ fn jni_fetch_doc<'local>( Err(_) => ProxyConfig::None, }; - let ser_result = match jni_fetch_doc_impl(&mut env, doc_id, request_json, &proxy_config) { - Ok(it) => it, - Err(_err) => { - return JObject::null().into(); - } - }; + let ser_result = + match jni_fetch_doc_impl(&mut env, doc_id, version_id, request_json, &proxy_config) { + Ok(it) => it, + Err(_err) => { + return JObject::null().into(); + } + }; env.byte_array_from_slice(&ser_result).unwrap_or_else(|_| { throw_basic_exception(&mut env, "Internal JNI Error".to_string()); @@ -130,13 +140,14 @@ fn jni_fetch_doc<'local>( fn jni_fetch_doc_impl( env: &mut JNIEnv, doc_id: String, + version_id: String, request_json: String, proxy_config: &ProxyConfig, ) -> Result, figma_import::Error> { let request: ConvertRequest = serde_json::from_str(&request_json)?; let convert_result: figma_import::ConvertResponse = - match fetch_doc(&doc_id, request, proxy_config) { + match fetch_doc(&doc_id, &version_id, request, proxy_config) { Ok(it) => it, Err(err) => { map_err_to_exception(env, &err, doc_id).expect("Failed to throw exception"); @@ -351,7 +362,7 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint { &[ jni::NativeMethod { name: "jniFetchDoc".into(), - sig: "(Ljava/lang/String;Ljava/lang/String;Lcom/android/designcompose/ProxyConfig;)[B".into(), + sig: "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/android/designcompose/ProxyConfig;)[B".into(), fn_ptr: jni_fetch_doc as *mut c_void, }, jni::NativeMethod { diff --git a/crates/figma_import/src/bin/fetch.rs b/crates/figma_import/src/bin/fetch.rs index 7940afcb0..c4ae95012 100644 --- a/crates/figma_import/src/bin/fetch.rs +++ b/crates/figma_import/src/bin/fetch.rs @@ -50,6 +50,9 @@ struct Args { /// Figma Document ID to fetch and convert #[arg(short, long)] doc_id: String, + /// Figma Document Version ID to fetch and convert + #[arg(short, long)] + version_id: Option, /// Figma API key to use for Figma requests #[arg(short, long)] api_key: String, @@ -68,7 +71,13 @@ fn fetch_impl(args: Args) -> Result<(), ConvertError> { Some(x) => ProxyConfig::HttpProxyConfig(x), None => ProxyConfig::None, }; - let mut doc = Document::new(args.api_key.as_str(), args.doc_id, &proxy_config, None)?; + let mut doc = Document::new( + args.api_key.as_str(), + args.doc_id, + args.version_id.unwrap_or(String::new()), + &proxy_config, + None, + )?; let mut error_list = Vec::new(); // Convert the requested nodes from the Figma doc. let views = doc.nodes( diff --git a/crates/figma_import/src/bin/fetch_layout.rs b/crates/figma_import/src/bin/fetch_layout.rs index c6f35a641..68326d04a 100644 --- a/crates/figma_import/src/bin/fetch_layout.rs +++ b/crates/figma_import/src/bin/fetch_layout.rs @@ -74,6 +74,9 @@ struct Args { /// Figma Document ID to fetch and convert #[arg(short, long)] doc_id: String, + /// Figma Document Version ID to fetch and convert + #[arg(short, long)] + version_id: Option, /// Figma API key to use for Figma requests #[arg(short, long)] api_key: String, @@ -203,8 +206,13 @@ fn fetch_impl(args: Args) -> Result<(), ConvertError> { Some(x) => ProxyConfig::HttpProxyConfig(x), None => ProxyConfig::None, }; - let mut doc: Document = - Document::new(args.api_key.as_str(), args.doc_id.clone(), &proxy_config, None)?; + let mut doc: Document = Document::new( + args.api_key.as_str(), + args.doc_id.clone(), + args.version_id.unwrap_or(String::new()), + &proxy_config, + None, + )?; let mut error_list = Vec::new(); // Convert the requested nodes from the Figma doc. let views = doc.nodes( diff --git a/crates/figma_import/src/document.rs b/crates/figma_import/src/document.rs index 6232755a5..812ef7674 100644 --- a/crates/figma_import/src/document.rs +++ b/crates/figma_import/src/document.rs @@ -112,19 +112,21 @@ impl NodeQuery { pub struct FigmaDocInfo { pub name: String, pub id: String, + pub version_id: String, } impl FigmaDocInfo { - pub(crate) fn new(name: String, id: String) -> FigmaDocInfo { - FigmaDocInfo { name, id } + pub(crate) fn new(name: String, id: String, version_id: String) -> FigmaDocInfo { + FigmaDocInfo { name, id, version_id } } } +/// Branches alwasy return head of file, i.e. no version returned fn get_branches(document_root: &FileResponse) -> Vec { let mut branches = vec![]; if let Some(doc_branches) = &document_root.branches { for hash in doc_branches { if let (Some(Some(id)), Some(Some(name))) = (hash.get("key"), hash.get("name")) { - branches.push(FigmaDocInfo::new(name.clone(), id.clone())); + branches.push(FigmaDocInfo::new(name.clone(), id.clone(), String::new())); } } } @@ -137,6 +139,7 @@ fn get_branches(document_root: &FileResponse) -> Vec { pub struct Document { api_key: String, document_id: String, + version_id: String, proxy_config: ProxyConfig, document_root: FileResponse, image_context: ImageContext, @@ -153,18 +156,23 @@ impl Document { pub fn new( api_key: &str, document_id: String, + version_id: String, proxy_config: &ProxyConfig, image_session: Option, ) -> Result { // Fetch the document... - let document_url = format!( + let mut document_url = format!( "{}{}?plugin_data=shared&geometry=paths&branch_data=true", BASE_FILE_URL, document_id, ); + if !version_id.is_empty() { + document_url.push_str("&version="); + document_url.push_str(&version_id); + } let document_root: FileResponse = serde_json::from_str(http_fetch(api_key, document_url, proxy_config)?.as_str())?; - // ...and the mapping from imageRef to URL + // ...and the mapping from imageRef to URL. It returns images from all versions. let image_ref_url = format!("{}{}/images", BASE_FILE_URL, document_id); let image_refs: ImageFillResponse = serde_json::from_str(http_fetch(api_key, image_ref_url, proxy_config)?.as_str())?; @@ -179,6 +187,7 @@ impl Document { Ok(Document { api_key: api_key.to_string(), document_id, + version_id, proxy_config: proxy_config.clone(), document_root, image_context, @@ -193,20 +202,26 @@ impl Document { pub fn new_if_changed( api_key: &str, document_id: String, + requested_version_id: String, proxy_config: &ProxyConfig, last_modified: String, - version: String, + last_version: String, image_session: Option, ) -> Result, Error> { - let document_head_url = format!("{}{}?depth=1", BASE_FILE_URL, document_id); + let mut document_head_url = format!("{}{}?depth=1", BASE_FILE_URL, document_id); + if !requested_version_id.is_empty() { + document_head_url.push_str("&version="); + document_head_url.push_str(&requested_version_id); + } let document_head: FileHeadResponse = serde_json::from_str(http_fetch(api_key, document_head_url, proxy_config)?.as_str())?; - if document_head.last_modified == last_modified && document_head.version == version { + if document_head.last_modified == last_modified && document_head.version == last_version { return Ok(None); } - Document::new(api_key, document_id, proxy_config, image_session).map(Some) + Document::new(api_key, document_id, requested_version_id, proxy_config, image_session) + .map(Some) } /// Ask Figma if an updated document is available, and then fetch the updated document @@ -215,7 +230,11 @@ impl Document { self.proxy_config = proxy_config.clone(); // Fetch just the top level of the document. (depth=0 causes an internal server error). - let document_head_url = format!("{}{}?depth=1", BASE_FILE_URL, self.document_id); + let mut document_head_url = format!("{}{}?depth=1", BASE_FILE_URL, self.document_id); + if !self.version_id.is_empty() { + document_head_url.push_str("&version="); + document_head_url.push_str(&self.version_id); + } let document_head: FileHeadResponse = serde_json::from_str( http_fetch(self.api_key.as_str(), document_head_url, &self.proxy_config)?.as_str(), )?; @@ -231,15 +250,19 @@ impl Document { } // Fetch the updated document in its entirety and replace our document root... - let document_url = format!( + let mut document_url = format!( "{}{}?plugin_data=shared&geometry=paths&branch_data=true", BASE_FILE_URL, self.document_id, ); + if !self.version_id.is_empty() { + document_url.push_str("&version="); + document_url.push_str(&self.version_id); + } let document_root: FileResponse = serde_json::from_str( http_fetch(self.api_key.as_str(), document_url, &self.proxy_config)?.as_str(), )?; - // ...and the mapping from imageRef to URL + // ...and the mapping from imageRef to URL. It returns images from all versions. let image_ref_url = format!("{}{}/images", BASE_FILE_URL, self.document_id); let image_refs: ImageFillResponse = serde_json::from_str( http_fetch(self.api_key.as_str(), image_ref_url, &self.proxy_config)?.as_str(), @@ -879,7 +902,8 @@ impl Document { for file_hash in &project_files.files { if let Some(doc_id) = file_hash.get("key") { if let Some(name) = file_hash.get("name") { - figma_docs.push(FigmaDocInfo::new(name.clone(), doc_id.clone())); + // Getting project files return head version of the files. + figma_docs.push(FigmaDocInfo::new(name.clone(), doc_id.clone(), String::new())); } } } diff --git a/crates/figma_import/src/fetch.rs b/crates/figma_import/src/fetch.rs index 7b4c63259..d2f423108 100644 --- a/crates/figma_import/src/fetch.rs +++ b/crates/figma_import/src/fetch.rs @@ -74,12 +74,14 @@ pub enum ConvertResponse { pub fn fetch_doc( id: &str, + requested_version_id: &str, rq: ConvertRequest, proxy_config: &ProxyConfig, ) -> Result { if let Some(mut doc) = Document::new_if_changed( rq.figma_api_key, id.into(), + requested_version_id.into(), proxy_config, rq.last_modified.unwrap_or(String::new()), rq.version.unwrap_or(String::new()), diff --git a/designcompose/src/androidTest/kotlin/com/android/designcompose/RenderTests.kt b/designcompose/src/androidTest/kotlin/com/android/designcompose/RenderTests.kt index 7becfc72c..3927a7ceb 100644 --- a/designcompose/src/androidTest/kotlin/com/android/designcompose/RenderTests.kt +++ b/designcompose/src/androidTest/kotlin/com/android/designcompose/RenderTests.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.tooling.preview.Preview import androidx.test.platform.app.InstrumentationRegistry +import com.android.designcompose.common.DesignDocId import com.android.designcompose.test.assertDCRenderStatus import com.android.designcompose.test.onDCDoc import kotlin.test.assertNotNull @@ -32,7 +33,12 @@ import org.junit.Test @Preview @Composable fun DesignSwitcherDeadbeef() { - DesignSwitcher(doc = null, currentDocId = "DEADBEEF", branchHash = null, setDocId = {}) + DesignSwitcher( + doc = null, + currentDocId = DesignDocId("DEADBEEF"), + branchHash = null, + setDocId = {} + ) } /** @@ -82,7 +88,7 @@ class RenderTests { .assertExists() } // It was not loaded from disk and did not render - with(DesignSettings.testOnlyFigmaFetchStatus(helloWorldDocId)) { + with(DesignSettings.testOnlyFigmaFetchStatus(DesignDocId(helloWorldDocId))) { assertNotNull(this) assertNull(lastLoadFromDisk) assertNull(lastFetch) diff --git a/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/JniFetchTests.kt b/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/JniFetchTests.kt index 3e6b473e2..a6e4020d5 100644 --- a/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/JniFetchTests.kt +++ b/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/JniFetchTests.kt @@ -23,6 +23,7 @@ import com.android.designcompose.FigmaFileNotFoundException import com.android.designcompose.Jni import com.android.designcompose.LiveUpdate import com.android.designcompose.ProxyConfig +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.DocumentServerParams import com.android.designcompose.constructPostJson import com.android.designcompose.decodeServerDoc @@ -63,16 +64,16 @@ class JniFetchTests { @Test fun invalidDocId() { assertFailsWith { - Jni.jniFetchDoc("InvalidDocID", firstFetchJson, ProxyConfig()) + Jni.jniFetchDoc("InvalidDocID", "", firstFetchJson, ProxyConfig()) } } private fun testFetch(docID: String) { - with(LiveUpdate.fetchDocBytes(docID, firstFetchJson, ProxyConfig())) { + with(LiveUpdate.fetchDocBytes(DesignDocId(docID), firstFetchJson, ProxyConfig())) { assertNotNull(this) - val decodedDoc = decodeServerDoc(this, null, docID, null, Feedback) + val decodedDoc = decodeServerDoc(this, null, DesignDocId(docID), null, Feedback) assertNotNull(decodedDoc) - assertEquals(decodedDoc.c.docId, docID) + assertEquals(decodedDoc.c.docId.id, docID) } } @@ -94,7 +95,7 @@ class JniFetchTests { @Test fun invalidToken() { assertFailsWith(AccessDeniedException::class) { - Jni.jniFetchDoc("DummyDocId", dummyFigmaTokenJson, ProxyConfig()) + Jni.jniFetchDoc("DummyDocId", "", dummyFigmaTokenJson, ProxyConfig()) } } } diff --git a/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/LiveUpdateBehaviorTests.kt b/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/LiveUpdateBehaviorTests.kt index df508940d..3994b8565 100644 --- a/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/LiveUpdateBehaviorTests.kt +++ b/designcompose/src/androidTest/kotlin/com/android/designcompose/figmaIntegrationTests/LiveUpdateBehaviorTests.kt @@ -24,6 +24,7 @@ import com.android.designcompose.HelloWorldDoc import com.android.designcompose.TestUtils import com.android.designcompose.annotation.DesignComponent import com.android.designcompose.annotation.DesignDoc +import com.android.designcompose.common.DesignDocId import com.android.designcompose.helloWorldDocId import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -63,7 +64,7 @@ class LiveUpdateBehaviorTests { composeTestRule.setContent { HelloWorldDoc.mainFrame(name = "Testers!") } TestUtils.triggerLiveUpdate() - with(DesignSettings.testOnlyFigmaFetchStatus(helloWorldDocId)) { + with(DesignSettings.testOnlyFigmaFetchStatus(DesignDocId(helloWorldDocId))) { assertNotNull(this) assertNull(lastLoadFromDisk) assertNotNull(lastFetch) @@ -84,7 +85,7 @@ class LiveUpdateBehaviorTests { .assertExists() // The doc was fetched - with(DesignSettings.testOnlyFigmaFetchStatus(helloWorldDocId)) { + with(DesignSettings.testOnlyFigmaFetchStatus(DesignDocId(helloWorldDocId))) { assertNotNull(this) assertNotNull(lastUpdateFromFetch) assertNotNull(lastFetch) diff --git a/designcompose/src/main/java/com/android/designcompose/DesignSwitcher.kt b/designcompose/src/main/java/com/android/designcompose/DesignSwitcher.kt index de20c3224..8ac190341 100644 --- a/designcompose/src/main/java/com/android/designcompose/DesignSwitcher.kt +++ b/designcompose/src/main/java/com/android/designcompose/DesignSwitcher.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.DocumentServerParams import com.android.designcompose.common.FeedbackLevel import com.android.designcompose.serdegen.NodeQuery @@ -212,12 +213,12 @@ private interface DesignSwitcher { @Composable fun FigmaDoc( name: String, - id: String, + docId: String, modifier: Modifier, ) { val customizations = CustomizationContext() customizations.setText("#Name", name) - customizations.setText("#Id", id) + customizations.setText("#Id", docId) customizations.setModifier("#GoButton", modifier) CompositionLocalProvider(LocalCustomizationContext provides customizations) { @@ -296,7 +297,7 @@ private interface DesignSwitcher { internal object DesignSwitcherDoc : DesignSwitcher {} -internal fun designSwitcherDocId() = "Ljph4e3sC0lHcynfXpoh9f" +internal fun designSwitcherDocId() = DesignDocId("Ljph4e3sC0lHcynfXpoh9f") internal fun designSwitcherDocName() = "DesignSwitcherDoc" @@ -348,7 +349,7 @@ private fun elapsedTimeString(elapsedSeconds: Long): String { private fun GetBranches( branchHash: HashMap?, - setDocId: (String) -> Unit, + setDocId: (DesignDocId) -> Unit, interactionState: InteractionState ): ReplacementContent { val branchList = branchHash?.toList() ?: listOf() @@ -361,7 +362,7 @@ private fun GetBranches( branchList[index].first, Modifier.clickable { interactionState.close(null) - setDocId(branchList[index].first) + setDocId(DesignDocId(branchList[index].first)) }, ) } @@ -376,19 +377,20 @@ private fun GetProjectFileCount(doc: DocContent?): String { private fun GetProjectList( doc: DocContent?, - setDocId: (String) -> Unit, + setDocId: (DesignDocId) -> Unit, interactionState: InteractionState ): ReplacementContent { return ReplacementContent( count = doc?.c?.project_files?.size ?: 0, content = { index -> { + val docId = doc?.c?.project_files?.get(index)?.id ?: "" DesignSwitcherDoc.FigmaDoc( doc?.c?.project_files?.get(index)?.name ?: "", - doc?.c?.project_files?.get(index)?.id ?: "", + docId, Modifier.clickable { interactionState.close(null) - setDocId(doc?.c?.project_files?.get(index)?.id ?: "") + setDocId(DesignDocId(docId)) }, ) } @@ -397,7 +399,7 @@ private fun GetProjectList( } @Composable -private fun GetMessages(docId: String): ReplacementContent { +private fun GetMessages(docId: DesignDocId): ReplacementContent { val (_, setMessagesId) = remember { mutableStateOf(0) } DisposableEffect(docId) { Feedback.register(docId, setMessagesId) @@ -527,9 +529,9 @@ private fun GetShowRecompositionCheckbox( @Composable internal fun DesignSwitcher( doc: DocContent?, - currentDocId: String, + currentDocId: DesignDocId, branchHash: HashMap?, - setDocId: (String) -> Unit + setDocId: (DesignDocId) -> Unit ) { remember { Feedback.addIgnoredDocument(designSwitcherDocId()) } val (docIdText, setDocIdText) = remember { mutableStateOf("") } @@ -585,7 +587,7 @@ internal fun DesignSwitcher( Modifier.onKeyEvent { if (it.nativeKeyEvent.keyCode == android.view.KeyEvent.KEYCODE_ENTER) { interactionState.close(null) - setDocId(docIdText.trim()) + setDocId(DesignDocId(docIdText.trim())) true } else { false @@ -598,7 +600,7 @@ internal fun DesignSwitcher( Modifier.clickable { if (docIdText.isNotEmpty()) { interactionState.close(null) - setDocId(docIdText) + setDocId(DesignDocId(docIdText)) } }, node_names_checkbox = GetNodeNamesCheckbox(nodeNamesChecked, setNodeNamesChecked), diff --git a/designcompose/src/main/java/com/android/designcompose/DesignView.kt b/designcompose/src/main/java/com/android/designcompose/DesignView.kt index 36d8fe8d9..5930febd5 100644 --- a/designcompose/src/main/java/com/android/designcompose/DesignView.kt +++ b/designcompose/src/main/java/com/android/designcompose/DesignView.kt @@ -74,6 +74,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.android.designcompose.annotation.DesignMetaKey +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.DocumentServerParams import com.android.designcompose.serdegen.Action import com.android.designcompose.serdegen.ComponentInfo @@ -208,7 +209,7 @@ internal object DebugNodeManager { showRecomposition.postValue(show) } - internal fun addNode(docId: String, existingId: Int, node: NodePosition): Int { + internal fun addNode(docId: DesignDocId, existingId: Int, node: NodePosition): Int { if ( !showNodes.value!! || !node.nodeName.startsWith("#") || @@ -342,7 +343,7 @@ internal fun DesignView( modifier: Modifier = Modifier, v: View, variantParentName: String, - docId: String, + docId: DesignDocId, document: DocContent, customizations: CustomizationContext, interactionState: InteractionState, @@ -859,7 +860,7 @@ val LocalCustomizationContext = compositionLocalOf { CustomizationContext() } // Current document override ID that can be used to override the document ID specified from the // @DesignDoc annotation -internal val LocalDocOverrideContext = compositionLocalOf { String() } +internal val LocalDocOverrideContext = compositionLocalOf { DesignDocId("") } // Public function to set the document override ID @Composable @@ -869,7 +870,7 @@ internal val LocalDocOverrideContext = compositionLocalOf { String() } " than one root document is used, all will instead use this document ID. Use this function only" + " when there is no other way to set the document ID." ) -fun DesignDocOverride(docId: String, content: @Composable () -> Unit) { +fun DesignDocOverride(docId: DesignDocId, content: @Composable () -> Unit) { CompositionLocalProvider(LocalDocOverrideContext provides docId) { content() } } @@ -877,18 +878,18 @@ fun DesignDocOverride(docId: String, content: @Composable () -> Unit) { // When switching document IDs, we notify all subscribers of the change to trigger // a recomposition. internal object DocumentSwitcher { - private val subscribers: HashMap Unit>> = HashMap() - private val documentSwitchHash: HashMap = HashMap() - private val documentSwitchReverseHash: HashMap = HashMap() + private val subscribers: HashMap Unit>> = HashMap() + private val documentSwitchHash: HashMap = HashMap() + private val documentSwitchReverseHash: HashMap = HashMap() - internal fun subscribe(originalDocId: String, setDocId: (String) -> Unit) { + internal fun subscribe(originalDocId: DesignDocId, setDocId: (DesignDocId) -> Unit) { val list = subscribers[originalDocId] ?: ArrayList() list.add(setDocId) subscribers[originalDocId] = list } - internal fun switch(originalDocId: String, newDocId: String) { - if (newDocId.isEmpty()) return + internal fun switch(originalDocId: DesignDocId, newDocId: DesignDocId) { + if (!newDocId.isValid()) return if (originalDocId != newDocId) { documentSwitchHash[originalDocId] = newDocId documentSwitchReverseHash[newDocId] = originalDocId @@ -900,7 +901,7 @@ internal object DocumentSwitcher { list?.forEach { it(newDocId) } } - internal fun revertToOriginal(docId: String) { + internal fun revertToOriginal(docId: DesignDocId) { val originalDocId = documentSwitchReverseHash[docId] if (originalDocId != null) { switch(originalDocId, originalDocId) @@ -908,12 +909,12 @@ internal object DocumentSwitcher { } } - internal fun isNotOriginalDocId(docId: String): Boolean { + internal fun isNotOriginalDocId(docId: DesignDocId): Boolean { val originalDocId = documentSwitchReverseHash[docId] return originalDocId != null } - internal fun getSwitchedDocId(docId: String): String { + internal fun getSwitchedDocId(docId: DesignDocId): DesignDocId { return documentSwitchHash[docId] ?: docId } } @@ -930,20 +931,20 @@ enum class LiveUpdateMode { } class DesignComposeCallbacks( - val docReadyCallback: ((String) -> Unit)? = null, - val newDocDataCallback: ((String, ByteArray?) -> Unit)? = null, + val docReadyCallback: ((DesignDocId) -> Unit)? = null, + val newDocDataCallback: ((DesignDocId, ByteArray?) -> Unit)? = null, ) @Composable fun DesignDoc( docName: String, - docId: String, + docId: DesignDocId, rootNodeQuery: NodeQuery, modifier: Modifier = Modifier, placeholder: (@Composable () -> Unit)? = null, customizations: CustomizationContext = CustomizationContext(), serverParams: DocumentServerParams = DocumentServerParams(), - setDocId: (String) -> Unit = {}, + setDocId: (DesignDocId) -> Unit = {}, designSwitcherPolicy: DesignSwitcherPolicy = DesignSwitcherPolicy.SHOW_IF_ROOT, designComposeCallbacks: DesignComposeCallbacks? = null, parentComponents: List = listOf(), @@ -968,13 +969,13 @@ fun DesignDoc( @Composable internal fun DesignDocInternal( docName: String, - incomingDocId: String, + incomingDocId: DesignDocId, rootNodeQuery: NodeQuery, modifier: Modifier = Modifier, placeholder: (@Composable () -> Unit)? = null, customizations: CustomizationContext = CustomizationContext(), serverParams: DocumentServerParams = DocumentServerParams(), - setDocId: (String) -> Unit = {}, + setDocId: (DesignDocId) -> Unit = {}, designSwitcherPolicy: DesignSwitcherPolicy = DesignSwitcherPolicy.SHOW_IF_ROOT, liveUpdateMode: LiveUpdateMode = LiveUpdateMode.LIVE, designComposeCallbacks: DesignComposeCallbacks? = null, @@ -998,7 +999,7 @@ internal fun DesignDocInternal( var docRenderStatus by remember { mutableStateOf(DocRenderStatus.NotAvailable) } val overrideDocId = LocalDocOverrideContext.current // Use the override document ID if it is not empty - val currentDocId = if (overrideDocId.isNotEmpty()) overrideDocId else incomingDocId + val currentDocId = if (overrideDocId.isValid()) overrideDocId else incomingDocId val docId = DocumentSwitcher.getSwitchedDocId(currentDocId) val doc = DocServer.doc( @@ -1042,7 +1043,7 @@ internal fun DesignDocInternal( DocumentSwitcher.subscribe(docId, setDocId) docId } - val switchDocId: (String) -> Unit = { newDocId: String -> + val switchDocId: (DesignDocId) -> Unit = { newDocId: DesignDocId -> run { DocumentSwitcher.switch(originalDocId, newDocId) } } diff --git a/designcompose/src/main/java/com/android/designcompose/DocContent.kt b/designcompose/src/main/java/com/android/designcompose/DocContent.kt index 5b75cbe39..60a95a979 100644 --- a/designcompose/src/main/java/com/android/designcompose/DocContent.kt +++ b/designcompose/src/main/java/com/android/designcompose/DocContent.kt @@ -19,6 +19,7 @@ package com.android.designcompose import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.tracing.trace +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.FeedbackImpl import com.android.designcompose.common.GenericDocContent import com.android.designcompose.common.decodeDiskBaseDoc @@ -90,7 +91,7 @@ class DocContent(var c: GenericDocContent, previousDoc: DocContent?) { fun decodeDiskDoc( docStream: InputStream, previousDoc: DocContent?, - docId: String, + docId: DesignDocId, feedback: FeedbackImpl ): DocContent? { var docContent: DocContent? = null @@ -104,7 +105,7 @@ fun decodeDiskDoc( fun decodeServerDoc( docBytes: ByteArray, previousDoc: DocContent?, - docId: String, + docId: DesignDocId, save: File?, feedback: FeedbackImpl ): DocContent? { diff --git a/designcompose/src/main/java/com/android/designcompose/DocServer.kt b/designcompose/src/main/java/com/android/designcompose/DocServer.kt index e7ea8cacf..bd9bd581e 100644 --- a/designcompose/src/main/java/com/android/designcompose/DocServer.kt +++ b/designcompose/src/main/java/com/android/designcompose/DocServer.kt @@ -34,6 +34,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.tracing.Trace.beginSection import androidx.tracing.Trace.endSection +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.DocumentServerParams import java.io.BufferedInputStream import java.io.File @@ -51,9 +52,9 @@ internal const val TAG = "DesignCompose" internal class LiveDocSubscription( val id: String, - val docId: String, + val docId: DesignDocId, val onUpdate: (DocContent?) -> Unit, - val docUpdateCallback: ((String, ByteArray?) -> Unit)?, + val docUpdateCallback: ((DesignDocId, ByteArray?) -> Unit)?, ) internal class LiveDocSubscriptions( @@ -92,11 +93,11 @@ object DesignSettings { internal var figmaToken = mutableStateOf(null) internal var isDocumentLive = mutableStateOf(false) private var fontDb: HashMap = HashMap() - internal var fileFetchStatus: HashMap = HashMap() + internal var fileFetchStatus: HashMap = HashMap() @VisibleForTesting @RestrictTo(RestrictTo.Scope.TESTS) - fun testOnlyFigmaFetchStatus(fileId: String) = fileFetchStatus[fileId] + fun testOnlyFigmaFetchStatus(fileId: DesignDocId) = fileFetchStatus[fileId] fun enableLiveUpdates( activity: ComponentActivity, @@ -211,10 +212,10 @@ internal object SpanCache { internal object DocServer { internal const val FETCH_INTERVAL_MILLIS: Long = 5000L internal const val DEFAULT_HTTP_PROXY_PORT = "3128" - internal val documents: HashMap = HashMap() - internal val subscriptions: HashMap = HashMap() + internal val documents: HashMap = HashMap() + internal val subscriptions: HashMap = HashMap() internal val branchHash: HashMap> = - HashMap() // doc ID -> { docID -> docName } + HashMap() // doc ID -> { docID -> docName }, branches always point to head of the figma doc internal val mainHandler = Handler(Looper.getMainLooper()) internal var firstFetch = true internal var pauseUpdates = false @@ -405,36 +406,37 @@ internal fun DocServer.unsubscribe(doc: LiveDocSubscription) { } } -private fun DocServer.updateBranches(docId: String, doc: DocContent) { - val docBranches = branchHash[docId] ?: HashMap() +private fun DocServer.updateBranches(docId: DesignDocId, doc: DocContent) { + val id = docId.id + val docBranches = branchHash[id] ?: HashMap() doc.c.branches?.forEach { if (!docBranches.containsKey(it.id)) docBranches[it.id] = it.name } // Create a "Main" branch for this doc ID so that we can go back to it after switching to a // branch - if (doc.c.branches?.isNotEmpty() == true && !docBranches.containsKey(docId)) - docBranches[docId] = "Main" + if (doc.c.branches?.isNotEmpty() == true && !docBranches.containsKey(id)) + docBranches[id] = "Main" // Update the branch list for this ID as well as all branches of this ID - branchHash[docId] = docBranches + branchHash[id] = docBranches doc.c.branches?.forEach { branchHash[it.id] = docBranches } } @Composable internal fun DocServer.doc( resourceName: String, - docId: String, + docId: DesignDocId, serverParams: DocumentServerParams, - docUpdateCallback: ((String, ByteArray?) -> Unit)?, + docUpdateCallback: ((DesignDocId, ByteArray?) -> Unit)?, disableLiveMode: Boolean, ): DocContent? { // Check that the document ID is valid - if (!validateFigmaDocId(docId)) { + if (!validateFigmaDocId(docId.id)) { Log.w(TAG, "Invalid Figma document ID: $docId") return null } beginSection(DCTraces.DOCSERVER_DOC) - val id = "${resourceName}_$docId" + val id = "${resourceName}_${docId}" // Create a state var to remember the document contents and update it when the doc changes val (liveDoc, setLiveDoc) = remember { mutableStateOf(null) } @@ -530,6 +532,6 @@ internal fun DocServer.doc( return null } -internal fun DocServer.branches(docId: String): HashMap? { - return branchHash[docId] +internal fun DocServer.branches(docId: DesignDocId): HashMap? { + return branchHash[docId.id] } diff --git a/designcompose/src/main/java/com/android/designcompose/Feedback.kt b/designcompose/src/main/java/com/android/designcompose/Feedback.kt index 1ce174c94..05405f8cb 100644 --- a/designcompose/src/main/java/com/android/designcompose/Feedback.kt +++ b/designcompose/src/main/java/com/android/designcompose/Feedback.kt @@ -21,12 +21,13 @@ import android.os.Looper import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.FeedbackImpl import com.android.designcompose.common.FeedbackLevel object Feedback : FeedbackImpl() { private val lastMessage: MutableLiveData = MutableLiveData("") - val subscribers: HashMap Unit> = HashMap() + val subscribers: HashMap Unit> = HashMap() // Implementation-specific functions override fun logMessage(str: String, level: FeedbackLevel) { @@ -44,39 +45,39 @@ object Feedback : FeedbackImpl() { } // Register and unregister a listener of feedback messages - fun register(id: String, setMessagesId: (Int) -> Unit) { + fun register(id: DesignDocId, setMessagesId: (Int) -> Unit) { subscribers[id] = setMessagesId } - fun unregister(id: String) { + fun unregister(id: DesignDocId) { subscribers.remove(id) } // Message functions - fun addSubscriber(docId: String) { + fun addSubscriber(docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Add subscriber for $truncatedId", FeedbackLevel.Debug, docId) } - fun removeSubscriber(docId: String) { + fun removeSubscriber(docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Remove subscriber for $truncatedId", FeedbackLevel.Debug, docId) } - internal fun assetLoadFail(id: String, docId: String) { - setStatus("Unable to open $id from assets", FeedbackLevel.Debug, docId) + internal fun assetLoadFail(dcfAssetId: String, docId: DesignDocId) { + setStatus("Unable to open $dcfAssetId from assets", FeedbackLevel.Debug, docId) } - internal fun startLiveUpdate(docId: String) { + internal fun startLiveUpdate(docId: DesignDocId) { val truncatedId = shortDocId(docId) setStatus("Live update fetching $truncatedId", FeedbackLevel.Debug, docId) } - fun documentDecodeImages(numImages: Int, name: String, docId: String) { + fun documentDecodeImages(numImages: Int, name: String, docId: DesignDocId) { setStatus("Decoded $numImages images for $name", FeedbackLevel.Info, docId) } - override fun setStatus(str: String, level: FeedbackLevel, docId: String) { + override fun setStatus(str: String, level: FeedbackLevel, docId: DesignDocId) { super.setStatus(str, level, docId) lastMessage.postValue(str) diff --git a/designcompose/src/main/java/com/android/designcompose/InteractionState.kt b/designcompose/src/main/java/com/android/designcompose/InteractionState.kt index 9d7921414..7ba788dfd 100644 --- a/designcompose/src/main/java/com/android/designcompose/InteractionState.kt +++ b/designcompose/src/main/java/com/android/designcompose/InteractionState.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.VariantPropertyMap import com.android.designcompose.serdegen.Action import com.android.designcompose.serdegen.Navigation @@ -766,9 +767,9 @@ internal fun InteractionState.squooshRootNode( /// InteractionState as a global ensuring that views can access the correct state regardless /// of the view tree organization. internal object InteractionStateManager { - val states: HashMap = HashMap() + val states: HashMap = HashMap() } -internal fun InteractionStateManager.stateForDoc(docId: String): InteractionState { +internal fun InteractionStateManager.stateForDoc(docId: DesignDocId): InteractionState { return states.getOrPut(docId) { InteractionState() } } diff --git a/designcompose/src/main/java/com/android/designcompose/Jni.kt b/designcompose/src/main/java/com/android/designcompose/Jni.kt index a1d92c2d3..da4f1978f 100644 --- a/designcompose/src/main/java/com/android/designcompose/Jni.kt +++ b/designcompose/src/main/java/com/android/designcompose/Jni.kt @@ -43,15 +43,23 @@ internal class TextSize( @Keep internal object Jni { - fun tracedJnifetchdoc(docId: String, requestJson: String, proxyConfig: ProxyConfig): ByteArray { + fun tracedJnifetchdoc( + docId: String, + versionId: String, + requestJson: String, + proxyConfig: ProxyConfig + ): ByteArray { lateinit var result: ByteArray - trace(DCTraces.JNIFETCHDOC) { result = jniFetchDoc(docId, requestJson, proxyConfig) } + trace(DCTraces.JNIFETCHDOC) { + result = jniFetchDoc(docId, versionId, requestJson, proxyConfig) + } return result } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) external fun jniFetchDoc( docId: String, + versionId: String, requestJson: String, proxyConfig: ProxyConfig ): ByteArray diff --git a/designcompose/src/main/java/com/android/designcompose/LiveUpdate.kt b/designcompose/src/main/java/com/android/designcompose/LiveUpdate.kt index 8b85d2d7b..eec98a222 100644 --- a/designcompose/src/main/java/com/android/designcompose/LiveUpdate.kt +++ b/designcompose/src/main/java/com/android/designcompose/LiveUpdate.kt @@ -16,12 +16,18 @@ package com.android.designcompose +import com.android.designcompose.common.DesignDocId import com.android.designcompose.serdegen.ConvertResponse import com.novi.bincode.BincodeDeserializer internal object LiveUpdate { - fun fetchDocBytes(docId: String, requestJson: String, proxyConfig: ProxyConfig): ByteArray? { - val serializedResponse: ByteArray = Jni.tracedJnifetchdoc(docId, requestJson, proxyConfig) + fun fetchDocBytes( + docId: DesignDocId, + requestJson: String, + proxyConfig: ProxyConfig + ): ByteArray? { + val serializedResponse: ByteArray = + Jni.tracedJnifetchdoc(docId.id, docId.versionId, requestJson, proxyConfig) val deserializer = BincodeDeserializer(serializedResponse) val convResp = ConvertResponse.deserialize(deserializer) if (convResp is ConvertResponse.Document) return convResp.value.toByteArray() diff --git a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt index c1648ff6b..06a6dc658 100644 --- a/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt +++ b/designcompose/src/main/java/com/android/designcompose/squoosh/SquooshRoot.kt @@ -59,6 +59,7 @@ import com.android.designcompose.asAnimationSpec import com.android.designcompose.asBuilder import com.android.designcompose.branches import com.android.designcompose.clonedWithAnimatedActionsApplied +import com.android.designcompose.common.DesignDocId import com.android.designcompose.common.DocumentServerParams import com.android.designcompose.doc import com.android.designcompose.rootNode @@ -240,12 +241,12 @@ internal val LocalSquooshIsRootContext = compositionLocalOf { SquooshIsRoot(true @Composable fun SquooshRoot( docName: String, - incomingDocId: String, + incomingDocId: DesignDocId, rootNodeQuery: NodeQuery, modifier: Modifier = Modifier, customizationContext: CustomizationContext = CustomizationContext(), serverParams: DocumentServerParams = DocumentServerParams(), - setDocId: (String) -> Unit = {}, + setDocId: (DesignDocId) -> Unit = {}, designSwitcherPolicy: DesignSwitcherPolicy = DesignSwitcherPolicy.SHOW_IF_ROOT, liveUpdateMode: LiveUpdateMode = LiveUpdateMode.LIVE, designComposeCallbacks: DesignComposeCallbacks? = null, @@ -274,7 +275,7 @@ fun SquooshRoot( DocumentSwitcher.subscribe(docId, setDocId) docId } - val switchDocId: (String) -> Unit = { newDocId: String -> + val switchDocId: (DesignDocId) -> Unit = { newDocId: DesignDocId -> run { DocumentSwitcher.switch(originalDocId, newDocId) } } diff --git a/designcompose/src/testDebug/kotlin/com/android/designcompose/DesignSwitcherBasicTests.kt b/designcompose/src/testDebug/kotlin/com/android/designcompose/DesignSwitcherBasicTests.kt index 12367058f..eff7e1a79 100644 --- a/designcompose/src/testDebug/kotlin/com/android/designcompose/DesignSwitcherBasicTests.kt +++ b/designcompose/src/testDebug/kotlin/com/android/designcompose/DesignSwitcherBasicTests.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.designcompose.common.DesignDocId import com.android.designcompose.test.internal.captureRootRoboImage import com.android.designcompose.test.internal.designComposeRoborazziRule import com.android.designcompose.test.onDCDoc @@ -39,7 +40,12 @@ import org.robolectric.annotation.GraphicsMode @Composable fun DesignSwitcherTest(testName: TestName) { val idState = remember { mutableStateOf(testName.methodName) } - DesignSwitcher(doc = null, currentDocId = idState.value, branchHash = null, setDocId = {}) + DesignSwitcher( + doc = null, + currentDocId = DesignDocId(idState.value), + branchHash = null, + setDocId = {} + ) } @RunWith(AndroidJUnit4::class) diff --git a/integration-tests/validation/src/main/assets/figma/HelloVersionDoc_v62Vwlxa4Bb6nopJiAxQAQ_5668177823.dcf b/integration-tests/validation/src/main/assets/figma/HelloVersionDoc_v62Vwlxa4Bb6nopJiAxQAQ_5668177823.dcf new file mode 100644 index 000000000..0091bb67a Binary files /dev/null and b/integration-tests/validation/src/main/assets/figma/HelloVersionDoc_v62Vwlxa4Bb6nopJiAxQAQ_5668177823.dcf differ diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt index ead2d75dd..53f9d8f04 100644 --- a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/AllExamples.kt @@ -25,6 +25,7 @@ val EXAMPLES: ArrayList Unit, String?>> = Triple("Hello", { HelloWorld() }, HelloWorldDoc.javaClass.name), Triple("HelloBye", { HelloBye() }, HelloByeDoc.javaClass.name), Triple("HelloSquoosh", { HelloSquoosh() }, HelloWorldDoc.javaClass.name), + Triple("HelloVersion", { HelloVersion() }, HelloVersionDoc.javaClass.name), Triple("Image Update", { ImageUpdateTest() }, ImageUpdateTestDoc.javaClass.name), Triple("Telltales", { TelltaleTest() }, TelltaleTestDoc.javaClass.name), Triple("OpenLink", { OpenLinkTest() }, OpenLinkTestDoc.javaClass.name), diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/HelloBye.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/HelloBye.kt index 2312f025b..2803381f1 100644 --- a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/HelloBye.kt +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/HelloBye.kt @@ -21,6 +21,7 @@ import com.android.designcompose.DesignDocOverride import com.android.designcompose.annotation.Design import com.android.designcompose.annotation.DesignComponent import com.android.designcompose.annotation.DesignDoc +import com.android.designcompose.common.DesignDocId // Hello World with override to change doc ID @DesignDoc(id = "pxVlixodJqZL95zo2RzTHl") @@ -30,5 +31,5 @@ interface HelloBye { @Composable fun HelloBye() { - DesignDocOverride("MCHaMYcIEnRpbvU9Ms7a0o") { HelloByeDoc.Main(name = "World") } + DesignDocOverride(DesignDocId("MCHaMYcIEnRpbvU9Ms7a0o")) { HelloByeDoc.Main(name = "World") } } diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/HelloVersion.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/HelloVersion.kt new file mode 100644 index 000000000..4bf0badc6 --- /dev/null +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/HelloVersion.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.designcompose.testapp.validation.examples + +import android.util.Log +import androidx.compose.runtime.Composable +import com.android.designcompose.DesignComposeCallbacks +import com.android.designcompose.annotation.Design +import com.android.designcompose.annotation.DesignComponent +import com.android.designcompose.annotation.DesignDoc + +@DesignDoc(id = "v62Vwlxa4Bb6nopJiAxQAQ", designVersion = "5668177823") +interface HelloVersion { + @DesignComponent(node = "#MainFrame") fun Main(@Design(node = "#Name") name: String) +} + +@Composable +fun HelloVersion() { + HelloVersionDoc.Main( + name = "Version", + designComposeCallbacks = + DesignComposeCallbacks( + docReadyCallback = { id -> + Log.i("DesignCompose", "HelloWorld Ready: doc ID = $id") + }, + newDocDataCallback = { docId, data -> + Log.i( + "DesignCompose", + "HelloWorld Updated doc ID $docId: ${data?.size ?: 0} bytes" + ) + }, + ) + ) +} diff --git a/integration-tests/validation/src/testDebug/roborazzi/RenderAllExamples/HelloVersion.png b/integration-tests/validation/src/testDebug/roborazzi/RenderAllExamples/HelloVersion.png new file mode 100644 index 000000000..851fb2783 Binary files /dev/null and b/integration-tests/validation/src/testDebug/roborazzi/RenderAllExamples/HelloVersion.png differ diff --git a/reference-apps/aaos-unbundled/mediacompose/src/main/java/com/android/designcompose/reference/mediacompose/MainActivity.kt b/reference-apps/aaos-unbundled/mediacompose/src/main/java/com/android/designcompose/reference/mediacompose/MainActivity.kt index 0f77d8b9d..db7e14e2e 100644 --- a/reference-apps/aaos-unbundled/mediacompose/src/main/java/com/android/designcompose/reference/mediacompose/MainActivity.kt +++ b/reference-apps/aaos-unbundled/mediacompose/src/main/java/com/android/designcompose/reference/mediacompose/MainActivity.kt @@ -68,7 +68,7 @@ enum class NavButtonType { // Media 4 5n0LhOQ6wOiDxrH0YUVhJS // Media 5 dui99iAKZ273s7RN11Z9Ak // Media Nova 2DQtQOf6U26mA8dqBie3gT -@DesignDoc(id = "7rvM6aVWe0jZCm7jhO9ITx", version = "0.1") +@DesignDoc(id = "7rvM6aVWe0jZCm7jhO9ITx", customizationInterfaceVersion = "0.1") interface CenterDisplay { @DesignComponent(node = "#stage", isRoot = true) fun MainFrame( diff --git a/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt b/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt index b19041c1e..792d2af92 100644 --- a/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt +++ b/reference-apps/tutorial/app/src/main/java/com/android/designcompose/tutorial/MainActivity.kt @@ -78,7 +78,7 @@ enum class ButtonState { pressed, } -@DesignDoc(id = "3z4xExq0INrL9vxPhj9tl7", version = "0.8") +@DesignDoc(id = "3z4xExq0INrL9vxPhj9tl7", customizationInterfaceVersion = "0.8") interface Tutorial { @DesignComponent(node = "#stage", isRoot = true) fun Stage(