Test practic 1

Exemplu de colocviu rezolvat: https://ocw.cs.pub.ro/courses/eim/colocvii/colocviu01


Cerințe pentru colocviul 1

  • un cont GitHub personal;
  • Android SDK (imagine pentru API 36);
  • Android Studio;
  • un dispozitiv mobil:
    • fizic (bring your own device); sau
    • virtual: Genymotion, AVD.

Rezolvări

Proiectul Android Studio cu rezolvările complete este disponibil pe contul de GitHub al disciplinei.

A.1

a) Crearea depozitului GitHub

  1. Autentifică-te în GitHub (Sign in).
  2. Creează un proiect nou (New Repository).
  3. Configurează repository-ul:
    • denumire;
    • tip: public;
    • modul de inițializare:
      • local, cu git init; sau
      • la distanță, cu git clone (depozitul la distanță nu trebuie să fie vid în această situație);
    • fișier README cu numele studentului;
    • .gitignore (Android);

b) Clonarea depozitului

git clone https://www.github.com/perfectstudent/PracticalTest01

Poți specifica un director țintă după URL.

c) Crearea aplicației Android

Urmează indicațiile din secțiunea Crearea unei aplicații Android în Android Studio.


A.2 Interfața grafică (layout)

Fișier XML în /res/layout.

Variante de implementare

1) Un singur RelativeLayout

Aliniere orizontală: android:layout_alignLeft / android:layout_alignRight Aliniere verticală: android:layout_alignTop / android:layout_alignBottom

<!-- activity_practical_test01_main.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  tools:context="ro.pub.cs.systems.eim.practicaltest01.PracticalTest01MainActivity">

  <Button
    android:id="@+id/navigate_to_secondary_activity_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:text="@string/navigate_to_secondary_activity" />

  <EditText
    android:id="@+id/left_edit_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:saveEnabled="false"
    android:enabled="false"
    android:inputType="number"
    android:ems="7"
    android:layout_alignParentLeft="true"
    android:layout_below="@id/navigate_to_secondary_activity_button"
    android:gravity="center" />

  <EditText
    android:id="@+id/right_edit_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:saveEnabled="false"
    android:enabled="false"
    android:inputType="number"
    android:ems="7"
    android:layout_alignParentRight="true"
    android:layout_below="@id/navigate_to_secondary_activity_button"
    android:gravity="center" />

  <Button
    android:id="@+id/left_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@id/left_edit_text"
    android:layout_below="@id/left_edit_text"
    android:layout_alignRight="@id/left_edit_text"
    android:text="@string/press_me" />

  <Button
    android:id="@+id/right_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@id/right_edit_text"
    android:layout_alignRight="@id/right_edit_text"
    android:layout_below="@id/right_edit_text"
    android:text="@string/press_me_too" />

</RelativeLayout>

2) LinearLayout-uri îmbricate

Pentru proporții: android:layout_weight (setează layout_width sau layout_height la 0dp pe direcția dorită)

<!-- activity_practical_test01_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical"
  tools:context="ro.pub.cs.systems.eim.practicaltest01.PracticalTest01MainActivity">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <EditText
      android:id="@+id/left_edit_text"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:enabled="false"
      android:inputType="number"
      android:gravity="center" />

    <EditText
      android:id="@+id/right_edit_text"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:enabled="false"
      android:inputType="number"
      android:gravity="center" />

  </LinearLayout>

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:baselineAligned="false">

    <ScrollView
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight="1">

      <Button
        android:id="@+id/left_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/press_me" />
    </ScrollView>

    <ScrollView
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight="1">
      <Button
        android:id="@+id/right_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/press_me_too" />
    </ScrollView>

  </LinearLayout>

</LinearLayout>

3) LinearLayout + GridLayout (2×2)

