Desarrollo de Aplicación Móvil en Android Studio – RepoBit

En este laboratorio vamos a desarrollar una aplicación móvil empleando diferentes recursos y conceptos expuestos en clase.

Nombraremos a la aplicación RepoBit y en la presente guía se detallaremos el procedimiento paso a paso de su construcción.

1. Estructura

2. Construir Proyecto

Recordar apunta su proyecto a una carpeta previamente creada.

Al iniciar un nuevo proyecto Android Studio iniciará la construcción básica para una estructura de desarrollo limpia, ser paciente en algunos casos descargará dependencias y recursos necesarios.

3. Crear Layout y XML

Al tener lista la estructura base construida por Android Studio iniciaremos con la adecuación a nuestra estructura propuesta

4. Crear assets

Con ello tendríamos completada nuestra estructura

5. Core de la App

La aplicación que vamos a construir consiste en un minirepositorio de comandos y scripts típicamente usados en los diferentes laboratorios realizados, así mismo cuenta con herramientas de Subneteo y conversores binario-decimal-hexadecimal.

Este proyecto usa y amplía las prestaciones de nuestro proyecto Calculadora de Sub Redes.

Los comandos se encuentran en un archivo comandos.js debidamente alojado como CDN en Git hub

Versiones y SDK

  • Android Gradle Plugin: 8.13.0
  • Kotlin: 2.0.21
  • Compile SDK: 36
  • Target SDK: 36
  • Min SDK: 23 (Android 6.0)
  • Java Version: 17

Detalle de funciones:

Calculadora de Subredes FLSM (Fixed Length Subnet Masking)
  • Cálculo de subredes de tamaño fijo
  • Entrada de dirección de red en formato CIDR
  • Selección dinámica del número de subredes
  • Generación automática de tablas con:
  • Dirección de red
  • Dirección de broadcast
  • Máscara de subred
Calculadora de Subredes VLSM (Variable Length Subnet Masking)
  • Cálculo de subredes de tamaño variable
  • Asignación optimizada por requerimientos de hosts
  • Ordenamiento automático (mayor a menor)
  • Tabla detallada con hosts usables
Conversores Numéricos
  • Binario ↔ Decimal ↔ Hexadecimal
  • Validación de entrada binaria
  • Contador de bits en tiempo real
  • Conversión bidireccional
Buscador de Comandos
  • Búsqueda dinámica de comandos de múltiples tecnologías:
  • Cisco
  • Linux
  • Windows
  • Git
  • SQL
  • Docker
  • Python
  • ADB
  • Nmap
  • Bash
  • Arduino
  • Seguridad
  • Backend
Características Adicionales
  • Modo oscuro/claro con toggle
  • Detección de IP pública automática
  • Diseño responsivo para móviles
  • Interfaz moderna con Bootstrap 4

6. AndroidManifest.xml

No hay mucho que tocar aqui, solo debemos asegurar el permiso a Internet

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.RepoBit">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.RepoBit">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

7. MainActivity.kt

La actividad principal de nuestro proyecto, por defecto viene con un Hola Mundo, nosotros lo actualizaremos con el siguiente código:

package com.example.repobit

import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.util.Log
import androidx.activity.ComponentActivity

class MainActivity : ComponentActivity() {
    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val webView = WebView(this)
        setContentView(webView)

        val settings = webView.settings
        settings.javaScriptEnabled = true
        settings.allowFileAccess = true
        settings.allowContentAccess = true

        webView.webViewClient = object : WebViewClient() {
            override fun onReceivedError(
                view: WebView?,
                request: WebResourceRequest?,
                error: WebResourceError?
            ) {
                Log.e("WebViewError", "Error: ${error?.description}")
            }
        }

        webView.loadUrl("file:///android_asset/index.html")
    }
}

8. activity_main.xml

Nuestra vista principal

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

9. index.html

