This shows you the differences between two versions of the page.
smd:laboratoare:02 [2020/02/28 18:19] adriana.draghici [Lab 02. Communication between Android components using Intents] |
smd:laboratoare:02 [2021/03/25 20:41] (current) adriana.draghici [Task 3 - Make a call (1p)] |
||
---|---|---|---|
Line 4: | Line 4: | ||
===== Objectives ===== | ===== Objectives ===== | ||
* Learn how to communicate between Android components using Intents and Broadcast Receivers | * Learn how to communicate between Android components using Intents and Broadcast Receivers | ||
- | * Learn how to listen for events from the system using Broadcast Receivers | + | * Learn how to listen for events from the system using Broadcast Receivers |
+ | * Use several types of Intents and Broadcast Receivers inside an app to communicate between activities and to start other apps. | ||
Line 10: | Line 11: | ||
===== What are Intents? ===== | ===== What are Intents? ===== | ||
- | In Android, applications and application components can interact with one another by using **Intent** objects. An **Intent** is a messaging object used to request an action from another application component. It can be used between components of the same app or from different applications. | + | In Android, applications and application components can interact with one another by using **Intent** objects. An **Intent** is a **messaging** object used to request an action from another application component. It can be used between components of the same app or from different applications. |
- | Usually an intent is used for: | + | Usually **an intent is used for**: |
* Launching an activity from the same Android application or other activities from other applications; | * Launching an activity from the same Android application or other activities from other applications; | ||
* For delivering a broadcast message; | * For delivering a broadcast message; | ||
Line 18: | Line 19: | ||
An Intent is an object of type [[https://developer.android.com/reference/android/content/Intent.html|android.content.Intent]]. This is sent as a parameter for methods, [[https://developer.android.com/reference/android/content/Context.html#startActivity(android.content.Intent)|startActivity()]], [[https://developer.android.com/reference/android/content/Context.html#startService(android.content.Intent)|startService()]] or [[https://developer.android.com/reference/android/content/Context.html#sendBroadcast(android.content.Intent)|sendBroadcast()]], for starting an activity, a service or sending a broadcast message. The intent can transmit data to the components via a [[https://developer.android.com/reference/android/os/Bundle|Bundle]] object. | An Intent is an object of type [[https://developer.android.com/reference/android/content/Intent.html|android.content.Intent]]. This is sent as a parameter for methods, [[https://developer.android.com/reference/android/content/Context.html#startActivity(android.content.Intent)|startActivity()]], [[https://developer.android.com/reference/android/content/Context.html#startService(android.content.Intent)|startService()]] or [[https://developer.android.com/reference/android/content/Context.html#sendBroadcast(android.content.Intent)|sendBroadcast()]], for starting an activity, a service or sending a broadcast message. The intent can transmit data to the components via a [[https://developer.android.com/reference/android/os/Bundle|Bundle]] object. | ||
+ | |||
+ | ==== Example explicit intent ==== | ||
+ | Launch another activity using an explicit intent. | ||
+ | <code Java> | ||
+ | Intent intent = new Intent( | ||
+ | context, // Application context | ||
+ | Activity.class); // Component name | ||
+ | startActivity(intent); // Launch component | ||
+ | </code> | ||
+ | |||
+ | ==== Example implicit intent ==== | ||
+ | |||
+ | Open an url in your phone's browser using an implicit intent - not specifying the exact app which will open the url, only tell the system that it need to be opened. | ||
+ | |||
+ | <code Java> | ||
+ | Intent intent = new Intent(Intent.ACTION_VIEW); // We've set the ACTION | ||
+ | intent.setData(Uri.parse("https://ocw.cs.pub.ro/smd/lab2"); // We've set DATA | ||
+ | |||
+ | // We check before if an application that can support our intent exists. | ||
+ | // This blocks the app from crashing. We should inform the user about | ||
+ | // this or to disable this feature in our app. | ||
+ | if(intent.resolveActivity(getPackageManager())!=null) { | ||
+ | startActivity(intent); | ||
+ | } | ||
+ | </code> | ||
===== Explicit Intents ===== | ===== Explicit Intents ===== | ||
- | They specify the target component name, using the Java class name. Usually there are used inside applications. A good example is when an activity starts another activity as presented below: | + | They specify the target component name, using the Java class name. Usually they are used inside applications. A good example is when an activity starts another activity as presented below: |
<code> | <code> | ||
Line 38: | Line 64: | ||
**a) startActivity** | **a) startActivity** | ||
- | Create a new project and replace the content of the activity layout with **activity_main.xml** from the lab archive. Get a reference to **"Go to secondary activity"** button in the onCreate method of MainActivity. Add a new Activity to the project. You can automatically add a new activity by right clicking on the project and selecting **New > Activity > Empty Activity** from the menu. This will start a wizard similar to the one at application creation. | + | Create a new project and replace the content of the activity layout with **activity_main.xml** provided in the [[smd:laboratoare:02#resources|Resources]] section. You can use/make your own layout but make sure it allows you to test all the tasks in this lab. |
+ | |||
+ | Get a reference to **"Go to secondary activity"** button in the onCreate method of MainActivity. Add a new Activity to the project. You can automatically add a new activity by right clicking on the project and selecting **New > Activity > Empty Activity** from the menu. This will start a wizard similar to the one at application creation. | ||
Back in ''MainActivity'', add code to the onClick of the ''View.OnClickListener'' so that the new activity gets started. To start a new Activity you have to create an intent first which will **explicitly** take two parameters: a Context and the Class of the second activity. | Back in ''MainActivity'', add code to the onClick of the ''View.OnClickListener'' so that the new activity gets started. To start a new Activity you have to create an intent first which will **explicitly** take two parameters: a Context and the Class of the second activity. | ||
Line 87: | Line 115: | ||
**Action** | **Action** | ||
- | It is represented by a string that describes the action, for example: | + | It is represented by a string that describes the action, for example some defined in the [[https://developer.android.com/reference/android/content/Intent|Intent]] class: |
- | * **VIEW** - transmit data that can be shown to the user in the activity - phone, adress, file | + | * **VIEW** - transmit data that can be shown to the user in the activity - phone, address, file |
* **SEND** - usually named share intent, in which an app can share data with another | * **SEND** - usually named share intent, in which an app can share data with another | ||
* **DIAL** - dial a number specified in the data | * **DIAL** - dial a number specified in the data | ||
- | * **CALL** - perform a call | + | * **CALL** - directly perform a call |
- | * **MAIN** - start as the main entry point, no data is needed | + | |
- | * **PICK** for activities - to pick an item from the data an return it | + | You can have actions defined in other classes, such as [[https://developer.android.com/reference/android/provider/AlarmClock|AlarmClock]] with actions for managing the alarms |
- | * **BOOT_COMPLETED** - used after the phone has finished booting | + | |
- | * **SHUTDOWN** - the device has shutdown | + | <code Java> |
- | * **PACKAGE_ADDED** for broadcast receivers - a new package has been installed on the device | + | Intent intent = new Intent(Intent.ACTION_SENDTO); // setting the action |
+ | </code> | ||
You can also add custom actions. They respect the same rules by adding the application package name before the name of the action: | You can also add custom actions. They respect the same rules by adding the application package name before the name of the action: | ||
<code> | <code> | ||
- | static final String ACTION_START_ENGINE = "com.smd.lab2.START_ENGINE;" | + | static final String ACTION_START_ENGINE = "com.smd.lab2.START_ENGINE"; |
</code> | </code> | ||
**Data** | **Data** | ||
- | Based on the specified action the data field can take different values. In general it is an URI and/or MIME type of the data. The URI si a reference to the data on which the action will be done. If the URIs are similar and the MIME type can not be inferred it should be specified. | + | Based on the specified action the data field can take different values. In general it is an URI and/or [[https://wikipedia.org/wiki/MIME|MIME]] type of the data. The URI is a reference to the data on which the action will be done. If the URIs are similar and the MIME type can not be inferred it should be specified. |
+ | |||
+ | |||
+ | <code Java> | ||
+ | Intent intent = new Intent(Intent.ACTION_SENDTO); // setting the action | ||
+ | intent.setData(Uri.parse("smsto:")); // setting the data (this uri can also be specified as second param in the constructor) | ||
+ | </code> | ||
These are some standard data fields used in an Intent: | These are some standard data fields used in an Intent: | ||
Line 120: | Line 156: | ||
<note>If you want that one of your application components to accept implicit intents, you need to add the **DEFAULT** category. Without it the system will not accept that component as handler for an Intent even if it can handle it.</note> | <note>If you want that one of your application components to accept implicit intents, you need to add the **DEFAULT** category. Without it the system will not accept that component as handler for an Intent even if it can handle it.</note> | ||
- | These properties are enough for defining an Intent. Using them the system is able to determine which application component to start. Besides that, an Intent can contain additional information. | + | <code Java> |
+ | intent.addCategory(Intent.CATEGORY_APP_EMAIL); // add the category for the activity to be able to send or receive emails | ||
+ | </code> | ||
**Extras** | **Extras** | ||
Line 126: | Line 164: | ||
Extras are //key-value// pairs which contain additional information needed to finish the specified action. Extra data is added through the **putExtra()** method, using key value pairs or by adding a Bundle object. The Intent class contains **EXTRA*** constants key for different data types. As in the example with custom actions you can create custom extras, using the same method of having the application package as a prefix. | Extras are //key-value// pairs which contain additional information needed to finish the specified action. Extra data is added through the **putExtra()** method, using key value pairs or by adding a Bundle object. The Intent class contains **EXTRA*** constants key for different data types. As in the example with custom actions you can create custom extras, using the same method of having the application package as a prefix. | ||
- | <code> | + | <code Java> |
- | static final String EXTRA_ANDROIDS = "com.smd.lab2.EXTRA_ANDROIDS"; | + | intent.putExtra("address", new String ("123456789")); // add extra for the phone number |
+ | intent.putExtra("sms_body","Important message.."); // add extra for the sms content | ||
</code> | </code> | ||
- | **Flags** | ||
- | These are needed for specifying metadata needed by the Android system. Check the [[https://developer.android.com/reference/android/content/Intent.html#setFlags(int)|setFlags()]] method. | + | ==== App chooser ==== |
- | === Example explicit intent === | + | Sometimes you want to let the user select the desired handler app every time. This can be done by forcing a chooser dialog. This will not override the default option but in our app the user will have to choose every time. This is most used when using the share action. As an example think about a user who wants to send a photo through another app of his choice. In this case we need to supply him a chooser dialog every time, considering the fact that he can use different apps for different cases. |
<code> | <code> | ||
- | Intent intent = new Intent( | + | Intent intent = new Intent(Intent.ACTION_SEND); |
- | context, // Application context | + | |
- | Activity.class); // Component name | + | |
- | startActivity(intent); // Launch component | + | |
- | </code> | + | |
- | + | ||
- | === Example implicit intent === | + | |
- | + | ||
- | <code> | + | |
- | Intent intent = new Intent(Intent.ACTION_VIEW); // We've set the ACTION | + | |
- | setData(Uri.parse("https://ocw.cs.pub.ro/smd/lab2"); // We've set DATA | + | |
- | + | ||
- | // We check before if there exists an application that can support our intent. | + | |
- | // This blocks the app from crashing. We should inform the user about | + | |
- | // this or to disable this feature in our app. | + | |
- | if(intent.resolveActivity(getPackageManager())!=null) { | + | |
- | startActivity(intent); | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | ==== App choser ==== | + | |
- | + | ||
- | Sometimes you want to let the user select the desired handler app every time. This can be done by forcing a chooser dialog. This will not override the default option but in our app the user will have to choose every time. This is most used when using the share action. As an example think about the user wants to send a photo through another app of his choice in this case we need to supply him a chooser dialog every time, considering the fact that he can use different apps for different cases. | + | |
- | + | ||
- | <code> | + | |
- | Intent = new Intent(Intent.ACTION_SEND); | + | |
String chooserTitle = "Share photo with"; | String chooserTitle = "Share photo with"; | ||
Intent intentChooser = Intent.createChooser(intent, chooserTitle); | Intent intentChooser = Intent.createChooser(intent, chooserTitle); | ||
Line 169: | Line 182: | ||
startActivity(intentChooser); | startActivity(intentChooser); | ||
} | } | ||
+ | |||
</code> | </code> | ||
+ | <note important> Your app will crash if there is no app on device that can receive the implicit intents it sends. Make sure to check if any app is available like in the above example.</note> | ||
==== Task 2 - Go to a web page (1p) ==== | ==== Task 2 - Go to a web page (1p) ==== | ||
Line 178: | Line 193: | ||
<code> | <code> | ||
Intent intent = new Intent(Intent.ACTION_VIEW); | Intent intent = new Intent(Intent.ACTION_VIEW); | ||
- | intent.setData(Uri.parse("https://www.webpage.com"); | + | intent.setData(Uri.parse("https://www.webpage.com")); |
startActivity(intent); | startActivity(intent); | ||
</code> | </code> | ||
Line 188: | Line 203: | ||
<code> | <code> | ||
Intent intent = new Intent(Intent.ACTION_DIAL); | Intent intent = new Intent(Intent.ACTION_DIAL); | ||
- | intent.setData(Uri.parse("tel:"+"enter the phonenumber"); | + | intent.setData(Uri.parse("tel:"+"enter the phonenumber")); |
startActivity(intent); | startActivity(intent); | ||
</code> | </code> | ||
Line 196: | Line 211: | ||
==== Task 4 - Send an email (2p) ==== | ==== Task 4 - Send an email (2p) ==== | ||
- | <note> For this task you need to have configured in your emulators email client an email address or to run the app on a phone which has been already configured</note> | + | <note> For this task you need to have configured in your emulator's email client an email address or to run the app on a phone which has been already configured</note> |
Now we are going to send an email. Edit the fields below with the receiver email, title and subject. The text of the email will be from the MainActivity EditText. Get a reference to "Send email" button and add a click listener as in the previous tasks. | Now we are going to send an email. Edit the fields below with the receiver email, title and subject. The text of the email will be from the MainActivity EditText. Get a reference to "Send email" button and add a click listener as in the previous tasks. | ||
Line 219: | Line 234: | ||
==== Task 5 - Get data from an activity (2p) ==== | ==== Task 5 - Get data from an activity (2p) ==== | ||
- | Usually activities are not started to only receive data they can also be started for returning data. Create a third activity in the project and add an EditText and a Button. In MainActivity, get a reference to "Get data from activity" button which will start the third activity. We will use [[https://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent,%20int)|startActivityForResult()]] in order to start the activity. | + | In this task we start an activity and and also receive data from it. |
+ | |||
+ | Create a **third activity** in the project and add an EditText and a Button. In ''MainActivity'', get a reference to //"Get data from activity"// button which will start the third activity. We will use [[https://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent,%20int)|startActivityForResult()]] in order to start the activity. | ||
<code> | <code> | ||
Line 226: | Line 243: | ||
</code> | </code> | ||
- | Now we need to add some code that handles the Intent that will be received from the started activity. For this we need to override onActivityResult, which is called when the started activity is finished. You can see that we used a **request code**. This is used in order to differentiate between multiple requests code used for multiple activities. Besides the request code we have **resultCode** which identifies if the started activity acted as we wanted, by setting **RESULT_OK** before finishing. | + | Now we need to add some code that handles the Intent that will be received from the started activity. For this we need to override //onActivityResult//, which is called when the started activity is finished. You can see that we used a **request code**. This is used in order to differentiate between multiple requests code used for multiple activities. Besides the request code we have **resultCode** which identifies if the started activity acted as we wanted, by setting **RESULT_OK** before finishing. |
<code> | <code> | ||
Line 241: | Line 258: | ||
</code> | </code> | ||
- | In the third activity get a reference to the added Buttton and write an click listener in which we take the text from the EditText and send it to the activity that started it. We do this by adding the text into a simple Intent object and by setting the wanted result code. In order to close this activity will call **finish()**. | + | In the third activity get a reference to the added Button and write a click listener in which we take the text from the EditText and send it to the activity that started it. We do this by adding the text into a simple Intent object and by setting the wanted result code. In order to close this activity will call **finish()**. |
<code> | <code> | ||
Line 263: | Line 280: | ||
* **<category>** - intent category, represented by a string value. | * **<category>** - intent category, represented by a string value. | ||
- | Example intent filter: | + | **Example:** |
<code> | <code> | ||
- | <activity android:name=”EditPhotoActivity”> | + | <activity android:name="EditPhotoActivity"> |
<intent-filter> | <intent-filter> | ||
- | <action android:name=”android.intent.action.EDIT”/> | + | <action android:name="android.intent.action.EDIT"/> |
- | <category android:name=”android.intent.category.DEFAULT”/> | + | <category android:name="android.intent.category.DEFAULT"/> |
- | <data android:mimeType=”image”/> | + | <data android:mimeType="image"/> |
</intent-filter> | </intent-filter> | ||
</activity> | </activity> | ||
</code> | </code> | ||
- | Multiple action, category, data can be used in an intent filter or multiple intent filters can be defined. For both cases, the corresponding component must be checked if it can handle all of the defined intents. | + | Multiple actions, categories and data can be used in an intent filter and an activity can contain multiple intent filters. |
- | <note warning>As a security feature to do not declare intent-filters for components used just inside the app. You should use explicit intent for those. Intent-filters can be used by another app if it determines the exact name of the component. Then it can use explicit intents to start it.</note> | ||
- | If we need to define an intent filter for just a period of time for a broadcast receiver, we can do it by registering it dynamically. **registerReceiver()** and **unregisterReceiver()** are used for registering and unregistering. | + | <note warning>For security reasons you should avoid declaring intent-filters for components used just inside the app. You should use explicit intent for actions internal to your app. The reason is that intent-filters need to be declared in the manifest, therefore visible to anyone. A malicious app can then start your exported components (obtaining their names is not that difficult) using explicit intents</note> |
+ | |||
+ | If we need to define an intent filter for just a period of time for a broadcast receiver, we can do it by **registering it dynamically**. | ||
===== Broadcast Receivers ===== | ===== Broadcast Receivers ===== | ||
Line 294: | Line 312: | ||
<code> | <code> | ||
<receiver android:name=".MyReceiver" | <receiver android:name=".MyReceiver" | ||
- | android:exported="true/false" // accesible/not accesible by other applications with different used id | + | android:exported="true/false" // accessible/not accessible by other applications with different used id |
android:enabled="true/false" /> // can/cannot be registered by the system | android:enabled="true/false" /> // can/cannot be registered by the system | ||
</code> | </code> | ||
Line 300: | Line 318: | ||
A [[https://developer.android.com/guide/components/broadcasts#context-registered-receivers|Context broadcast receiver]] is a receiver which is dynamically registered. In this case the receiver will live as long as the Activity is not destroyed or the receiver is unregistered. Also we can use the Application context, in this case the receiver will live as long our application is not destroyed. | A [[https://developer.android.com/guide/components/broadcasts#context-registered-receivers|Context broadcast receiver]] is a receiver which is dynamically registered. In this case the receiver will live as long as the Activity is not destroyed or the receiver is unregistered. Also we can use the Application context, in this case the receiver will live as long our application is not destroyed. | ||
- | If you define a Broadcast Receiver dynamically you have to pass to the registerReceiver() two objects: a receiver and an IntentFilter. The only condition is that we can not use an empty IntentFilter: | + | If you define a Broadcast Receiver dynamically you have to pass to the registerReceiver() two objects: a receiver and an IntentFilter. The only condition is that //we can not use an empty IntentFilter//: |
<code> | <code> | ||
Line 331: | Line 349: | ||
</code> | </code> | ||
- | The difference is that the intent is sent and received only inside our app. When using Context.registerReceiver() or ApplicationContext.registerReceiver() we can receive broadcasts from outside the app as the receiver is globally registered, no matter if that is only for the lifetime of the activity or the app. By using this we avoid security concerns like other apps sending broadcasts to our receiver or other apps defining a receiver with the same action in which case when we send the broadcast intent it will be received by both receivers. | + | The difference is that the intent is sent and received only inside our app. When using Context.registerReceiver() or ApplicationContext.registerReceiver() we can receive broadcasts from outside the app as the receiver is globally registered, no matter if that is only for the lifetime of the activity or the app. **By using this we avoid security concerns like other apps sending broadcasts to our receiver or other apps defining a receiver with the same action in which case when we send the broadcast intent it will be received by both receivers.** |
For all the types of broadcast receivers we need to create a class, for our example MyReceiver, and extend the BroadcastReceiver abstract class. Inside it we need to override the onReceive() method. This will be called when we receive the broadcast intent. | For all the types of broadcast receivers we need to create a class, for our example MyReceiver, and extend the BroadcastReceiver abstract class. Inside it we need to override the onReceive() method. This will be called when we receive the broadcast intent. | ||
Line 350: | Line 368: | ||
==== Task 6 - Broadcast receivers (2p) ==== | ==== Task 6 - Broadcast receivers (2p) ==== | ||
- | Add a Broadcast Receiver to the project **New > Other > Broadcast Receiver** and name it MyReceiver.java. We will want to use this receiver to pop a simple message on the screen when clicking on the **Send text to receiver** button. The message will contain the text inserted in the EditText. | + | Add a Broadcast Receiver to the project **New > Other > Broadcast Receiver** and name it ''MyReceiver''. We will want to use this receiver to pop a simple message on the screen when clicking on the ''Send text to receiver'' button. The message will contain the text inserted in the EditText. |
- | In order to achieve this, create a click listener with an Intent containing the context object and the receiver's class name. Add the text using //putExtra//. Afterwards, use //sendBroadcast// to send the intent to the BroadcastReceiver. | + | In order to achieve this, create a ClickListener with an Intent containing the context object and the receiver's class name. Add the text using ''putExtra''. Afterwards, use ''sendBroadcast'' to send the intent to the BroadcastReceiver. |
<code> | <code> | ||
Line 366: | Line 384: | ||
</code> | </code> | ||
- | Inside the receiver, in the //onReceive// method, retrieve the String added to the intent and display it using a toast notification. | + | Inside the receiver, in the ''onReceive'' method, retrieve the String added to the intent and display it using a toast notification. |
<code> | <code> | ||
Line 374: | Line 392: | ||
<note important>Do not use broadcast receivers to maintain state or to be interacted with. It "lives" only until the //onReceive// method finishes. Also, do not forget to register and unregister your broadcast receiver in the activity's lifecycle callbacks, e.g. in onPause and onResume.</note> | <note important>Do not use broadcast receivers to maintain state or to be interacted with. It "lives" only until the //onReceive// method finishes. Also, do not forget to register and unregister your broadcast receiver in the activity's lifecycle callbacks, e.g. in onPause and onResume.</note> | ||
+ | ===== Summary ===== | ||
+ | |||
+ | * Intents facilitate communication between android components (activities, services etc) | ||
+ | * Two types of intents: | ||
+ | * explicit: contains the destination | ||
+ | * implicit: sent to the system and the system find an application that can handle the action. If it doesn't, the app crashes. | ||
+ | * Intent filters: | ||
+ | * define which intents a component can handle (the component can be an activity) | ||
+ | * Broadcast receivers: | ||
+ | * Android components that can receive messages under the form of intents | ||
+ | * Can receive system broadcasts (e.g. bluetooth connectivity updates) | ||
+ | * Can receive broadcasts from other apps or from inside the same app | ||
+ | * Can be dynamically registered and unregistered or declared in the manifest | ||
+ | |||
+ | |||
+ | <note warning> | ||
+ | **Security concerns summary** | ||
+ | * Make sure to check your manifest for any components marked as exported by default when you created them. This flag tells the system that the component can be invoked by other apps (see [[https://developer.android.com/training/articles/security-tips#BroadcastReceivers|info here]], exploit example on broadcast receivers [[https://resources.infosecinstitute.com/topic/android-hacking-security-part-3-exploiting-broadcast-receivers/|here]]) | ||
+ | * If your broadcast only targets your app, then declare the receivers locally (see LocalBroadcastManager) | ||
+ | * There are limitations to what implicit broadcasts are supported for manifest-declared broadcast receivers (see [[https://developer.android.com/guide/components/broadcasts#manifest-declared-receivers|info here]]) | ||
+ | </note> | ||
+ | ===== Links ===== | ||
+ | * [[https://developer.android.com/codelabs/basic-android-kotlin-training-activities-intents?hl=en&continue=https%3A%2F%2Fcodelabs.developers.google.com%2F%3Fcat%3Dandroid|Activity and Intents Codelab]] - easy step by step tutorial to learn about intents | ||
+ | * [[https://developer.android.com/guide/components/broadcasts#security-and-best-practices|Security considerations about Broadcast Receivers]] | ||
+ | * [[https://developer.android.com/guide/components/intents-filters|Intents documentation and security warnings]] | ||
===== Resources ===== | ===== Resources ===== | ||
- | {{:smd:laboratoare:lab2.zip}} | + | Layout: |
+ | * {{:smd:laboratoare:lab2.zip}} | ||
+ | * [[https://github.com/SMD-UPB/labs/tree/main/lab2|Available in the Github repo]] | ||