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.