Nuestra vista principal de recursos assets

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> Repobit | by @curowalther </title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="estilos.css">
</head>
<body>
<div class="container main-container">
    <!-- Cabecera -->
    <div class="header">
        <span id="public-ip">Mi IP: Cargando...</span>
        <h1> Repobit | by @curowalther </h1>
        <button id="theme-toggle" class="btn btn-dark-mode" onclick="modoOscuro()">🌙</button>
    </div>

    <!-- Buscador de comandos unificado -->
    <div class="section-container mt-4">
        <h5> Comandos / Scripts </h5>
        <input type="text" class="form-control" id="buscar-comando" placeholder="Escribe un comando..." oninput="buscarComandoDinamico()">
        <div class="alert alert-secondary mt-3" id="resultado-comando" style="display:none;"></div>
    </div>

    <!-- Contenedor de Calculadora de Subredes (FLSM) -->
    <div class="section-container">
        <h3 class="mb-3">FLSM (mismo tamaño)</h3>
        <div class="form-group">
            <label for="network">Dirección de Red (ejemplo. 192.168.1.0/24)</label>
            <input type="text" class="form-control" id="network" placeholder="Ingrese la dirección de red" oninput="desplegarSubnets()">
            <div id="subnet-error" class="text-danger"></div>
        </div>
        <div class="form-group">
            <label for="subnets">Número de Subredes</label>
            <select class="form-control" id="subnets">
                <option value="">Seleccione una opción</option>
            </select>
        </div>
        <button class="btn btn-calculate" onclick="calcularSubnets()">Calcular Subredes</button>
        <table class="table table-bordered mt-4">
            <thead>
            <tr>
                <th>Subred</th>
                <th>Dirección de Red</th>
                <th>Broadcast</th>
                <th>Máscara de Subred</th>
            </tr>
            </thead>
            <tbody id="resultados"></tbody>
        </table>
    </div>

    <!-- Contenedor de Calculadora de Subredes (VLSM) -->
    <div class="section-container mt-5">
        <h3 class="mb-3">VLSM (tamaños variables)</h3>
        <div class="form-group">
            <label for="vlsm-network">Dirección de Red base (ejemplo. 192.168.1.0/24)</label>
            <input type="text" class="form-control" id="vlsm-network" placeholder="Ingrese la red base en CIDR">
        </div>
        <div class="form-group">
            <label for="vlsm-hosts">Requerimientos de hosts por subred</label>
            <textarea id="vlsm-hosts" class="form-control" rows="3" placeholder="Ejemplo: 50,20,10,6"></textarea>
            <small class="text-muted">Se asignarán en orden descendente (primero las subredes más grandes).</small>
        </div>
        <button class="btn btn-success" onclick="calcularVLSM()">Calcular VLSM</button>
        <div id="vlsm-error" class="text-danger mt-2"></div>
        <table class="table table-bordered mt-4">
            <thead>
            <tr>
                <th>#</th>
                <th>Hosts requeridos</th>
                <th>Red / Prefijo</th>
                <th>Máscara</th>
                <th>Usables</th>
                <th>Broadcast</th>
            </tr>
            </thead>
            <tbody id="vlsm-resultados"></tbody>
        </table>
    </div>

    <!-- Contenedores de Conversores -->
    <div class="converter-container">
        <div class="converter">
            <h3>Binario a Decimal/Hexadecimal</h3>
            <div class="form-group">
                <label for="binary-input" id="bit-counter">Número Binario</label>
                <input type="text" class="form-control" id="binary-input" oninput="validarBits(event)" placeholder="Ingrese solo bits (0s y 1s)">
            </div>
            <button class="btn btn-secondary" onclick="convertirBinario()">Convertir</button>
            <table class="table table-bordered mt-3">
                <thead>
                <tr>
                    <th>Decimal</th>
                    <th>Hexadecimal</th>
                </tr>
                </thead>
                <tbody id="binario-a-decimal"></tbody>
            </table>
        </div>

        <div class="converter">
            <h3>Decimal a Binario/Hexadecimal</h3>
            <div class="form-group">
                <label for="decimal-input">Número Decimal</label>
                <input type="number" class="form-control" id="decimal-input" placeholder="Ingrese un número decimal">
            </div>
            <button class="btn btn-secondary" onclick="convertirDecimal()">Convertir</button>
            <table class="table table-bordered mt-3">
                <thead>
                <tr>
                    <th>Binario</th>
                    <th>Hexadecimal</th>
                </tr>
                </thead>
                <tbody id="decimal-a-binario"></tbody>
            </table>
        </div>
    </div>
</div>

<!-- Archivos JS al final del body -->
<script src="https://walthercurodelacruz.github.io/cdn-repo-projects/comandos.js"></script>
<script src="logica.js"></script>
</body>
</html>