<!-- activity_practical_test01_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingBottom="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:orientation="vertical"
  tools:context="ro.pub.cs.systems.eim.practicaltest01.PracticalTest01MainActivity">

  <Button
    android:id="@+id/navigate_to_secondary_activity_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/navigate_to_secondary_activity"/>

  <GridLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:rowCount="2"
    android:columnCount="2">

    <EditText
      android:id="@+id/left_edit_text"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:saveEnabled="false"
      android:enabled="false"
      android:inputType="number"
      android:ems="6"
      android:layout_row="0"
      android:layout_column="0"
      android:layout_gravity="center"
      android:gravity="center"/>

    <EditText
      android:id="@+id/right_edit_text"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:saveEnabled="false"
      android:enabled="false"
      android:inputType="number"
      android:ems="6"
      android:layout_row="0"
      android:layout_column="1"
      android:layout_gravity="center"
      android:gravity="center" />

    <Button
      android:id="@+id/left_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_row="1"
      android:layout_column="0"
      android:layout_gravity="center"
      android:text="@string/press_me" />

    <Button
      android:id="@+id/right_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_row="1"
      android:layout_column="1"
      android:layout_gravity="center"
      android:text="@string/press_me_too" />

  </GridLayout>

</LinearLayout>

Încărcarea layout-ului în activitate

// PracticalTest01MainActivity.java
public class PracticalTest01MainActivity extends AppCompatActivity {

  private EditText leftEditText;
  private EditText rightEditText;
  private Button pressMeButton, pressMeTooButton;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_practical_test01_main);

    leftEditText  = findViewById(R.id.left_edit_text);
    rightEditText = findViewById(R.id.right_edit_text);

    pressMeButton     = findViewById(R.id.press_me_button);
    pressMeTooButton  = findViewById(R.id.press_me_too_button);

    leftEditText.setText(String.valueOf(0));
    rightEditText.setText(String.valueOf(0));
  }
}

În Android Studio, auto-import: Alt+Enter.


B.1 Tratarea evenimentelor UI

  • definește o clasă ascultător (OnClickListener);
  • înregistrează instanța pe controalele grafice.
public class PracticalTest01MainActivity extends AppCompatActivity {

  private ButtonClickListener buttonClickListener = new ButtonClickListener();
  private class ButtonClickListener implements View.OnClickListener {
    @Override
    public void onClick(View view) {
      int leftNumberOfClicks = Integer.valueOf(leftEditText.getText().toString());
      int rightNumberOfClicks = Integer.valueOf(rightEditText.getText().toString());

      switch(view.getId()) {
        case R.id.press_me_button:
          leftNumberOfClicks++;
          leftEditText.setText(String.valueOf(leftNumberOfClicks));
          break;
        case R.id.press_me_too_button:
          rightNumberOfClicks++;
          rightEditText.setText(String.valueOf(rightNumberOfClicks));
          break;
      }
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ...
    pressMeButton.setOnClickListener(buttonClickListener);
    pressMeTooButton.setOnClickListener(buttonClickListener);
  }
}

B.2 Salvarea/restaurarea stării

a) Dezactivarea auto-save pe controale

<LinearLayout ... >
  <EditText
    android:id="@+id/left_edit_text"
    android:saveEnabled="false"
    ... />
  <EditText
    android:id="@+id/right_edit_text"
    android:saveEnabled="false"
    ... />
</LinearLayout>

b) onSaveInstanceState / onRestoreInstanceState

public class PracticalTest01MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ...

    if (savedInstanceState != null) {
      if (savedInstanceState.containsKey(Constants.LEFT_COUNT)) {
        leftEditText.setText(savedInstanceState.getString(Constants.LEFT_COUNT));
      } else {
        leftEditText.setText(String.valueOf(0));
      }
      if (savedInstanceState.containsKey(Constants.RIGHT_COUNT)) {
        rightEditText.setText(savedInstanceState.getString(Constants.RIGHT_COUNT));
      } else {
        rightEditText.setText(String.valueOf(0));
      }
    } else {
      leftEditText.setText(String.valueOf(0));
      rightEditText.setText(String.valueOf(0));
    }
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    outState.putString(Constants.LEFT_COUNT, leftEditText.getText().toString());
    outState.putString(Constants.RIGHT_COUNT, rightEditText.getText().toString());
  }

  @Override
  protected void onRestoreInstanceState(Bundle savedInstanceState) {
    if (savedInstanceState.containsKey(Constants.LEFT_COUNT)) {
      leftEditText.setText(savedInstanceState.getString(Constants.LEFT_COUNT));
    } else {
      leftEditText.setText(String.valueOf(0));
    }
    if (savedInstanceState.containsKey(Constants.RIGHT_COUNT)) {
      rightEditText.setText(savedInstanceState.getString(Constants.RIGHT_COUNT));
    } else {
      rightEditText.setText(String.valueOf(0));
    }
  }
}

