/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afwsamples.testdpc;
import static com.afwsamples.testdpc.policy.PolicyManagementFragment.OVERRIDE_KEY_SELECTION_KEY;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.app.admin.NetworkEvent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.afwsamples.testdpc.common.Util;
import com.afwsamples.testdpc.provision.PostProvisioningTask;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Handles events related to the managed profile.
*/
public class DeviceAdminReceiver extends android.app.admin.DeviceAdminReceiver {
private static final String TAG = "DeviceAdminReceiver";
public static final String ACTION_PASSWORD_REQUIREMENTS_CHANGED =
"com.afwsamples.testdpc.policy.PASSWORD_REQUIREMENTS_CHANGED";
private static final String LOGS_DIR = "logs";
private static final String FAILED_PASSWORD_LOG_FILE =
"failed_pw_attempts_timestamps.log";
private static final int CHANGE_PASSWORD_NOTIFICATION_ID = 101;
private static final int PASSWORD_FAILED_NOTIFICATION_ID = 102;
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_PASSWORD_REQUIREMENTS_CHANGED:
case Intent.ACTION_BOOT_COMPLETED:
updatePasswordQualityNotification(context);
break;
default:
super.onReceive(context, intent);
break;
}
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onSecurityLogsAvailable(Context context, Intent intent) {
Log.i(TAG, "onSecurityLogsAvailable() called");
Toast.makeText(context,
context.getString(R.string.on_security_logs_available),
Toast.LENGTH_LONG)
.show();
}
/*
* TODO: reconsider how to store and present the logs in the future, e.g. save the file into
* internal memory and show the content in a ListView
*/
@TargetApi(Build.VERSION_CODES.O)
@Override
public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
int networkLogsCount) {
Log.i(TAG, "onNetworkLogsAvailable(), batchToken: " + batchToken
+ ", event count: " + networkLogsCount);
DevicePolicyManager dpm =
(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
List<NetworkEvent> events = null;
try {
events = dpm.retrieveNetworkLogs(getComponentName(context), batchToken);
} catch (SecurityException e) {
Log.e(TAG,
"Exception while retrieving network logs batch with batchToken: " + batchToken, e);
}
if (events == null) {
Log.e(TAG, "Failed to retrieve network logs batch with batchToken: " + batchToken);
Toast.makeText(context,
context.getString(R.string.on_network_logs_available_failure, batchToken),
Toast.LENGTH_LONG)
.show();
return;
}
Toast.makeText(context,
context.getString(R.string.on_network_logs_available_success, batchToken),
Toast.LENGTH_LONG)
.show();
ArrayList<String> loggedEvents = new ArrayList<String>();
events.forEach(event -> loggedEvents.add(event.toString()));
new EventSavingTask(context, loggedEvents).execute();
}
private static class EventSavingTask extends AsyncTask<Void, Void, Void> {
private Context mContext;
private List<String> mLoggedEvents;
public EventSavingTask(Context context, ArrayList<String> loggedEvents) {
mContext = context;
mLoggedEvents = loggedEvents;
}
@Override
protected Void doInBackground(Void... params) {
String filename = "network_logs_"
+ new Date().toString().replaceAll("\\s+","_") + ".txt";
File file = new File(mContext.getExternalFilesDir(null), filename);
try (OutputStream os = new FileOutputStream(file)) {
for (String event : mLoggedEvents) {
os.write((event + "\n").getBytes());
}
Log.d(TAG, "Saved network logs to file: " + filename);
} catch (IOException e) {
Log.e(TAG, "Failed saving network events to file" + filename, e);
}
return null;
}
}
@Override
public void onProfileProvisioningComplete(Context context, Intent intent) {
PostProvisioningTask task = new PostProvisioningTask(context);
if (!task.performPostProvisioningOperations(intent)) {
return;
}
Intent launchIntent = task.getPostProvisioningLaunchIntent(intent);
if (launchIntent != null) {
context.startActivity(launchIntent);
} else {
Log.e(TAG, "DeviceAdminReceiver.onProvisioningComplete() invoked, but ownership "
+ "not assigned");
Toast.makeText(context, R.string.device_admin_receiver_failure, Toast.LENGTH_LONG)
.show();
}
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onBugreportSharingDeclined(Context context, Intent intent) {
Log.i(TAG, "Bugreport sharing declined");
Util.showNotification(context, R.string.bugreport_title,
context.getString(R.string.bugreport_sharing_declined),
Util.BUGREPORT_NOTIFICATION_ID);
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onBugreportShared(final Context context, Intent intent,
final String bugreportFileHash) {
Log.i(TAG, "Bugreport shared, hash: " + bugreportFileHash);
final Uri bugreportUri = intent.getData();
final PendingResult result = goAsync();
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
File outputBugreportFile;
String message;
InputStream in;
OutputStream out;
try {
ParcelFileDescriptor mInputPfd = context.getContentResolver()
.openFileDescriptor(bugreportUri, "r");
in = new FileInputStream(mInputPfd.getFileDescriptor());
outputBugreportFile = new File(context.getExternalFilesDir(null),
bugreportUri.getLastPathSegment());
out = new FileOutputStream(outputBugreportFile);
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
out.close();
message = context.getString(R.string.received_bugreport,
outputBugreportFile.getPath(), bugreportFileHash);
} catch (IOException e) {
Log.e(TAG, e.getMessage());
message = context.getString(R.string.received_bugreport_failed_retrieval);
}
return message;
}
@Override
protected void onPostExecute(String message) {
Util.showNotification(context, R.string.bugreport_title,
message, Util.BUGREPORT_NOTIFICATION_ID);
result.finish();
}
}.execute();
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onBugreportFailed(Context context, Intent intent, int failureCode) {
String failureReason;
switch (failureCode) {
case BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE:
failureReason = context.getString(
R.string.bugreport_failure_file_no_longer_available);
break;
case BUGREPORT_FAILURE_FAILED_COMPLETING:
//fall through
default:
failureReason = context.getString(
R.string.bugreport_failure_failed_completing);
}
Log.i(TAG, "Bugreport failed: " + failureReason);
Util.showNotification(context, R.string.bugreport_title,
context.getString(R.string.bugreport_failure_message, failureReason),
Util.BUGREPORT_NOTIFICATION_ID);
}
@TargetApi(Build.VERSION_CODES.O)
@Override
public void onUserAdded(Context context, Intent intent, UserHandle newUser) {
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
String message = context.getString(R.string.on_user_added_message,
userManager.getSerialNumberForUser(newUser));
Log.i(TAG, message);
Util.showNotification(context, R.string.on_user_added_title,
message,
Util.USER_ADDED_NOTIFICATION_ID);
}
@TargetApi(Build.VERSION_CODES.O)
@Override
public void onUserRemoved(Context context, Intent intent, UserHandle removedUser) {
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
String message = context.getString(R.string.on_user_removed_message,
userManager.getSerialNumberForUser(removedUser));
Log.i(TAG, message);
Util.showNotification(context, R.string.on_user_removed_title, message,
Util.USER_REMOVED_NOTIFICATION_ID);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onSystemUpdatePending(Context context, Intent intent, long receivedTime) {
if (receivedTime != -1) {
DateFormat sdf = new SimpleDateFormat("hh:mm:ss dd/MM/yyyy");
String timeString = sdf.format(new Date(receivedTime));
Toast.makeText(context, "System update received at: " + timeString,
Toast.LENGTH_LONG).show();
} else {
// No system update is currently available on this device.
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public String onChoosePrivateKeyAlias(Context context, Intent intent, int uid, Uri uri,
String alias) {
if (uid == Process.myUid()) {
// Always show the chooser if we were the one requesting the cert.
return null;
}
String chosenAlias = PreferenceManager.getDefaultSharedPreferences(context)
.getString(OVERRIDE_KEY_SELECTION_KEY, null);
if (!TextUtils.isEmpty(chosenAlias)) {
Toast.makeText(context, "Substituting private key alias: \"" + chosenAlias + "\"",
Toast.LENGTH_LONG).show();
return chosenAlias;
} else {
return null;
}
}
/**
* @param context The context of the application.
* @return The component name of this component in the given context.
*/
public static ComponentName getComponentName(Context context) {
return new ComponentName(context.getApplicationContext(), DeviceAdminReceiver.class);
}
@Deprecated
@Override
public void onPasswordExpiring(Context context, Intent intent) {
onPasswordExpiring(context, intent, Process.myUserHandle());
}
@TargetApi(Build.VERSION_CODES.O)
// @Override
public void onPasswordExpiring(Context context, Intent intent, UserHandle user) {
if (!Process.myUserHandle().equals(user)) {
// This password expiration was on another user, for example a parent profile. Skip it.
return;
}
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
final long timeNow = System.currentTimeMillis();
final long timeAdminExpires =
devicePolicyManager.getPasswordExpiration(getComponentName(context));
final boolean expiredBySelf = (timeNow >= timeAdminExpires && timeAdminExpires != 0);
Util.showNotification(context, R.string.password_expired_title,
context.getString(expiredBySelf
? R.string.password_expired_by_self
: R.string.password_expired_by_others),
Util.PASSWORD_EXPIRATION_NOTIFICATION_ID);
}
@Deprecated
@Override
public void onPasswordFailed(Context context, Intent intent) {
onPasswordFailed(context, intent, Process.myUserHandle());
}
@TargetApi(Build.VERSION_CODES.O)
// @Override
public void onPasswordFailed(Context context, Intent intent, UserHandle user) {
if (!Process.myUserHandle().equals(user)) {
// This password failure was on another user, for example a parent profile. Ignore it.
return;
}
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
/*
* Post a notification to show:
* - how many wrong passwords have been entered;
* - how many wrong passwords need to be entered for the device to be wiped.
*/
int attempts = devicePolicyManager.getCurrentFailedPasswordAttempts();
int maxAttempts = devicePolicyManager.getMaximumFailedPasswordsForWipe(null);
String title = context.getResources().getQuantityString(
R.plurals.password_failed_attempts_title, attempts, attempts);
ArrayList<Date> previousFailedAttempts = getFailedPasswordAttempts(context);
Date date = new Date();
previousFailedAttempts.add(date);
Collections.sort(previousFailedAttempts, Collections.<Date>reverseOrder());
try {
saveFailedPasswordAttempts(context, previousFailedAttempts);
} catch (IOException e) {
Log.e(TAG, "Unable to save failed password attempts", e);
}
String content = maxAttempts == 0
? context.getString(R.string.password_failed_no_limit_set)
: context.getResources().getQuantityString(
R.plurals.password_failed_attempts_content, maxAttempts, maxAttempts);
Notification.Builder warn = new Notification.Builder(context)
.setSmallIcon(R.drawable.ic_launcher)
.setTicker(title)
.setContentTitle(title)
.setContentText(content)
.setContentIntent(PendingIntent.getActivity(context, /* requestCode */ -1,
new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD), /* flags */ 0));
Notification.InboxStyle inboxStyle = new Notification.InboxStyle();
inboxStyle.setBigContentTitle(title);
final DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance();
for(Date d : previousFailedAttempts) {
inboxStyle.addLine(dateFormat.format(d));
}
warn.setStyle(inboxStyle);
NotificationManager nm = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(PASSWORD_FAILED_NOTIFICATION_ID, warn.getNotification());
}
@Deprecated
@Override
public void onPasswordSucceeded(Context context, Intent intent) {
onPasswordSucceeded(context, intent, Process.myUserHandle());
}
@TargetApi(Build.VERSION_CODES.O)
// @Override
public void onPasswordSucceeded(Context context, Intent intent, UserHandle user) {
if (Process.myUserHandle().equals(user)) {
logFile(context).delete();
}
}
@Deprecated
@Override
public void onPasswordChanged(Context context, Intent intent) {
onPasswordChanged(context, intent, Process.myUserHandle());
}
@TargetApi(Build.VERSION_CODES.O)
// @Override
public void onPasswordChanged(Context context, Intent intent, UserHandle user) {
if (Process.myUserHandle().equals(user)) {
updatePasswordQualityNotification(context);
}
}
private static File logFile(Context context) {
File parent = context.getDir(LOGS_DIR, Context.MODE_PRIVATE);
return new File(parent, FAILED_PASSWORD_LOG_FILE);
}
private static ArrayList<Date> getFailedPasswordAttempts(Context context) {
File logFile = logFile(context);
ArrayList<Date> result = new ArrayList<Date>();
if(!logFile.exists()) {
return result;
}
FileInputStream fis = null;
try {
fis = new FileInputStream(logFile);
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
String line = null;
while ((line = br.readLine()) != null && line.length() > 0) {
result.add(new Date(Long.parseLong(line)));
}
br.close();
} catch (IOException e) {
Log.e(TAG, "Unable to read failed password attempts", e);
} finally {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close failed password attempts log file", e);
}
}
}
return result;
}
private static void saveFailedPasswordAttempts(Context context, ArrayList<Date> attempts)
throws IOException {
File logFile = logFile(context);
if(!logFile.exists()) {
logFile.createNewFile();
}
FileOutputStream fos = new FileOutputStream(logFile);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
for(Date date : attempts) {
bw.write(Long.toString(date.getTime()));
bw.newLine();
}
bw.close();
}
private static void updatePasswordQualityNotification(Context context) {
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (!devicePolicyManager.isProfileOwnerApp(context.getPackageName())
&& !devicePolicyManager.isDeviceOwnerApp(context.getPackageName())) {
// Only try to update the notification if we are a profile or device owner.
return;
}
NotificationManager nm = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
if (!devicePolicyManager.isActivePasswordSufficient()) {
Notification.Builder warn = new Notification.Builder(context)
.setOngoing(true)
.setSmallIcon(R.drawable.ic_launcher)
.setTicker(context.getText(R.string.password_not_compliant_title))
.setContentTitle(context.getText(R.string.password_not_compliant_title))
.setContentText(context.getText(R.string.password_not_compliant_content))
.setContentIntent(PendingIntent.getActivity(context, /*requestCode*/ -1,
new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD), /*flags*/ 0));
nm.notify(CHANGE_PASSWORD_NOTIFICATION_ID, warn.getNotification());
} else {
nm.cancel(CHANGE_PASSWORD_NOTIFICATION_ID);
}
}
}