10. logica.js

Manejará la logica y matemática de nuestra aplicación.

// Obtener IP pública
async function traerIpPublica() {
    try {
        const publicIpResponse = await fetch('https://api.ipify.org?format=json');
        const publicIpData = await publicIpResponse.json();
        document.getElementById('public-ip').innerText = publicIpData.ip;
    } catch (error) {
        document.getElementById('public-ip').innerText = "Error al obtener IP pública.";
    }
}

window.onload = () => {
    traerIpPublica();
};

// Conversión IP ↔ Entero
function esteroaIp(num) {
    return [
        (num >>> 24) & 255,
        (num >>> 16) & 255,
        (num >>> 8) & 255,
        num & 255
    ].join('.');
}

function ipaEntero(ip) {
    return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
}

function calcularMascara(prefix) {
    const mask = (0xFFFFFFFF >>> (32 - prefix)) << (32 - prefix);
    return [
        (mask >>> 24) & 255,
        (mask >>> 16) & 255,
        (mask >>> 8) & 255,
        mask & 255
    ].join('.');
}

// Subredes FLSM
function calcularSubnets() {
    const networkInput = document.getElementById('network').value;
    const subnetCountInput = document.getElementById('subnets');
    const subnetCount = parseInt(subnetCountInput.value);
    const resultBody = document.getElementById('resultados');
    resultBody.innerHTML = "";

    if (!networkInput || !subnetCount) {
        resultBody.innerHTML = "<tr><td colspan='4' class='text-danger'>Por favor ingresa una dirección de red válida y el número de subredes.</td></tr>";
        return;
    }

    const [networkAddress, prefix] = networkInput.split('/');
    const prefixInt = parseInt(prefix);
    const newPrefix = prefixInt + Math.ceil(Math.log2(subnetCount));
    const subnetSize = Math.pow(2, 32 - newPrefix);
    const subnetMask = calcularMascara(newPrefix);
    let currentNetwork = ipaEntero(networkAddress);

    for (let i = 0; i < subnetCount; i++) {
        const network = esteroaIp(currentNetwork);
        const broadcast = esteroaIp(currentNetwork + subnetSize - 1);
        resultBody.innerHTML += `<tr>
            <td>Subred ${i + 1}</td>
            <td>${network}/${newPrefix}</td>
            <td>${broadcast}</td>
            <td>${subnetMask}</td>
        </tr>`;
        currentNetwork += subnetSize;
    }

    subnetCountInput.value = subnetCount;
}

// Generar opciones de subredes
function desplegarSubnets() {
    const networkInput = document.getElementById('network').value;
    const subnetCountInput = document.getElementById('subnets');
    const subnetError = document.getElementById('subnet-error');
    const prefixMatch = networkInput.match(/\/(\d+)$/);

    if (prefixMatch) {
        const prefix = parseInt(prefixMatch[1]);
        const maxSubnets = Math.pow(2, 32 - prefix);
        subnetCountInput.innerHTML = `<option value="">Seleccione una opción</option>`;
        for (let i = 1; i <= maxSubnets / 2; i *= 2) {
            subnetCountInput.innerHTML += `<option value="${i}">${i}</option>`;
        }
        subnetError.innerText = "";
    } else {
        subnetCountInput.innerHTML = `<option value="">Seleccione una opción</option>`;
        subnetError.innerText = "Por favor ingresa una dirección de red válida con prefijo.";
    }
}

// Validar entrada binaria
function validarBits(event) {
    const input = event.target;
    const bits = input.value.replace(/[^01]/g, '');
    input.value = bits;
    document.getElementById('bit-counter').innerText = `Bits ingresados: ${bits.length}`;
}

// Conversores
function convertirBinario() {
    const binaryInput = document.getElementById('binary-input').value;
    const decimal = parseInt(binaryInput, 2);
    const hexadecimal = isNaN(decimal) ? 'Inválido' : decimal.toString(16).toUpperCase();

    const conversionBody = document.getElementById('binario-a-decimal');
    conversionBody.innerHTML = `<tr>
        <td>${isNaN(decimal) ? 'Inválido' : decimal}</td>
        <td>${hexadecimal}</td>
    </tr>`;
}