c) Simulare distrugere/relansare activitate

  1. Părăsește activitatea (Menu) → onPause() / onStop()onSaveInstanceState().
  2. Terminate Application din Android Monitor.
  3. Redeschide activitatea din meniu → onCreate()onStart()onResume()onRestoreInstanceState().

C.1 Definirea unei activități

  • în AndroidManifest.xml (etichetă <activity> + <intent-filter>);
  • layout XML în /res/layout;
  • clasă Java derivată din Activity sau AppCompatActivity.

Manifest

<!-- AndroidManifest.xml -->
<manifest ... >
  <application ... >
    <activity android:name=".view.PracticalTest01SecondaryActivity">
      <intent-filter>
        <action android:name="ro.pub.cs.systems.eim.practicaltest01.intent.action.PracticalTest01SecondaryActivity" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
    </activity>
  </application>
</manifest>

Layout secundar

<!-- activity_practical_test01_secondary.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context="ro.pub.cs.systems.eim.practicaltest01.PracticalTest01SecondaryActivity">

  <TextView
    android:id="@+id/number_of_clicks_text_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center" />

  <LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center">

    <Button
      android:id="@+id/ok_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/ok" />

    <Button
      android:id="@+id/cancel_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/cancel" />
  </LinearLayout>

</LinearLayout>

Cod activitate secundară

// PracticalTest01SecondaryActivity.java
public class PracticalTest01SecondaryActivity extends AppCompatActivity {

  private TextView numberOfClicksTextView;
  private Button okButton, cancelButton;

  private ButtonClickListener buttonClickListener = new ButtonClickListener();
  private class ButtonClickListener implements View.OnClickListener {
    @Override
    public void onClick(View view) {
      switch (view.getId()) {
        case R.id.ok_button:
          setResult(RESULT_OK, null);
          break;
        case R.id.cancel_button:
          setResult(RESULT_CANCELED, null);
          break;
      }
      finish();
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_practical_test01_secondary);

    numberOfClicksTextView = findViewById(R.id.number_of_clicks_text_view);
    Intent intent = getIntent();
    if (intent != null && intent.getExtras().containsKey(Constants.NUMBER_OF_CLICKS)) {
      int numberOfClicks = intent.getIntExtra(Constants.NUMBER_OF_CLICKS, -1);
      numberOfClicksTextView.setText(String.valueOf(numberOfClicks));
    }

    okButton = findViewById(R.id.ok_button);
    okButton.setOnClickListener(buttonClickListener);
    cancelButton = findViewById(R.id.cancel_button);
    cancelButton.setOnClickListener(buttonClickListener);
  }
}

C.2 Navigarea către activitatea secundară

Buton în layout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical"
  tools:context="ro.pub.cs.systems.eim.practicaltest01.PracticalTest01MainActivity">

  <Button
    android:id="@+id/navigate_to_secondary_activity_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="right"
    android:text="@string/navigate_to_secondary_activity" />

</LinearLayout>

Cod în activitatea principală

public class PracticalTest01MainActivity extends AppCompatActivity {

  private Button navigateToSecondaryActivityButton;

  private ButtonClickListener buttonClickListener = new ButtonClickListener();
  private class ButtonClickListener implements View.OnClickListener {
    @Override
    public void onClick(View view) {
      switch(view.getId()) {
        case R.id.navigate_to_secondary_activity_button:
          Intent intent = new Intent(getApplicationContext(), PracticalTest01SecondaryActivity.class);
          int numberOfClicks = Integer.parseInt(leftEditText.getText().toString()) +
                               Integer.parseInt(rightEditText.getText().toString());
          intent.putExtra(Constants.NUMBER_OF_CLICKS, numberOfClicks);
          startActivityForResult(intent, Constants.SECONDARY_ACTIVITY_REQUEST_CODE);
          break;
      }
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ...
    navigateToSecondaryActivityButton = findViewById(R.id.navigate_to_secondary_activity_button);
    navigateToSecondaryActivityButton.setOnClickListener(buttonClickListener);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    if (requestCode == Constants.SECONDARY_ACTIVITY_REQUEST_CODE) {
      Toast.makeText(this, "The activity returned with result " + resultCode, Toast.LENGTH_LONG).show();
    }
  }
}

D.1 Serviciu Android (started service)

Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
  <application ...>
    <!-- other components -->
    <service
      android:name=".service.PracticalTest01Service"
      android:enabled="true"
      android:exported="false" />
  </application>
</manifest>

Implementare serviciu

// PracticalTest01Service.java
public class PracticalTest01Service extends Service {