function convertirDecimal() {
    const decimalInput = document.getElementById('decimal-input').value;
    const number = parseInt(decimalInput);
    const binary = isNaN(number) ? 'Inválido' : number.toString(2);
    const hexadecimal = isNaN(number) ? 'Inválido' : number.toString(16).toUpperCase();

    const decimalConversionBody = document.getElementById('decimal-a-binario');
    decimalConversionBody.innerHTML = `<tr>
        <td>${binary}</td>
        <td>${hexadecimal}</td>
    </tr>`;
}

// Modo oscuro
function modoOscuro() {
    document.body.classList.toggle('dark-mode');
}

// 🔍 Búsqueda dinámica de comandos
function buscarComandoDinamico() {
    const input = document.getElementById('buscar-comando').value.toLowerCase().trim();
    const resultadoDiv = document.getElementById('resultado-comando');
    resultadoDiv.innerHTML = "";
    resultadoDiv.style.display = 'none';

    if (input === "") return;

    const grupos = [
        window.comandosCisco,
        window.comandosLinux,
        window.comandosWindows,
        window.comandosGit,
        window.comandosSQL,
        window.comandosDocker,
        window.comandosPython,
        window.comandosADB,
        window.comandosNmap,
        window.comandosBash,
        window.comandosArduino,
        window.comandosSeguridad,
        window.comandosBackend
    ];

    let resultadosEncontrados = 0;

    for (const grupo of grupos) {
        for (const clave in grupo) {
            if (clave.toLowerCase().includes(input)) {
                resultadosEncontrados++;
                resultadoDiv.innerHTML += `
                    <div class="mb-3">
                        <strong>${clave}</strong><br>
                        ${grupo[clave].descripcion}<br>
                        <code>${grupo[clave].ejemplo}</code>
                    </div>`;
            }
        }
    }

    if (resultadosEncontrados > 0) {
        resultadoDiv.style.display = 'block';
    } else {
        resultadoDiv.innerHTML = `<span class="text-danger">No se encontró el comando "${input}".</span>`;
        resultadoDiv.style.display = 'block';
    }
}

// ===================== VLSM =====================
function calcularVLSM() {
    const networkInput = document.getElementById('vlsm-network').value.trim();
    const hostsInput = document.getElementById('vlsm-hosts').value.trim();
    const resultBody = document.getElementById('vlsm-resultados');
    const errorDiv = document.getElementById('vlsm-error');
    resultBody.innerHTML = "";
    errorDiv.innerText = "";

    if (!networkInput || !hostsInput) {
        errorDiv.innerText = "Debes ingresar una red base y la lista de hosts.";
        return;
    }

    const [networkAddress, prefix] = networkInput.split('/');
    if (!prefix) {
        errorDiv.innerText = "Debes ingresar la red base en formato CIDR (ej: 192.168.1.0/24).";
        return;
    }
    const prefixInt = parseInt(prefix);

    // Parsear lista de hosts
    let hosts = hostsInput.split(",").map(h => parseInt(h.trim())).filter(h => !isNaN(h) && h > 0);
    if (hosts.length === 0) {
        errorDiv.innerText = "Lista de hosts inválida.";
        return;
    }

    // Ordenar de mayor a menor
    hosts.sort((a, b) => b - a);

    let currentIP = ipaEntero(networkAddress);
    const maxHosts = Math.pow(2, 32 - prefixInt);

    for (let i = 0; i < hosts.length; i++) {
        const needed = hosts[i] + 2; // red + broadcast
        let bits = Math.ceil(Math.log2(needed));
        let subnetSize = Math.pow(2, bits);
        let newPrefix = 32 - bits;
        let subnetMask = calcularMascara(newPrefix);

        if (subnetSize > maxHosts) {
            errorDiv.innerText = "No hay suficientes direcciones para asignar todas las subredes.";
            return;
        }

        const network = esteroaIp(currentIP);
        const broadcast = esteroaIp(currentIP + subnetSize - 1);
        const usable = subnetSize - 2;

        resultBody.innerHTML += `<tr>
            <td>${i + 1}</td>
            <td>${hosts[i]}</td>
            <td>${network}/${newPrefix}</td>
            <td>${subnetMask}</td>
            <td>${usable}</td>
            <td>${broadcast}</td>
        </tr>`;

        currentIP += subnetSize;
    }
}

11. estilos.css

Nada que explicar, todos sabemos que son los CSS, pero si por algun motivo te saltaste ese laboratorio aqui lo tienes.

body {
    background-color: #ffffff;
    color: #000000;
    transition: background-color 0.3s, color 0.3s;
}

body.dark-mode {
    background-color: #121212;
    color: #ffffff;
}

.main-container {
    max-width: 800px;
    margin: auto;
    padding: 20px;
}

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}

.header h1 {
    font-size: 1.5rem;
    margin: 0;
}

#public-ip {
    font-weight: bold;
}

.btn-dark-mode {
    background-color: #e5322b;
    color: #fff;
    border: none;
    border-radius: 50%;
    width: 35px;
    height: 35px;
    font-size: 1.2rem;
    display: flex;
    align-items: center;
    justify-content: center;
}

.section-container {
    background-color: #f9f9f9;
    padding: 20px;
    border-radius: 10px;
    margin-bottom: 20px;
    box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1);
}

.btn-calculate {
    background-color: #e5322b;
    color: #fff;
    border: none;
    margin-top: 10px;
    width: 100%;
}

.converter-container {
    display: flex;
    justify-content: space-between;
    gap: 20px;
}

.converter {
    flex: 1;
    background-color: #f9f9f9;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1);
}

.converter h3 {
    font-size: 1rem;
    margin-bottom: 15px;
}

.table {
    background-color: #ffffff;
    border-collapse: collapse;
}

.table th, .table td {
    border: 1px solid #ddd;
    text-align: center;
    font-size: 0.9rem;
    padding: 8px;
}

input:focus, select:focus {
    outline: none;
    border-color: #e5322b;
}

.dark-mode .btn-dark-mode {
    background-color: #333;
    color: #e5322b;
}

.dark-mode .btn-calculate,
.dark-mode .btn-secondary {
    background-color: #e5322b;
    color: #fff;
}

.dark-mode .section-container,
.dark-mode .converter {
    background-color: #333;
    color: #fff;
}

/* Media Query para pantallas pequeñas (como iPhone 5) */
@media (max-width: 480px) {
    .main-container {
        padding: 10px;
    }

    .header {
        flex-direction: column;
        align-items: center;
    }

    .header h1 {
        font-size: 1.2rem;
        text-align: center;
    }

    #public-ip {
        text-align: center;
        margin-top: 10px;
    }

    .btn-dark-mode {
        margin-top: 10px;
        width: 30px;
        height: 30px;
        font-size: 1rem;
    }

    .section-container, .converter {
        padding: 15px;
    }

    .converter h3 {
        font-size: 0.9rem;
    }

    .btn-calculate {
        font-size: 0.9rem;
    }

    .table th, .table td {
        font-size: 0.8rem;
        padding: 5px;
    }

    .converter-container {
        flex-direction: column;
    }
}

#descripcion-comando {
    font-size: 0.9rem;
    line-height: 1.6;
    background-color: #f8f9fa;
    padding: 15px;
    border-left: 5px solid #e5322b;
    white-space: pre-wrap;
}

12. build.gradle.kts

Uno de los archivos mas importantes de este y de la mayoría de proyectos a desarrollar en Android, aqui definimos dependencias, librerias, versiones de paquetes, etc.

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
}

android {
    namespace = "com.example.repobit"
    compileSdk = 36 //requerido por core-ktx actual

    defaultConfig {
        applicationId = "com.example.repobit"
        minSdk = 23
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = "17"
    }

    buildFeatures {
        viewBinding = true
    }
}

dependencies {
    implementation("androidx.core:core-ktx:1.13.1") // compatible con SDK 36
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
    implementation("com.google.android.material:material:1.11.0")
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

13. Importante

Dentro de ui.theme no debe existir ningun archivo, si por algun motivo se ha generado alguno, eliminarlo’s

En el menú Tools / SDK Manager verificar que tienes las versiones SDK requeridas para este proyecto, de no tenerlas simplemente seleccionarlas y esperar a que Android Studio las descague e instale.

14. Ejecutar

Con todo listo procedemos a conectar nuestro dispositivo Android en modo desarrollador con depuración USB habilitada + instalación por USB si es que deseamos o necesitamos ejecutar la App en nuestro dispositivo, caso contrario podemos usar la VM emulada que nos ofrece Android Studio.

Finalmente probar la App y presentar tu informe con los cambios o ajustes solicitados en el laboratorio.