  private ProcessingThread processingThread = null;

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    int firstNumber  = intent.getIntExtra(Constants.FIRST_NUMBER, -1);
    int secondNumber = intent.getIntExtra(Constants.SECOND_NUMBER, -1);
    processingThread = new ProcessingThread(this, firstNumber, secondNumber);
    processingThread.start();
    return Service.START_REDELIVER_INTENT;
  }

  @Override
  public IBinder onBind(Intent intent) { return null; }

  @Override
  public void onDestroy() {
    processingThread.stopThread();
  }
}

Thread de procesare

// ProcessingThread.java
public class ProcessingThread extends Thread {

  private Context context = null;
  private boolean isRunning = true;

  private Random random = new Random();

  private double arithmeticMean;
  private double geometricMean;

  public ProcessingThread(Context context, int firstNumber, int secondNumber) {
    this.context = context;
    arithmeticMean = (firstNumber + secondNumber) / 2.0;
    geometricMean  = Math.sqrt(firstNumber * secondNumber);
  }

  @Override
  public void run() {
    Log.d(Constants.PROCESSING_THREAD_TAG, "Thread has started! PID: " + Process.myPid() + " TID: " + Process.myTid());
    while (isRunning) {
      sendMessage();
      sleep10s();
    }
    Log.d(Constants.PROCESSING_THREAD_TAG, "Thread has stopped!");
  }

  private void sendMessage() {
    Intent intent = new Intent();
    intent.setAction(Constants.actionTypes[random.nextInt(Constants.actionTypes.length)]);
    intent.putExtra(Constants.BROADCAST_RECEIVER_EXTRA,
        new Date(System.currentTimeMillis()) + " " + arithmeticMean + " " + geometricMean);
    context.sendBroadcast(intent);
  }

  private void sleep10s() {
    try { Thread.sleep(10000); }
    catch (InterruptedException e) { e.printStackTrace(); }
  }

  public void stopThread() { isRunning = false; }
}

Pornire/Oprire serviciu din activitate

private class ButtonClickListener implements View.OnClickListener {
  @Override
  public void onClick(View view) {
    int leftNumberOfClicks  = Integer.parseInt(leftEditText.getText().toString());
    int rightNumberOfClicks = Integer.parseInt(rightEditText.getText().toString());

    if (leftNumberOfClicks + rightNumberOfClicks > Constants.NUMBER_OF_CLICKS_THRESHOLD
        && serviceStatus == Constants.SERVICE_STOPPED) {
      Intent intent = new Intent(getApplicationContext(), PracticalTest01Service.class);
      intent.putExtra(Constants.FIRST_NUMBER, leftNumberOfClicks);
      intent.putExtra(Constants.SECOND_NUMBER, rightNumberOfClicks);
      getApplicationContext().startService(intent);
      serviceStatus = Constants.SERVICE_STARTED;
    }
  }
}

@Override
protected void onDestroy() {
  Intent intent = new Intent(this, PracticalTest01Service.class);
  stopService(intent);
  super.onDestroy();
}

D.2 BroadcastReceiver

Receiver + filtrare

private MessageBroadcastReceiver messageBroadcastReceiver = new MessageBroadcastReceiver();

private class MessageBroadcastReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
     Log.d(Constants.BROADCAST_RECEIVER_TAG,
           intent.getStringExtra(Constants.BROADCAST_RECEIVER_EXTRA));
  }
}

private IntentFilter intentFilter = new IntentFilter();

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_practical_test01_main);

  for (int i = 0; i < Constants.actionTypes.length; i++) {
    intentFilter.addAction(Constants.actionTypes[i]);
  }
}

@Override
protected void onResume() {
  super.onResume();
  registerReceiver(messageBroadcastReceiver, intentFilter);
}

@Override
protected void onPause() {
  unregisterReceiver(messageBroadcastReceiver);
  super.onPause();
}

D.3 Comenzi Git utile

# adaugă toate modificările
git add *

# commit cu mesaj
git commit -m "finished tasks for PracticalTest01"

# push pe master
git push origin master

Configurare utilizator (dacă e necesar):

git config --global user.name "Perfect Student"
git config --global user.email perfectstudent@cs.pub.ro